X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Flibs%2Frdbms%2Fdatabase%2FIDatabase.php;h=675ba7f2bcfe23a5d96b49564ba4b948b95bb97f;hp=545c8e36f79fdf79a08ae73a7823e3573dec273c;hb=e9602b246640c4878036e1d27b7f71704ec4bf53;hpb=2c46e066763dfd968e7fc7a594ca700e4a236329 diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 545c8e36f7..675ba7f2bc 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -22,7 +22,6 @@ namespace Wikimedia\Rdbms; use InvalidArgumentException; use Wikimedia\ScopedCallback; use RuntimeException; -use UnexpectedValueException; use stdClass; /** @@ -54,9 +53,11 @@ interface IDatabase { /** @var string Atomic section is cancelable */ const ATOMIC_CANCELABLE = 'cancelable'; - /** @var string Transaction operation comes from service managing all DBs */ + /** @var string Commit/rollback is from outside the IDatabase handle and connection manager */ + const FLUSHING_ONE = ''; + /** @var string Commit/rollback is from the connection manager for the IDatabase handle */ const FLUSHING_ALL_PEERS = 'flush'; - /** @var string Transaction operation comes from the database class internally */ + /** @var string Commit/rollback is from the IDatabase handle internally */ const FLUSHING_INTERNAL = 'flush'; /** @var string Do not remember the prior flags */ @@ -90,7 +91,7 @@ interface IDatabase { const DBO_NOBUFFER = 2; /** @var int Ignore query errors (internal use only!) */ const DBO_IGNORE = 4; - /** @var int Autoatically start transaction on first query (work with ILoadBalancer rounds) */ + /** @var int Automatically start a transaction before running a query if none is active */ const DBO_TRX = 8; /** @var int Use DBO_TRX in non-CLI mode */ const DBO_DEFAULT = 16; @@ -253,7 +254,7 @@ interface IDatabase { public function writesPending(); /** - * Returns true if there is a transaction open with possible write + * Returns true if there is a transaction/round open with possible write * queries or transaction pre-commit/idle callbacks waiting on it to finish. * This does *not* count recurring callbacks, e.g. from setTransactionListener(). * @@ -1472,11 +1473,13 @@ interface IDatabase { /** * Run a callback as soon as the current transaction commits or rolls back. * An error is thrown if no transaction is pending. Queries in the function will run in - * AUTO-COMMIT mode unless there are begin() calls. Callbacks must commit any transactions + * AUTOCOMMIT mode unless there are begin() calls. Callbacks must commit any transactions * that they begin. * * This is useful for combining cooperative locks and DB transactions. * + * @note: do not assume that *other* IDatabase instances will be AUTOCOMMIT mode + * * The callback takes one argument: * - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK) * @@ -1490,16 +1493,24 @@ interface IDatabase { /** * Run a callback as soon as there is no transaction pending. * If there is a transaction and it is rolled back, then the callback is cancelled. - * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls. + * + * When transaction round mode (DBO_TRX) is set, the callback will run at the end + * of the round, just after all peer transactions COMMIT. If the transaction round + * is rolled back, then the callback is cancelled. + * + * Queries in the function will run in AUTOCOMMIT mode unless there are begin() calls. * Callbacks must commit any transactions that they begin. * * This is useful for updates to different systems or when separate transactions are needed. * For example, one might want to enqueue jobs into a system outside the database, but only * after the database is updated so that the jobs will see the data when they actually run. - * It can also be used for updates that easily cause deadlocks if locks are held too long. + * It can also be used for updates that easily suffer from lock timeouts and deadlocks, + * but where atomicity is not essential. * * Updates will execute in the order they were enqueued. * + * @note: do not assume that *other* IDatabase instances will be AUTOCOMMIT mode + * * The callback takes one argument: * - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE) * @@ -1512,10 +1523,15 @@ interface IDatabase { /** * Run a callback before the current transaction commits or now if there is none. * If there is a transaction and it is rolled back, then the callback is cancelled. + * + * When transaction round mode (DBO_TRX) is set, the callback will run at the end + * of the round, just before all peer transactions COMMIT. If the transaction round + * is rolled back, then the callback is cancelled. + * * Callbacks must not start nor commit any transactions. If no transaction is active, * then a transaction will wrap the callback. * - * This is useful for updates that easily cause deadlocks if locks are held too long + * This is useful for updates that easily suffer from lock timeouts and deadlocks, * but where atomicity is strongly desired for these updates and some related updates. * * Updates will execute in the order they were enqueued. @@ -1544,26 +1560,77 @@ interface IDatabase { public function setTransactionListener( $name, callable $callback = null ); /** - * Begin an atomic section of statements + * Begin an atomic section of SQL statements * - * If a transaction has been started already, (optionally) sets a savepoint - * and tracks the given section name to make sure the transaction is not - * committed pre-maturely. This function can be used in layers (with - * sub-sections), so use a stack to keep track of the different atomic - * sections. If there is no transaction, one is started implicitly. + * Start an implicit transaction if no transaction is already active, set a savepoint + * (if $cancelable is ATOMIC_CANCELABLE), and track the given section name to enforce + * that the transaction is not committed prematurely. The end of the section must be + * signified exactly once, either by endAtomic() or cancelAtomic(). Sections can have + * have layers of inner sections (sub-sections), but all sections must be ended in order + * of innermost to outermost. Transactions cannot be started or committed until all + * atomic sections are closed. * - * The goal of this function is to create an atomic section of SQL queries - * without having to start a new transaction if it already exists. + * ATOMIC_CANCELABLE is useful when the caller needs to handle specific failure cases + * by discarding the section's writes. This should not be used for failures when: + * - upsert() could easily be used instead + * - insert() with IGNORE could easily be used instead + * - select() with FOR UPDATE could be checked before issuing writes instead + * - The failure is from code that runs after the first write but doesn't need to + * - The failures are from contention solvable via onTransactionPreCommitOrIdle() + * - The failures are deadlocks; the RDBMs usually discard the whole transaction * - * All atomic levels *must* be explicitly closed using IDatabase::endAtomic() - * or IDatabase::cancelAtomic(), and any database transactions cannot be - * began or committed until all atomic levels are closed. There is no such - * thing as implicitly opening or closing an atomic section. + * @note: callers must use additional measures for situations involving two or more + * (peer) transactions (e.g. updating two database servers at once). The transaction + * and savepoint logic of this method only applies to this specific IDatabase instance. + * + * Example usage: + * @code + * // Start a transaction if there isn't one already + * $dbw->startAtomic( __METHOD__ ); + * // Serialize these thread table updates + * $dbw->select( 'thread', '1', [ 'td_id' => $tid ], __METHOD__, 'FOR UPDATE' ); + * // Add a new comment for the thread + * $dbw->insert( 'comment', $row, __METHOD__ ); + * $cid = $db->insertId(); + * // Update thread reference to last comment + * $dbw->update( 'thread', [ 'td_latest' => $cid ], [ 'td_id' => $tid ], __METHOD__ ); + * // Demark the end of this conceptual unit of updates + * $dbw->endAtomic( __METHOD__ ); + * @endcode + * + * Example usage (atomic changes that might have to be discarded): + * @code + * // Start a transaction if there isn't one already + * $sectionId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE ); + * // Create new record metadata row + * $dbw->insert( 'records', $row, __METHOD__ ); + * // Figure out where to store the data based on the new row's ID + * $path = $recordDirectory . '/' . $dbw->insertId(); + * // Write the record data to the storage system + * $status = $fileBackend->create( [ 'dst' => $path, 'content' => $data ] ); + * if ( $status->isOK() ) { + * // Try to cleanup files orphaned by transaction rollback + * $dbw->onTransactionResolution( + * function ( $type ) use ( $fileBackend, $path ) { + * if ( $type === IDatabase::TRIGGER_ROLLBACK ) { + * $fileBackend->delete( [ 'src' => $path ] ); + * } + * }, + * __METHOD__ + * ); + * // Demark the end of this conceptual unit of updates + * $dbw->endAtomic( __METHOD__ ); + * } else { + * // Discard these writes from the transaction (preserving prior writes) + * $dbw->cancelAtomic( __METHOD__, $sectionId ); + * } + * @endcode * * @since 1.23 * @param string $fname * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a * savepoint and enable self::cancelAtomic() for this section. + * @return AtomicSectionIdentifier section ID token * @throws DBError */ public function startAtomic( $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE ); @@ -1591,33 +1658,79 @@ interface IDatabase { * corresponding startAtomic() implicitly started a transaction, that * transaction is rolled back. * - * Note that a call to IDatabase::rollback() will also roll back any open - * atomic sections. + * @note: callers must use additional measures for situations involving two or more + * (peer) transactions (e.g. updating two database servers at once). The transaction + * and savepoint logic of startAtomic() are bound to specific IDatabase instances. + * + * Note that a call to IDatabase::rollback() will also roll back any open atomic sections. * * @note As a micro-optimization to save a few DB calls, this method may only * be called when startAtomic() was called with the ATOMIC_CANCELABLE flag. * @since 1.31 * @see IDatabase::startAtomic * @param string $fname + * @param AtomicSectionIdentifier $sectionId Section ID from startAtomic(); + * passing this enables cancellation of unclosed nested sections [optional] * @throws DBError */ - public function cancelAtomic( $fname = __METHOD__ ); + public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null ); /** - * Run a callback to do an atomic set of updates for this database + * Perform an atomic section of reversable SQL statements from a callback * * The $callback takes the following arguments: * - This database object * - The value of $fname * - * If any exception occurs in the callback, then cancelAtomic() will be - * called to back out any statements executed by the callback and the error - * will be re-thrown. It may also be that the cancel itself fails with an - * exception before then. In any case, such errors are expected to - * terminate the request, without any outside caller attempting to catch - * errors and commit anyway. + * This will execute the callback inside a pair of startAtomic()/endAtomic() calls. + * If any exception occurs during execution of the callback, it will be handled as follows: + * - If $cancelable is ATOMIC_CANCELABLE, cancelAtomic() will be called to back out any + * (and only) statements executed during the atomic section. If that succeeds, then the + * exception will be re-thrown; if it fails, then a different exception will be thrown + * and any further query attempts will fail until rollback() is called. + * - If $cancelable is ATOMIC_NOT_CANCELABLE, cancelAtomic() will be called to mark the + * end of the section and the error will be re-thrown. Any further query attempts will + * fail until rollback() is called. + * + * This method is convenient for letting calls to the caller of this method be wrapped + * in a try/catch blocks for exception types that imply that the caller failed but was + * able to properly discard the changes it made in the transaction. This method can be + * an alternative to explicit calls to startAtomic()/endAtomic()/cancelAtomic(). + * + * Example usage, "RecordStore::save" method: + * @code + * $dbw->doAtomicSection( __METHOD__, function ( $dbw ) use ( $record ) { + * // Create new record metadata row + * $dbw->insert( 'records', $record->toArray(), __METHOD__ ); + * // Figure out where to store the data based on the new row's ID + * $path = $this->recordDirectory . '/' . $dbw->insertId(); + * // Write the record data to the storage system; + * // blob store throughs StoreFailureException on failure + * $this->blobStore->create( $path, $record->getJSON() ); + * // Try to cleanup files orphaned by transaction rollback + * $dbw->onTransactionResolution( + * function ( $type ) use ( $path ) { + * if ( $type === IDatabase::TRIGGER_ROLLBACK ) { + * $this->blobStore->delete( $path ); + * } + * }, + * __METHOD__ + * ); + * }, $dbw::ATOMIC_CANCELABLE ); + * @endcode * - * This can be an alternative to explicit startAtomic()/endAtomic()/cancelAtomic() calls. + * Example usage, caller of the "RecordStore::save" method: + * @code + * $dbw->startAtomic( __METHOD__ ); + * // ...various SQL writes happen... + * try { + * $recordStore->save( $record ); + * } catch ( StoreFailureException $e ) { + * // ...various SQL writes happen... + * } + * // ...various SQL writes happen... + * $dbw->endAtomic( __METHOD__ ); + * @endcode * * @see Database::startAtomic * @see Database::endAtomic @@ -1625,15 +1738,18 @@ interface IDatabase { * * @param string $fname Caller name (usually __METHOD__) * @param callable $callback Callback that issues DB updates + * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a + * savepoint and enable self::cancelAtomic() for this section. * @return mixed $res Result of the callback (since 1.28) * @throws DBError * @throws RuntimeException - * @throws UnexpectedValueException * @since 1.27; prior to 1.31 this did a rollback() instead of * cancelAtomic(), and assumed no callers up the stack would ever try to * catch the exception. */ - public function doAtomicSection( $fname, callable $callback ); + public function doAtomicSection( + $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE + ); /** * Begin a transaction. If a transaction is already in progress, @@ -1768,7 +1884,7 @@ interface IDatabase { * This is useful when transactions might use snapshot isolation * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data * is this lag plus transaction duration. If they don't, it is still - * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an + * safe to be pessimistic. In AUTOCOMMIT mode, this still gives an * indication of the staleness of subsequent reads. * * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)