Database transactions/cs

 Databázové transakce využívá MediaWiki k zachování konzistence databáze a tím i ke zlepšení jejího výkonu.

Některé obecné informace o databázových transakcích naleznete zde:


 * Na Wikipedii viz Databázová transakce a ACID
 * Pro MySQL viz transakční výpis a transakční model InnoDB



Rozsah transakce
Nejprve bychom měli rozlišit dva typy metod:


 * S vnějším rozsahem transakce: Metody, které strukturálně jasně zaručují, že nemají žádné volající v řetězci, kteří provádějí transakční operace (kromě zabalení metody okolo transakce jménem uvedené metody). O takových metodách se říká, že vlastní cyklus transakce. Místa, která mají tento rozsah, jsou metoda Skripty údržby, metoda  tříd Job a metoda  tříd DeferrableUpdate. Když jsou tyto metody spuštěny, žádní volající dále v zásobníku volání nebudou mít žádnou transakční aktivitu kromě případného definování počáteční/koncové hranice. O volajícím z vnějšího rozsahu, u kterého je také strukturálně zaručeno, že začne bez deklarovaného cyklu transakce, se říká, že má definující rozsah transakce. To znamená, že metody s vnějším rozsahem transakce jsou volné pro zahájení a ukončení transakcí (s ohledem na některá upozornění popsaná níže). Volající v zásobníku nemají vnější rozsah a očekává se, že tuto skutečnost budou respektovat.
 * S nejasným/vnitřním rozsahem transakce: To jsou metody, u kterých není jasně zaručeno, že mají vnější rozsah transakce. Takové metody nevlastní cyklus transakce, pokud takový existuje. Toto je většina metod v jádru MediaWiki a rozšířeních. Do této kategorie spadají různé metody, jako jsou třídy model/abstrakce, třídy užitných vlastností, objekty DAO, obslužné rutiny háčku, třídy obchodní/kontrolní logiky a tak dále. Tyto metody nesmí volně spouštět/ukončit transakce a musí používat pouze sémantiku transakcí, která podporuje vnořování. Pokud potřebují provést nějaké aktualizace po potvrzení, musí zaregistrovat metodu zpětného volání po potvrzení.

Pokud je kolo transakce zahájeno přes, pak se nazývá explicitní kolo transakce. V opačném případě, pokud  zabalí jakoukoli aktivitu dotazu do transakčního cyklu, jak je tomu obvykle během webových požadavků, pak se to nazývá implicitní cyklus transakce. Takové cykly jsou bez vlastníka a provádí je MediaWiki při vypnutí prostřednictvím. Volající mohou zahájit explicitní cykly uprostřed implicitních cyklů, v takovém případě budou všechny čekající zápisy do databáze potvrzeny, když se explicitní cyklus potvrdí.



Základní použití transakce
MediaWiki používá transakce několika způsoby:


 * 1) Používání "tradičních" párů / k ochraně kritických sekcí a ujištění, že jsou zavázány. Vnořené transakce nejsou podporovány. Toto by mělo být použito pouze u volajících, kteří mají vnější rozsah transakcí a ovlivňují pouze jednu databázi (přičemž také počítají s všemi možnými obslužnými osobami). Platné metody zahrnují zpětná volání na  nebo , kde je aktualizována pouze jedna DB a nejsou aktivovány žádné háčky. Vždy spojte každý  s.
 * 2) Použití párů / k ochraně kritických sekcí, aniž byste věděli, kdy se zadají. Vnořené sekce jsou plně podporovány. Ty lze použít kdekoli, ale musí být správně vnořené (např. neotevírejte sekci a poté ji nezavírejte před příkazem "return"). Ve skriptech údržby, když nejsou otevřené žádné atomické sekce, dojde k potvrzení. Pokud je však nastaven příznak , atomické sekce se připojí k hlavní sadě transakcí  . Uvnitř zpětných volání   nebo  je   pro zadanou databázi vypnuto, což znamená, že  se potvrdí, jakmile v těchto zpětných voláních nebudou žádné sekce.
 * 3) Použití implicitních otočných transakčních cyklů, pokud je povoleno   (toto je výchozí případ u webových požadavků, ale ne pro režim údržby nebo testy jednotek). První zápis při každém připojení databáze bez transakce spustí BEGIN. COMMIT nastane na konci požadavku pro všechna připojení databází s čekajícími zápisy. Pokud je zapsáno více databází, které mají nastaveno , pak všechny provedou svůj krok potvrzení v rychlém sledu na konci požadavku. To maximalizuje atomičnost transakcí napříč DB. Všimněte si, že optimistická kontrola souběžnosti (REPEATABLE-READ nebo SERIALIZABLE v PostgreSQL) to může poněkud podkopat, protože SERIALIZATION FAILURE může nastat na správné podmnožině odevzdání, i když se zdálo, že všechny zápisy byly úspěšné. V každém případě   snižuje počet potvrzení, což může pomoci výkonu webu (snížením  volání) a znamená, že všechny zápisy v požadavku jsou obvykle buď potvrzeny nebo odvolány společně.
 * 4) Použití explicitních pivotovaných transakcí zaokrouhluje přes  a . Tyto cykly jsou účinné v režimu webu i CLI a mají stejnou sémantiku jako jejich implicitní protějšky, s výjimkou následujících aspektů:
 * 5) * Volání z metody, která nezahájila cyklus, vyvolá chybu.
 * 6) * Po dokončení potvrdí všechny prázdné transakce v hlavních databázích a vymažou všechny REPEATABLE-READ snímky. To zajišťuje, že volající, kteří se spoléhají na v nastavení jednoho DB, budou mít stále čerstvé snímky pro připojení na  . Zajišťuje také, že posluchač transakcí nastavený na  vidí všechny databáze ve stavu nečinnosti transakce, což mu umožňuje spouštět odložené aktualizace.
 * 7) Pokud je v kterémkoli okamžiku vyvolána výjimka a není zachycena ničím jiným,   ji zachytí a vrátí zpět všechna databázová spojení s transakcemi. To je velmi užitečné v kombinaci s.



