Merge "Added DatabaseBase::startAtomic and endAtomic"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 4 Nov 2013 18:34:29 +0000 (18:34 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 4 Nov 2013 18:34:29 +0000 (18:34 +0000)
1  2 
includes/db/Database.php

diff --combined includes/db/Database.php
@@@ -281,6 -281,20 +281,20 @@@ abstract class DatabaseBase implements 
         */
        private $mTrxAutomatic = false;
  
+       /**
+        * Array of levels of atomicity within transactions
+        *
+        * @var SplStack
+        */
+       private $mTrxAtomicLevels;
+       /**
+        * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
+        *
+        * @var Bool
+        */
+       private $mTrxAutomaticAtomic = false;
        /**
         * @since 1.21
         * @var file handle for upgrade
        ) {
                global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions;
  
+               $this->mTrxAtomicLevels = new SplStack;
                $this->mFlags = $flags;
  
                if ( $this->mFlags & DBO_DEFAULT ) {
         * @since 1.20
         */
        final public function onTransactionIdle( $callback ) {
 -              $this->mTrxIdleCallbacks[] = $callback;
 +              $this->mTrxIdleCallbacks[] = array( $callback, wfGetCaller() );
                if ( !$this->mTrxLevel ) {
                        $this->runOnTransactionIdleCallbacks();
                }
         */
        final public function onTransactionPreCommitOrIdle( $callback ) {
                if ( $this->mTrxLevel ) {
 -                      $this->mTrxPreCommitCallbacks[] = $callback;
 +                      $this->mTrxPreCommitCallbacks[] = array( $callback, wfGetCaller() );
                } else {
                        $this->onTransactionIdle( $callback ); // this will trigger immediately
                }
                        $this->mTrxIdleCallbacks = array(); // recursion guard
                        foreach ( $callbacks as $callback ) {
                                try {
 +                                      list( $phpCallback ) = $callback;
                                        $this->clearFlag( DBO_TRX ); // make each query its own transaction
 -                                      call_user_func( $callback );
 +                                      call_user_func( $phpCallback );
                                        $this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin()
                                } catch ( Exception $e ) {}
                        }
                        $this->mTrxPreCommitCallbacks = array(); // recursion guard
                        foreach ( $callbacks as $callback ) {
                                try {
 -                                      call_user_func( $callback );
 +                                      list( $phpCallback ) = $callback;
 +                                      call_user_func( $phpCallback );
                                } catch ( Exception $e ) {}
                        }
                } while ( count( $this->mTrxPreCommitCallbacks ) );
                }
        }
  
+       /**
+        * Begin an atomic section of 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.
+        *
+        * 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 sections are more strict than transactions. With transactions,
+        * attempting to begin a new transaction when one is already running results
+        * in MediaWiki issuing a brief warning and doing an implicit commit. All
+        * atomic levels *must* be explicitly closed using DatabaseBase::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.
+        *
+        * @since 1.23
+        * @param string $fname
+        */
+       final public function startAtomic( $fname = __METHOD__ ) {
+               if ( !$this->mTrxLevel ) {
+                       $this->begin( $fname );
+                       $this->mTrxAutomatic = true;
+                       $this->mTrxAutomaticAtomic = true;
+               }
+               $this->mTrxAtomicLevels->push( $fname );
+       }
        /**
         * Begin a transaction. If a transaction is already in progress, that transaction will be committed before the
         * new transaction is started.
                global $wgDebugDBTransactions;
  
                if ( $this->mTrxLevel ) { // implicit commit
-                       if ( !$this->mTrxAutomatic ) {
+                       if ( !$this->mTrxAtomicLevels->isEmpty() ) {
+                               // If the current transaction was an automatic atomic one, then we definitely have
+                               // a problem. Same if there is any unclosed atomic level.
+                               throw new DBUnexpectedError( $this,
+                                       "Attempted to start explicit transaction when atomic levels are still open."
+                               );
+                       } elseif ( !$this->mTrxAutomatic ) {
                                // We want to warn about inadvertently nested begin/commit pairs, but not about
                                // auto-committing implicit transactions that were started by query() via DBO_TRX
                                $msg = "$fname: Transaction already in progress (from {$this->mTrxFname}), " .
                $this->mTrxFname = $fname;
                $this->mTrxDoneWrites = false;
                $this->mTrxAutomatic = false;
+               $this->mTrxAutomaticAtomic = false;
+               $this->mTrxAtomicLevels = new SplStack;
        }
  
        /**
                $this->mTrxLevel = 1;
        }
  
+       /**
+        * Ends an atomic section of SQL statements
+        *
+        * Ends the next section of atomic SQL statements and commits the transaction
+        * if necessary.
+        *
+        * @since 1.23
+        * @see DatabaseBase::startAtomic
+        * @param string $fname
+        */
+       final public function endAtomic( $fname = __METHOD__ ) {
+               if ( $this->mTrxAtomicLevels->isEmpty() ||
+                       $this->mTrxAtomicLevels->pop() !== $fname
+               ) {
+                       throw new DBUnexpectedError( $this, 'Invalid atomic section ended.' );
+               }
+               if ( $this->mTrxAtomicLevels->isEmpty() && $this->mTrxAutomaticAtomic ) {
+                       $this->commit( $fname, 'flush' );
+               }
+       }
        /**
         * Commits a transaction previously started using begin().
         * If no transaction is in progress, a warning is issued.
         *        that it is safe to ignore these warnings in your context.
         */
        final public function commit( $fname = __METHOD__, $flush = '' ) {
+               if ( !$this->mTrxAtomicLevels->isEmpty() ) {
+                       // There are still atomic sections open. This cannot be ignored
+                       throw new DBUnexpectedError( $this, "Attempted to commit transaction while atomic sections are still open" );
+               }
                if ( $flush != 'flush' ) {
                        if ( !$this->mTrxLevel ) {
                                wfWarn( "$fname: No transaction to commit, something got out of sync!" );
                if ( $this->mTrxDoneWrites ) {
                        Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
                }
 +              $this->mTrxDoneWrites = false;
                $this->runOnTransactionIdleCallbacks();
        }
  
                $this->doRollback( $fname );
                $this->mTrxIdleCallbacks = array(); // cancel
                $this->mTrxPreCommitCallbacks = array(); // cancel
+               $this->mTrxAtomicLevels = new SplStack;
                if ( $this->mTrxDoneWrites ) {
                        Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
                }
 +              $this->mTrxDoneWrites = false;
        }
  
        /**
                return (string)$this->mConn;
        }
  
 +      /**
 +       * Run a few simple sanity checks
 +       */
        public function __destruct() {
 +              if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
 +                      trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
 +              }
                if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
 -                      trigger_error( "Transaction idle or pre-commit callbacks still pending." ); // sanity
 +                      $callers = array();
 +                      foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) {
 +                              $callers[] = $callbackInfo[1];
 +
 +                      }
 +                      $callers = implode( ', ', $callers );
 +                      trigger_error( "DB transaction callbacks still pending (from $callers)." );
                }
        }
  }