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=a5392c81cbbfd8106b7cea4b0cec42cff6243187;hb=e9602b246640c4878036e1d27b7f71704ec4bf53;hpb=c97a962bdb54947affcbe6ca70787f42b23255e9 diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index a5392c81cb..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; /** @@ -49,9 +48,16 @@ interface IDatabase { /** @var string Transaction is requested internally via DBO_TRX/startAtomic() */ const TRANSACTION_INTERNAL = 'implicit'; - /** @var string Transaction operation comes from service managing all DBs */ + /** @var string Atomic section is not cancelable */ + const ATOMIC_NOT_CANCELABLE = ''; + /** @var string Atomic section is cancelable */ + const ATOMIC_CANCELABLE = 'cancelable'; + + /** @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 */ @@ -85,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; @@ -228,6 +234,7 @@ interface IDatabase { * Should return true if unsure. * * @return bool + * @deprecated Since 1.31; use lastDoneWrites() */ public function doneWrites(); @@ -247,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(). * @@ -454,17 +461,6 @@ interface IDatabase { */ public function lastError(); - /** - * mysql_fetch_field() wrapper - * Returns false if the field doesn't exist - * - * @param string $table Table name - * @param string $field Field name - * - * @return Field - */ - public function fieldInfo( $table, $field ); - /** * Get the number of rows affected by the last write query * @see https://secure.php.net/mysql_affected_rows @@ -503,12 +499,6 @@ interface IDatabase { */ public function close(); - /** - * @param string $error Fallback error message, used if none is given by DB - * @throws DBConnectionError - */ - public function reportConnectionError( $error = 'Unknown error' ); - /** * Run an SQL query and return the result. Normally throws a DBQueryError * on failure. If errors are ignored, returns false instead. @@ -537,19 +527,6 @@ interface IDatabase { */ public function query( $sql, $fname = __METHOD__, $tempIgnore = false ); - /** - * Report a query error. Log the error, and if neither the object ignore - * flag nor the $tempIgnore flag is set, throw a DBQueryError. - * - * @param string $error - * @param int $errno - * @param string $sql - * @param string $fname - * @param bool $tempIgnore - * @throws DBQueryError - */ - public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ); - /** * Free a result object returned by query() or select(). It's usually not * necessary to call this, just use unset() or let the variable holding @@ -898,16 +875,6 @@ interface IDatabase { */ public function tableExists( $table, $fname = __METHOD__ ); - /** - * Determines if a given index is unique - * - * @param string $table - * @param string $index - * - * @return bool - */ - public function indexUnique( $table, $index ); - /** * INSERT wrapper, inserts an array into a table. * @@ -1506,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) * @@ -1524,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) * @@ -1546,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. @@ -1578,27 +1560,80 @@ 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, just keep track of 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, - * start one 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(), - * 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__ ); + public function startAtomic( $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE ); /** * Ends an atomic section of SQL statements @@ -1614,33 +1649,107 @@ interface IDatabase { public function endAtomic( $fname = __METHOD__ ); /** - * Run a callback to do an atomic set of updates for this database + * Cancel an atomic section of SQL statements + * + * This will roll back only the statements executed since the start of the + * most recent atomic section, and close that section. If a transaction was + * open before the corresponding startAtomic() call, any statements before + * that call are *not* rolled back and the transaction remains open. If the + * corresponding startAtomic() implicitly started a transaction, that + * transaction is rolled back. + * + * @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__, AtomicSectionIdentifier $sectionId = null ); + + /** + * 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 rollback() will be called and the error will - * be re-thrown. It may also be that the rollback 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. Note that any rollback undoes all prior - * atomic section and uncommitted updates, which trashes the current request, requiring an - * error to be displayed. + * 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() 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 + * @see Database::cancelAtomic * * @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 + * @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, @@ -1722,16 +1831,6 @@ interface IDatabase { */ public function flushSnapshot( $fname = __METHOD__ ); - /** - * List all tables on the database - * - * @param string $prefix Only show tables with this prefix, e.g. mw_ - * @param string $fname Calling function name - * @throws DBError - * @return array - */ - public function listTables( $prefix = null, $fname = __METHOD__ ); - /** * Convert a timestamp in one of the formats accepted by wfTimestamp() * to the format used for inserting into timestamp fields in this DBMS. @@ -1769,11 +1868,9 @@ interface IDatabase { public function ping( &$rtt = null ); /** - * Get replica DB lag. Currently supported only by MySQL. + * Get the amount of replication lag for this database server * - * Note that this function will generate a fatal error on many - * installations. Most callers should use LoadBalancer::safeGetLag() - * instead. + * Callers should avoid using this method while a transaction is active * * @return int|bool Database replication lag in seconds or false on error * @throws DBError @@ -1787,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)