Chyby zneužití transakce
Různá zneužití transakcí způsobí výjimky nebo varování, například:


 * Vnoření volání vyvolá výjimku
 * Volání na transakci jiné metody, která začíná, vyvolá výjimku.
 * Volání nebo, když je atomová sekce aktivní, vyvolá výjimku.
 * Použití a  má analogická omezení jako výše.
 * Volání, když není otevřena žádná transakce, vyvolá varování.
 * a očekávají   jako argument a jejich hodnota se musí shodovat na každé úrovni vnoření atomových sekcí. Pokud se neshoduje, je vyvolána výjimka.
 * Volání nebo, když je nastaveno  , může zaznamenat varování a neoperaci.
 * Volání, když je  nastaveno, vyvolá chybu a spustí rollback všech DB.
 * Volání, zatímco v transakci stále čekají zápisy, bude mít za následek výjimku.
 * Zachycení výjimek,   nebo   bez volání  může vést k výjimce.
 * Pokus o použití nebo  v , která je nastavena na použití podpory transakcí, kterou třída poskytuje, může způsobit výjimky. Tím propadne vnější rozsah, takže více takových aktualizací může být součástí jednoho cyklu transakce.



Vhodné kontexty pro zápisové dotazy
Kromě staršího kódu by se transakce zápisu do databáze (včetně dotazů v režimu automatického potvrzení) v MediaWiki měly dít pouze během provádění:
 * HTTP POST požadavky na SpecialPages, kde ve třídě PHP vrátí hodnotu true
 * HTTP POST požadavky na stránky Action, kde ve třídě PHP vrátí hodnotu true
 * Požadavky HTTP POST na moduly API, kde ve třídě PHP vrací hodnotu true
 * Úlohy v JobRunner (který používá interní požadavky HTTP POST na webu)
 * Skripty údržby se spouštějí z příkazového řádku

Pro zápisy v kontextu požadavků HTTP GET použijte frontu úloh.

Pro zápisy, ke kterým nemusí dojít před odesláním HTTP odpovědi klientovi, mohou být odloženy přes s příslušnou podtřídou   (obvykle   nebo  ) nebo přes  se zpětným voláním. Fronta úloh by se měla pro takové aktualizace používat, když jsou pomalé nebo příliš náročné na prostředky, aby se spouštěly v běžných vláknech požadavků na nevyhrazených serverech.



Určení atomové skupiny zápisů
Když je sada dotazů úzce spojena při určování jednotky zápisů databáze, měli bychom použít atomickou sekci. Například:

Dalším způsobem, jak to udělat, je použít, což je užitečné, pokud existuje mnoho příkazů return.



Rozdělení zápisů do více transakcí


Situace
Předpokládejme, že máte nějaký kód, který aplikuje některé aktualizace databáze. Po dokončení metody můžete chtít:


 * a) Aplikovat některé vysoce sporné aktualizace databáze na konci transakce, aby nedržely uzamčení příliš dlouho
 * b) Aplikovat další aktualizace databáze, které jsou pomalé, neaktuální a nepotřebují 100% atomicitu (např. mohou být obnoveny)

