rdbms: Add IDatabase::cancelAtomic()
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index 417f64c..c3e36da 100644 (file)
@@ -187,6 +187,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @see Database::trxLevel
         */
        private $trxAutomatic = false;
+       /**
+        * Counter for atomic savepoint identifiers. Reset when a new transaction begins.
+        *
+        * @var int
+        */
+       private $trxAtomicCounter = 0;
        /**
         * Array of levels of atomicity within transactions
         *
@@ -1241,6 +1247,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        private function handleSessionLoss() {
                $this->trxLevel = 0;
+               $this->trxAtomicCounter = 0;
                $this->trxIdleCallbacks = []; // T67263; transaction already lost
                $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
                $this->sessionTempTables = [];
@@ -2547,7 +2554,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->endAtomic( $fname );
                        $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
        }
@@ -2630,7 +2637,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->endAtomic( $fname );
                        $this->affectedRowCount = $affectedRowCount;
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
 
@@ -2798,11 +2805,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->endAtomic( $fname );
                                $this->affectedRowCount = $affectedRowCount;
                        } else {
-                               $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                               $this->cancelAtomic( $fname );
                        }
                        return $ok;
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
        }
@@ -3093,7 +3100,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                call_user_func( $callback );
                                $this->endAtomic( __METHOD__ );
                        } catch ( Exception $e ) {
-                               $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
+                               $this->cancelAtomic( __METHOD__ );
                                throw $e;
                        }
                }
@@ -3230,6 +3237,48 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
+       /**
+        * Create a savepoint
+        *
+        * This is used internally to implement atomic sections. It should not be
+        * used otherwise.
+        *
+        * @since 1.31
+        * @param string $identifier Identifier for the savepoint
+        * @param string $fname Calling function name
+        */
+       protected function doSavepoint( $identifier, $fname ) {
+               $this->query( 'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+       }
+
+       /**
+        * Release a savepoint
+        *
+        * This is used internally to implement atomic sections. It should not be
+        * used otherwise.
+        *
+        * @since 1.31
+        * @param string $identifier Identifier for the savepoint
+        * @param string $fname Calling function name
+        */
+       protected function doReleaseSavepoint( $identifier, $fname ) {
+               $this->query( 'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+       }
+
+       /**
+        * Rollback to a savepoint
+        *
+        * This is used internally to implement atomic sections. It should not be
+        * used otherwise.
+        *
+        * @since 1.31
+        * @param string $identifier Identifier for the savepoint
+        * @param string $fname Calling function name
+        */
+       protected function doRollbackToSavepoint( $identifier, $fname ) {
+               $this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
+       }
+
        final public function startAtomic( $fname = __METHOD__ ) {
                if ( !$this->trxLevel ) {
                        $this->begin( $fname, self::TRANSACTION_INTERNAL );
@@ -3238,32 +3287,68 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        if ( !$this->getFlag( self::DBO_TRX ) ) {
                                $this->trxAutomaticAtomic = true;
                        }
+                       $savepointId = null;
+               } else {
+                       $savepointId = 'wikimedia_rdbms_atomic' . ++$this->trxAtomicCounter;
+                       if ( strlen( $savepointId ) > 30 ) { // 30 == Oracle's identifier length limit (pre 12c)
+                               $this->queryLogger->warning(
+                                       'There have been an excessively large number of atomic sections in a transaction'
+                                       . " started by $this->trxFname, reusing IDs (at $fname)",
+                                       [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+                               );
+                               $this->trxAtomicCounter = 0;
+                               $savepointId = 'wikimedia_rdbms_atomic' . ++$this->trxAtomicCounter;
+                       }
+                       $this->doSavepoint( $savepointId, $fname );
                }
 
-               $this->trxAtomicLevels[] = $fname;
+               $this->trxAtomicLevels[] = [ $fname, $savepointId ];
        }
 
        final public function endAtomic( $fname = __METHOD__ ) {
                if ( !$this->trxLevel ) {
                        throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
                }
-               if ( !$this->trxAtomicLevels ||
-                       array_pop( $this->trxAtomicLevels ) !== $fname
-               ) {
+
+               list( $savedFname, $savepointId ) = $this->trxAtomicLevels
+                       ? array_pop( $this->trxAtomicLevels ) : [ null, null ];
+               if ( $savedFname !== $fname ) {
                        throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
                }
 
-               if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
+               if ( !$savepointId ) {
                        $this->commit( $fname, self::FLUSHING_INTERNAL );
+               } else {
+                       $this->doReleaseSavepoint( $savepointId, $fname );
                }
        }
 
+       final public function cancelAtomic( $fname = __METHOD__ ) {
+               if ( !$this->trxLevel ) {
+                       throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
+               }
+
+               list( $savedFname, $savepointId ) = $this->trxAtomicLevels
+                       ? array_pop( $this->trxAtomicLevels ) : [ null, null ];
+               if ( $savedFname !== $fname ) {
+                       throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
+               }
+
+               if ( !$savepointId ) {
+                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+               } else {
+                       $this->doRollbackToSavepoint( $savepointId, $fname );
+               }
+
+               $this->affectedRowCount = 0; // for the sake of consistency
+       }
+
        final public function doAtomicSection( $fname, callable $callback ) {
                $this->startAtomic( $fname );
                try {
                        $res = call_user_func_array( $callback, [ $this, $fname ] );
                } catch ( Exception $e ) {
-                       $this->rollback( $fname, self::FLUSHING_INTERNAL );
+                       $this->cancelAtomic( $fname );
                        throw $e;
                }
                $this->endAtomic( $fname );
@@ -3275,7 +3360,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // Protect against mismatched atomic section, transaction nesting, and snapshot loss
                if ( $this->trxLevel ) {
                        if ( $this->trxAtomicLevels ) {
-                               $levels = implode( ', ', $this->trxAtomicLevels );
+                               $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
+                                       return $accum === null ? $v[0] : "$accum, " . $v[0];
+                               } );
                                $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
                                throw new DBUnexpectedError( $this, $msg );
                        } elseif ( !$this->trxAutomatic ) {
@@ -3294,6 +3381,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->assertOpen();
 
                $this->doBegin( $fname );
+               $this->trxAtomicCounter = 0;
                $this->trxTimestamp = microtime( true );
                $this->trxFname = $fname;
                $this->trxDoneWrites = false;
@@ -3331,7 +3419,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        final public function commit( $fname = __METHOD__, $flush = '' ) {
                if ( $this->trxLevel && $this->trxAtomicLevels ) {
                        // There are still atomic sections open. This cannot be ignored
-                       $levels = implode( ', ', $this->trxAtomicLevels );
+                       $levels = array_reduce( $this->trxAtomicLevels, function ( $accum, $v ) {
+                               return $accum === null ? $v[0] : "$accum, " . $v[0];
+                       } );
                        throw new DBUnexpectedError(
                                $this,
                                "$fname: Got COMMIT while atomic sections $levels are still open."