Metody
V některých případech může kód chtít vědět, že data jsou potvrzena, než budete pokračovat k dalším krokům. Jedním ze způsobů, jak toho dosáhnout, je umístit další kroky zpětného volání na,  nebo. Poslední dva jsou, které se poněkud liší v režimu údržby a požadavků na web/úlohu:


 * Ve webových požadavcích a úlohách (včetně úloh v režimu CLI) se odložené aktualizace spouštějí po potvrzení hlavního kola transakce. Každá aktualizace je zabalena do vlastního transakčního cyklu, i když  zakáže   na zadaném popisovači databáze a odevzdá každý dotaz za běhu. Pokud odložené aktualizace zařadí do fronty další odložené aktualizace, jednoduše se přidají další kola transakcí.
 * Ve skriptech údržby se odložené aktualizace spouštějí po potvrzení jakékoli transakce v lokální (např. "aktuální wiki") databáze (nebo okamžitě, pokud neexistuje žádná otevřená transakce). Odložené aktualizace nelze jednoduše automaticky odložit, dokud nebudou aktivní žádné transakce, protože to může vést k chybám z nedostatku paměti u dlouho běžících skriptů, kde nějaká (možná "cizí wiki") databáze má vždy aktivní transakci (což by jinak bylo ideální). To je důvod, proč jsou odložené aktualizace podivně vázány pouze na místní master databáze. Bez ohledu na to, protože má vnější rozsah transakce a   je pro ně vypnuto, obvykle nedává smysl přímo volat  z metody, protože kód by se mohl spustit okamžitě.

Jakákoli metoda s vnějším rozsahem transakcí má možnost zavolat na   singleton k vyprázdnění všech aktivních transakcí ve všech databázích. To zajišťuje, že všechny čekající aktualizace budou potvrzeny před provedením dalších řádků kódu. Také, pokud taková metoda chce zahájit cyklus transakce, může použít na singleton, provést aktualizace a pak zavolat  na singleton. Toto nastaví  na aktuální a nové DB handles během cyklu, což způsobí spuštění implicitních transakcí, dokonce i v CLI režimu. Značka  se po skončení cyklu vrátí do původního stavu. Všimněte si, že podobně jako a  nelze vnořit cykly transakcí.

Všimněte si, že některé databáze, jako ty, které zpracovávají, mají obvykle   vypnuté. To znamená, že zůstávají v režimu automatického potvrzení i během transakčních kol.

Příklad
Pro výše uvedené případy uvádíme několik technik, jak je zvládnout:

Případ A:

Případ B:



Situace
Zápis dotazů (např. operace vytvoření, aktualizace, odstranění), které ovlivňují mnoho řádků nebo mají špatné využití indexu, trvá dlouho, než se dokončí. Horší je, že replikované databáze často používají sériovou replikaci, takže aplikují hlavní transakce jednu po druhé. To znamená, že 10sekundový dotaz UPDATE zablokuje tak dlouho u každé replikované databáze (někdy i více, protože replikové databáze musí zpracovávat provoz čtení a replikovat hlavní zápisy). To vytváří zpoždění, kdy se jiné aktualizace na hlavním serveru chvíli nezobrazují ostatním uživatelům. Také to zpomaluje uživatele provádějící úpravy kvůli, který se snaží čekat, až repliky doběhnou.

Hlavní případy, kdy k tomu může dojít, jsou:


 * a) Pracovní třídy, které provádějí nákladné aktualizace
 * b) Skripty údržby, které provádějí hromadné aktualizace velkých částí tabulek

Další situace, která někdy nastane, je, když aktualizace spustí úlohu a tato úloha musí provést nějaké složité dotazy, aby něco přepočítala. Možná není dobrý nápad provádět dotazy na hlavní databázi, ale replikované databáze mohou být zpožděné a neodrážejí změnu, která spustila úlohu. To vede k následujícímu případu:


 * c) Jobs needing to wait for a single replica DB to catch up so they can query it to determine the updates

A similar scenario can happen with external services. Suppose a service needs to do expensive API queries in order to reflect changes in the wiki's database. So this leaves another case:


 * d) Callers that do updates before notifying an external service that it needs to query the DBs to update itself

Methods
Expensive updates that create lag need to be moved to a  class and that the job's  method should batch the updates, waiting for replicas to catch up between each batch.

Examples
 Case A / B:

 Case C:

 Case D:

Situations
Sometimes changes to the primary data-set demand updates to secondary data stores (that lack BEGIN...COMMIT), for example:


 * a) Enqueue a job that will query some of the affected rows, making the end-user wait on its insertion
 * b) Enqueue a job that will query some of the affected rows, inserting it after the MediaWiki response to the end-user is flushed
 * c) Send a request to a service that will query some of the effected rows, making the end-user wait on the service request
 * d) Send a request to a service that will query some of the effected rows, doing it after the MediaWiki response to the end-user is flushed
 * e) Purge CDN proxy cache for URLs that have content based on the effected rows
 * f) Purge the  entry for a changed row
 * g) Storing a non-derivable text/semi-structured blob to another store
 * h) Storing a non-derivable file to another store
 * i) Account creation hook handler creating an LDAP entry that must accompany the new user
 * j) Updating the database and sending an e-mail to a user's inbox as part of a request by the user

Methods
In general, derivable (e.g. can be regenerated) updates to external stores will use some sort  class or  to be applied post-commit. In cases where the external data is immutable, then it can be referenced by autoincrement ID, UUID, or hash of the externally stored contents; storing the data pre-commit is best in such cases. Updates that do not fall into either category should use, batch all the update to the external store into one transaction if possible, and throw an error if the update fails (which will trigger RDBMS rollback); this reduces the window that things could go wrong and result in inconsistent data.

Examples
 Case A:

Case B:

 Case C:

 Case D:

 Case E:

 Case F:

 Case G:

 Case H:

 Case I:

 Case J:

Use of transaction rollback
The use of should strongly be avoided, since it affects what all the previous executed code did before the rollback. Outer callers might still consider that an operation was successful and attempt additional updates and/or show bogus success page. Any REPEATABLE-READ snapshot is renewed, causing objects with lazy-loading to possibly not find what they were looking for, or to get unexpectedly newer data than the rest of what was loaded. Using is particularly bad since other databases might have related changes and it's easy to forget to roll those back too.

Instead, simply throwing an exception is enough to trigger rollback of all databases due to. There are two special cases to this rule:


 * Exceptions of the type  (used for human-friendly GUI errors) do not trigger rollback on web requests inside of  if thrown before . This lets actions,special pages, and   deferred updates show proper error messages, while auditing and anti-abuse tools can still log updates to the database.
 * Callers might catch  exceptions without either re-throwing them or throwing their own version of the error. Doing so is extremely bad practice and can cause all sorts of problems from partial commits to simply spewing up   errors. Only catch DB errors in order to do some cleanup before re-throwing an error or if the database in question is used exclusively by the code catching errors.

This is how rollback is normally used, as a fail-safe that aborts everything, returns to the initial state, and errors out. However, if directly calling  is truly needed, always use  on the   singleton to make sure all databases are reverted to the initial state of any transaction round.

Debug logging
Several channels (log groups) are used to log DB related errors and warnings:


 * DBQuery
 * DBConnection
 * DBPerformance
 * DBReplication
 * exception

At Wikimedia, these logs can be found by querying logstash.wikimedia.org using +channel:&lt;CHANNEL NAME>.

Old discussions
This is the result of some conversation on the wikitech-l mailing list and subsequent discussion on the Bugzilla. Some relevant discussions are:


 * Nested database transactions
 * Can we kill DBO_TRX? It seems evil!
 * Transaction warning : WikiPage::doDeleteArticleReal
 * Transaction warning : WikiPage::doEdit (User::loadFromDatabase) (TranslateMetadata::get)

In one mail, Tim Starling explained the reasoning behind the DBO_TRX system. Here is a redacted version of his explanation:

DBO_TRX provides the following benefits: * It provides improved consistency of write operations for code which is not transaction-aware, for example rollback-on-error. * It provides a snapshot for consistent reads, which improves application correctness when concurrent writes are occurring. DBO_TRX was introduced when we switched over to InnoDB, along with the introduction of Database::begin and Database::commit. [...]   Initially, I set up a scheme where transactions were "nested", in the sense that begin incremented the transaction level and commit decremented it. When it was decremented to zero, an actual COMMIT was issued. So you would have a call sequence like: * begin -- sends BEGIN * begin -- does nothing * commit -- does nothing * commit -- sends COMMIT This scheme soon proved to be inappropriate, since it turned out that the most important thing for performance and correctness is for an   application to be able to commit the current transaction after some particular query has completed. Database::immediateCommit was introduced to support this use case -- its function was to immediately reduce the transaction level to zero and commit the underlying transaction. When it became obvious that that every Database::commit call should really be Database::immediateCommit, I changed the semantics, effectively renaming Database::immediateCommit to   Database::commit. I removed the idea of nested transactions in   favour of a model of cooperative transaction length management: * Database::begin became effectively a no-op for web requests and was sometimes omitted for brevity. * Database::commit should be called after completion of a sequence of write operations where atomicity is desired, or at the earliest opportunity when contended locks are held. [...]   When transactions too long, you hit performance problems due to lock contention. When transactions are too short, you hit consistency problems when requests fail. The scheme I introduced favours performance over consistency. It resolves conflicts between callers and callees by using the shortest transaction time. I think was an   appropriate choice for Wikipedia, both then and now, and I think it is    probably appropriate for many other medium to high traffic wikis. Savepoints were not available at the time the scheme was introduced. But they are a refinement of the abandoned transaction nesting scheme, not a refinement of the current scheme which is optimised for reducing lock contention. In terms of performance, perhaps it would be feasible to use short transactions with an explicit begin with savepoints for nesting. But then you would lose the consistency benefits of DBO_TRX that I   mentioned at the start of this post. -- Tim Starling