MediaWiki  REL1_24
recompressTracked.php
Go to the documentation of this file.
00001 <?php
00025 $optionsWithArgs = RecompressTracked::getOptionsWithArgs();
00026 require __DIR__ . '/../commandLine.inc';
00027 
00028 if ( count( $args ) < 1 ) {
00029     echo "Usage: php recompressTracked.php [options] <cluster> [... <cluster>...]
00030 Moves blobs indexed by trackBlobs.php to a specified list of destination clusters,
00031 and recompresses them in the process. Restartable.
00032 
00033 Options:
00034     --procs <procs>       Set the number of child processes (default 1)
00035     --copy-only           Copy only, do not update the text table. Restart
00036                           without this option to complete.
00037     --debug-log <file>    Log debugging data to the specified file
00038     --info-log <file>     Log progress messages to the specified file
00039     --critical-log <file> Log error messages to the specified file
00040 ";
00041     exit( 1 );
00042 }
00043 
00044 $job = RecompressTracked::newFromCommandLine( $args, $options );
00045 $job->execute();
00046 
00053 class RecompressTracked {
00054     public $destClusters;
00055     public $batchSize = 1000;
00056     public $orphanBatchSize = 1000;
00057     public $reportingInterval = 10;
00058     public $numProcs = 1;
00059     public $useDiff, $pageBlobClass, $orphanBlobClass;
00060     public $slavePipes, $slaveProcs, $prevSlaveId;
00061     public $copyOnly = false;
00062     public $isChild = false;
00063     public $slaveId = false;
00064     public $noCount = false;
00065     public $debugLog, $infoLog, $criticalLog;
00066     public $store;
00067 
00068     private static $optionsWithArgs = array(
00069         'procs',
00070         'slave-id',
00071         'debug-log',
00072         'info-log',
00073         'critical-log'
00074     );
00075 
00076     private static $cmdLineOptionMap = array(
00077         'no-count' => 'noCount',
00078         'procs' => 'numProcs',
00079         'copy-only' => 'copyOnly',
00080         'child' => 'isChild',
00081         'slave-id' => 'slaveId',
00082         'debug-log' => 'debugLog',
00083         'info-log' => 'infoLog',
00084         'critical-log' => 'criticalLog',
00085     );
00086 
00087     static function getOptionsWithArgs() {
00088         return self::$optionsWithArgs;
00089     }
00090 
00091     static function newFromCommandLine( $args, $options ) {
00092         $jobOptions = array( 'destClusters' => $args );
00093         foreach ( self::$cmdLineOptionMap as $cmdOption => $classOption ) {
00094             if ( isset( $options[$cmdOption] ) ) {
00095                 $jobOptions[$classOption] = $options[$cmdOption];
00096             }
00097         }
00098 
00099         return new self( $jobOptions );
00100     }
00101 
00102     function __construct( $options ) {
00103         foreach ( $options as $name => $value ) {
00104             $this->$name = $value;
00105         }
00106         $this->store = new ExternalStoreDB;
00107         if ( !$this->isChild ) {
00108             $GLOBALS['wgDebugLogPrefix'] = "RCT M: ";
00109         } elseif ( $this->slaveId !== false ) {
00110             $GLOBALS['wgDebugLogPrefix'] = "RCT {$this->slaveId}: ";
00111         }
00112         $this->useDiff = function_exists( 'xdiff_string_bdiff' );
00113         $this->pageBlobClass = $this->useDiff ? 'DiffHistoryBlob' : 'ConcatenatedGzipHistoryBlob';
00114         $this->orphanBlobClass = 'ConcatenatedGzipHistoryBlob';
00115     }
00116 
00117     function debug( $msg ) {
00118         wfDebug( "$msg\n" );
00119         if ( $this->debugLog ) {
00120             $this->logToFile( $msg, $this->debugLog );
00121         }
00122     }
00123 
00124     function info( $msg ) {
00125         echo "$msg\n";
00126         if ( $this->infoLog ) {
00127             $this->logToFile( $msg, $this->infoLog );
00128         }
00129     }
00130 
00131     function critical( $msg ) {
00132         echo "$msg\n";
00133         if ( $this->criticalLog ) {
00134             $this->logToFile( $msg, $this->criticalLog );
00135         }
00136     }
00137 
00138     function logToFile( $msg, $file ) {
00139         $header = '[' . date( 'd\TH:i:s' ) . '] ' . wfHostname() . ' ' . posix_getpid();
00140         if ( $this->slaveId !== false ) {
00141             $header .= "({$this->slaveId})";
00142         }
00143         $header .= ' ' . wfWikiID();
00144         wfErrorLog( sprintf( "%-50s %s\n", $header, $msg ), $file );
00145     }
00146 
00152     function syncDBs() {
00153         $dbw = wfGetDB( DB_MASTER );
00154         $dbr = wfGetDB( DB_SLAVE );
00155         $pos = $dbw->getMasterPos();
00156         $dbr->masterPosWait( $pos, 100000 );
00157     }
00158 
00162     function execute() {
00163         if ( $this->isChild ) {
00164             $this->executeChild();
00165         } else {
00166             $this->executeParent();
00167         }
00168     }
00169 
00173     function executeParent() {
00174         if ( !$this->checkTrackingTable() ) {
00175             return;
00176         }
00177 
00178         $this->syncDBs();
00179         $this->startSlaveProcs();
00180         $this->doAllPages();
00181         $this->doAllOrphans();
00182         $this->killSlaveProcs();
00183     }
00184 
00189     function checkTrackingTable() {
00190         $dbr = wfGetDB( DB_SLAVE );
00191         if ( !$dbr->tableExists( 'blob_tracking' ) ) {
00192             $this->critical( "Error: blob_tracking table does not exist" );
00193 
00194             return false;
00195         }
00196         $row = $dbr->selectRow( 'blob_tracking', '*', false, __METHOD__ );
00197         if ( !$row ) {
00198             $this->info( "Warning: blob_tracking table contains no rows, skipping this wiki." );
00199 
00200             return false;
00201         }
00202 
00203         return true;
00204     }
00205 
00212     function startSlaveProcs() {
00213         $cmd = 'php ' . wfEscapeShellArg( __FILE__ );
00214         foreach ( self::$cmdLineOptionMap as $cmdOption => $classOption ) {
00215             if ( $cmdOption == 'slave-id' ) {
00216                 continue;
00217             } elseif ( in_array( $cmdOption, self::$optionsWithArgs ) && isset( $this->$classOption ) ) {
00218                 $cmd .= " --$cmdOption " . wfEscapeShellArg( $this->$classOption );
00219             } elseif ( $this->$classOption ) {
00220                 $cmd .= " --$cmdOption";
00221             }
00222         }
00223         $cmd .= ' --child' .
00224             ' --wiki ' . wfEscapeShellArg( wfWikiID() ) .
00225             ' ' . call_user_func_array( 'wfEscapeShellArg', $this->destClusters );
00226 
00227         $this->slavePipes = $this->slaveProcs = array();
00228         for ( $i = 0; $i < $this->numProcs; $i++ ) {
00229             $pipes = false;
00230             $spec = array(
00231                 array( 'pipe', 'r' ),
00232                 array( 'file', 'php://stdout', 'w' ),
00233                 array( 'file', 'php://stderr', 'w' )
00234             );
00235             wfSuppressWarnings();
00236             $proc = proc_open( "$cmd --slave-id $i", $spec, $pipes );
00237             wfRestoreWarnings();
00238             if ( !$proc ) {
00239                 $this->critical( "Error opening slave process: $cmd" );
00240                 exit( 1 );
00241             }
00242             $this->slaveProcs[$i] = $proc;
00243             $this->slavePipes[$i] = $pipes[0];
00244         }
00245         $this->prevSlaveId = -1;
00246     }
00247 
00251     function killSlaveProcs() {
00252         $this->info( "Waiting for slave processes to finish..." );
00253         for ( $i = 0; $i < $this->numProcs; $i++ ) {
00254             $this->dispatchToSlave( $i, 'quit' );
00255         }
00256         for ( $i = 0; $i < $this->numProcs; $i++ ) {
00257             $status = proc_close( $this->slaveProcs[$i] );
00258             if ( $status ) {
00259                 $this->critical( "Warning: child #$i exited with status $status" );
00260             }
00261         }
00262         $this->info( "Done." );
00263     }
00264 
00269     function dispatch( /*...*/ ) {
00270         $args = func_get_args();
00271         $pipes = $this->slavePipes;
00272         $numPipes = stream_select( $x = array(), $pipes, $y = array(), 3600 );
00273         if ( !$numPipes ) {
00274             $this->critical( "Error waiting to write to slaves. Aborting" );
00275             exit( 1 );
00276         }
00277         for ( $i = 0; $i < $this->numProcs; $i++ ) {
00278             $slaveId = ( $i + $this->prevSlaveId + 1 ) % $this->numProcs;
00279             if ( isset( $pipes[$slaveId] ) ) {
00280                 $this->prevSlaveId = $slaveId;
00281                 $this->dispatchToSlave( $slaveId, $args );
00282 
00283                 return;
00284             }
00285         }
00286         $this->critical( "Unreachable" );
00287         exit( 1 );
00288     }
00289 
00295     function dispatchToSlave( $slaveId, $args ) {
00296         $args = (array)$args;
00297         $cmd = implode( ' ', $args );
00298         fwrite( $this->slavePipes[$slaveId], "$cmd\n" );
00299     }
00300 
00304     function doAllPages() {
00305         $dbr = wfGetDB( DB_SLAVE );
00306         $i = 0;
00307         $startId = 0;
00308         if ( $this->noCount ) {
00309             $numPages = '[unknown]';
00310         } else {
00311             $numPages = $dbr->selectField( 'blob_tracking',
00312                 'COUNT(DISTINCT bt_page)',
00313                 # A condition is required so that this query uses the index
00314                 array( 'bt_moved' => 0 ),
00315                 __METHOD__
00316             );
00317         }
00318         if ( $this->copyOnly ) {
00319             $this->info( "Copying pages..." );
00320         } else {
00321             $this->info( "Moving pages..." );
00322         }
00323         while ( true ) {
00324             $res = $dbr->select( 'blob_tracking',
00325                 array( 'bt_page' ),
00326                 array(
00327                     'bt_moved' => 0,
00328                     'bt_page > ' . $dbr->addQuotes( $startId )
00329                 ),
00330                 __METHOD__,
00331                 array(
00332                     'DISTINCT',
00333                     'ORDER BY' => 'bt_page',
00334                     'LIMIT' => $this->batchSize,
00335                 )
00336             );
00337             if ( !$res->numRows() ) {
00338                 break;
00339             }
00340             foreach ( $res as $row ) {
00341                 $this->dispatch( 'doPage', $row->bt_page );
00342                 $i++;
00343             }
00344             $startId = $row->bt_page;
00345             $this->report( 'pages', $i, $numPages );
00346         }
00347         $this->report( 'pages', $i, $numPages );
00348         if ( $this->copyOnly ) {
00349             $this->info( "All page copies queued." );
00350         } else {
00351             $this->info( "All page moves queued." );
00352         }
00353     }
00354 
00361     function report( $label, $current, $end ) {
00362         $this->numBatches++;
00363         if ( $current == $end || $this->numBatches >= $this->reportingInterval ) {
00364             $this->numBatches = 0;
00365             $this->info( "$label: $current / $end" );
00366             $this->waitForSlaves();
00367         }
00368     }
00369 
00373     function doAllOrphans() {
00374         $dbr = wfGetDB( DB_SLAVE );
00375         $startId = 0;
00376         $i = 0;
00377         if ( $this->noCount ) {
00378             $numOrphans = '[unknown]';
00379         } else {
00380             $numOrphans = $dbr->selectField( 'blob_tracking',
00381                 'COUNT(DISTINCT bt_text_id)',
00382                 array( 'bt_moved' => 0, 'bt_page' => 0 ),
00383                 __METHOD__ );
00384             if ( !$numOrphans ) {
00385                 return;
00386             }
00387         }
00388         if ( $this->copyOnly ) {
00389             $this->info( "Copying orphans..." );
00390         } else {
00391             $this->info( "Moving orphans..." );
00392         }
00393 
00394         while ( true ) {
00395             $res = $dbr->select( 'blob_tracking',
00396                 array( 'bt_text_id' ),
00397                 array(
00398                     'bt_moved' => 0,
00399                     'bt_page' => 0,
00400                     'bt_text_id > ' . $dbr->addQuotes( $startId )
00401                 ),
00402                 __METHOD__,
00403                 array(
00404                     'DISTINCT',
00405                     'ORDER BY' => 'bt_text_id',
00406                     'LIMIT' => $this->batchSize
00407                 )
00408             );
00409             if ( !$res->numRows() ) {
00410                 break;
00411             }
00412             $ids = array();
00413             foreach ( $res as $row ) {
00414                 $ids[] = $row->bt_text_id;
00415                 $i++;
00416             }
00417             // Need to send enough orphan IDs to the child at a time to fill a blob,
00418             // so orphanBatchSize needs to be at least ~100.
00419             // batchSize can be smaller or larger.
00420             while ( count( $ids ) > $this->orphanBatchSize ) {
00421                 $args = array_slice( $ids, 0, $this->orphanBatchSize );
00422                 $ids = array_slice( $ids, $this->orphanBatchSize );
00423                 array_unshift( $args, 'doOrphanList' );
00424                 call_user_func_array( array( $this, 'dispatch' ), $args );
00425             }
00426             if ( count( $ids ) ) {
00427                 $args = $ids;
00428                 array_unshift( $args, 'doOrphanList' );
00429                 call_user_func_array( array( $this, 'dispatch' ), $args );
00430             }
00431 
00432             $startId = $row->bt_text_id;
00433             $this->report( 'orphans', $i, $numOrphans );
00434         }
00435         $this->report( 'orphans', $i, $numOrphans );
00436         $this->info( "All orphans queued." );
00437     }
00438 
00442     function executeChild() {
00443         $this->debug( 'starting' );
00444         $this->syncDBs();
00445 
00446         while ( !feof( STDIN ) ) {
00447             $line = rtrim( fgets( STDIN ) );
00448             if ( $line == '' ) {
00449                 continue;
00450             }
00451             $this->debug( $line );
00452             $args = explode( ' ', $line );
00453             $cmd = array_shift( $args );
00454             switch ( $cmd ) {
00455                 case 'doPage':
00456                     $this->doPage( intval( $args[0] ) );
00457                     break;
00458                 case 'doOrphanList':
00459                     $this->doOrphanList( array_map( 'intval', $args ) );
00460                     break;
00461                 case 'quit':
00462                     return;
00463             }
00464             $this->waitForSlaves();
00465         }
00466     }
00467 
00473     function doPage( $pageId ) {
00474         $title = Title::newFromId( $pageId );
00475         if ( $title ) {
00476             $titleText = $title->getPrefixedText();
00477         } else {
00478             $titleText = '[deleted]';
00479         }
00480         $dbr = wfGetDB( DB_SLAVE );
00481 
00482         // Finish any incomplete transactions
00483         if ( !$this->copyOnly ) {
00484             $this->finishIncompleteMoves( array( 'bt_page' => $pageId ) );
00485             $this->syncDBs();
00486         }
00487 
00488         $startId = 0;
00489         $trx = new CgzCopyTransaction( $this, $this->pageBlobClass );
00490 
00491         while ( true ) {
00492             $res = $dbr->select(
00493                 array( 'blob_tracking', 'text' ),
00494                 '*',
00495                 array(
00496                     'bt_page' => $pageId,
00497                     'bt_text_id > ' . $dbr->addQuotes( $startId ),
00498                     'bt_moved' => 0,
00499                     'bt_new_url IS NULL',
00500                     'bt_text_id=old_id',
00501                 ),
00502                 __METHOD__,
00503                 array(
00504                     'ORDER BY' => 'bt_text_id',
00505                     'LIMIT' => $this->batchSize
00506                 )
00507             );
00508             if ( !$res->numRows() ) {
00509                 break;
00510             }
00511 
00512             $lastTextId = 0;
00513             foreach ( $res as $row ) {
00514                 if ( $lastTextId == $row->bt_text_id ) {
00515                     // Duplicate (null edit)
00516                     continue;
00517                 }
00518                 $lastTextId = $row->bt_text_id;
00519                 // Load the text
00520                 $text = Revision::getRevisionText( $row );
00521                 if ( $text === false ) {
00522                     $this->critical( "Error loading {$row->bt_rev_id}/{$row->bt_text_id}" );
00523                     continue;
00524                 }
00525 
00526                 // Queue it
00527                 if ( !$trx->addItem( $text, $row->bt_text_id ) ) {
00528                     $this->debug( "$titleText: committing blob with " . $trx->getSize() . " items" );
00529                     $trx->commit();
00530                     $trx = new CgzCopyTransaction( $this, $this->pageBlobClass );
00531                     $this->waitForSlaves();
00532                 }
00533             }
00534             $startId = $row->bt_text_id;
00535         }
00536 
00537         $this->debug( "$titleText: committing blob with " . $trx->getSize() . " items" );
00538         $trx->commit();
00539     }
00540 
00554     function moveTextRow( $textId, $url ) {
00555         if ( $this->copyOnly ) {
00556             $this->critical( "Internal error: can't call moveTextRow() in --copy-only mode" );
00557             exit( 1 );
00558         }
00559         $dbw = wfGetDB( DB_MASTER );
00560         $dbw->begin( __METHOD__ );
00561         $dbw->update( 'text',
00562             array( // set
00563                 'old_text' => $url,
00564                 'old_flags' => 'external,utf-8',
00565             ),
00566             array( // where
00567                 'old_id' => $textId
00568             ),
00569             __METHOD__
00570         );
00571         $dbw->update( 'blob_tracking',
00572             array( 'bt_moved' => 1 ),
00573             array( 'bt_text_id' => $textId ),
00574             __METHOD__
00575         );
00576         $dbw->commit( __METHOD__ );
00577     }
00578 
00589     function finishIncompleteMoves( $conds ) {
00590         $dbr = wfGetDB( DB_SLAVE );
00591 
00592         $startId = 0;
00593         $conds = array_merge( $conds, array(
00594             'bt_moved' => 0,
00595             'bt_new_url IS NOT NULL'
00596         ) );
00597         while ( true ) {
00598             $res = $dbr->select( 'blob_tracking',
00599                 '*',
00600                 array_merge( $conds, array( 'bt_text_id > ' . $dbr->addQuotes( $startId ) ) ),
00601                 __METHOD__,
00602                 array(
00603                     'ORDER BY' => 'bt_text_id',
00604                     'LIMIT' => $this->batchSize,
00605                 )
00606             );
00607             if ( !$res->numRows() ) {
00608                 break;
00609             }
00610             $this->debug( 'Incomplete: ' . $res->numRows() . ' rows' );
00611             foreach ( $res as $row ) {
00612                 $this->moveTextRow( $row->bt_text_id, $row->bt_new_url );
00613                 if ( $row->bt_text_id % 10 == 0 ) {
00614                     $this->waitForSlaves();
00615                 }
00616             }
00617             $startId = $row->bt_text_id;
00618         }
00619     }
00620 
00625     function getTargetCluster() {
00626         $cluster = next( $this->destClusters );
00627         if ( $cluster === false ) {
00628             $cluster = reset( $this->destClusters );
00629         }
00630 
00631         return $cluster;
00632     }
00633 
00639     function getExtDB( $cluster ) {
00640         $lb = wfGetLBFactory()->getExternalLB( $cluster );
00641 
00642         return $lb->getConnection( DB_MASTER );
00643     }
00644 
00650     function doOrphanList( $textIds ) {
00651         // Finish incomplete moves
00652         if ( !$this->copyOnly ) {
00653             $this->finishIncompleteMoves( array( 'bt_text_id' => $textIds ) );
00654             $this->syncDBs();
00655         }
00656 
00657         $trx = new CgzCopyTransaction( $this, $this->orphanBlobClass );
00658 
00659         $res = wfGetDB( DB_SLAVE )->select(
00660             array( 'text', 'blob_tracking' ),
00661             array( 'old_id', 'old_text', 'old_flags' ),
00662             array(
00663                 'old_id' => $textIds,
00664                 'bt_text_id=old_id',
00665                 'bt_moved' => 0,
00666             ),
00667             __METHOD__,
00668             array( 'DISTINCT' )
00669         );
00670 
00671         foreach ( $res as $row ) {
00672             $text = Revision::getRevisionText( $row );
00673             if ( $text === false ) {
00674                 $this->critical( "Error: cannot load revision text for old_id={$row->old_id}" );
00675                 continue;
00676             }
00677 
00678             if ( !$trx->addItem( $text, $row->old_id ) ) {
00679                 $this->debug( "[orphan]: committing blob with " . $trx->getSize() . " rows" );
00680                 $trx->commit();
00681                 $trx = new CgzCopyTransaction( $this, $this->orphanBlobClass );
00682                 $this->waitForSlaves();
00683             }
00684         }
00685         $this->debug( "[orphan]: committing blob with " . $trx->getSize() . " rows" );
00686         $trx->commit();
00687     }
00688 
00692     function waitForSlaves() {
00693         $lb = wfGetLB();
00694         while ( true ) {
00695             list( $host, $maxLag ) = $lb->getMaxLag();
00696             if ( $maxLag < 2 ) {
00697                 break;
00698             }
00699             sleep( 5 );
00700         }
00701     }
00702 }
00703 
00707 class CgzCopyTransaction {
00708     public $parent;
00709     public $blobClass;
00710     public $cgz;
00711     public $referrers;
00712 
00718     function __construct( $parent, $blobClass ) {
00719         $this->blobClass = $blobClass;
00720         $this->cgz = false;
00721         $this->texts = array();
00722         $this->parent = $parent;
00723     }
00724 
00732     function addItem( $text, $textId ) {
00733         if ( !$this->cgz ) {
00734             $class = $this->blobClass;
00735             $this->cgz = new $class;
00736         }
00737         $hash = $this->cgz->addItem( $text );
00738         $this->referrers[$textId] = $hash;
00739         $this->texts[$textId] = $text;
00740 
00741         return $this->cgz->isHappy();
00742     }
00743 
00744     function getSize() {
00745         return count( $this->texts );
00746     }
00747 
00751     function recompress() {
00752         $class = $this->blobClass;
00753         $this->cgz = new $class;
00754         $this->referrers = array();
00755         foreach ( $this->texts as $textId => $text ) {
00756             $hash = $this->cgz->addItem( $text );
00757             $this->referrers[$textId] = $hash;
00758         }
00759     }
00760 
00766     function commit() {
00767         $originalCount = count( $this->texts );
00768         if ( !$originalCount ) {
00769             return;
00770         }
00771 
00772         // Check to see if the target text_ids have been moved already.
00773         //
00774         // We originally read from the slave, so this can happen when a single
00775         // text_id is shared between multiple pages. It's rare, but possible
00776         // if a delete/move/undelete cycle splits up a null edit.
00777         //
00778         // We do a locking read to prevent closer-run race conditions.
00779         $dbw = wfGetDB( DB_MASTER );
00780         $dbw->begin( __METHOD__ );
00781         $res = $dbw->select( 'blob_tracking',
00782             array( 'bt_text_id', 'bt_moved' ),
00783             array( 'bt_text_id' => array_keys( $this->referrers ) ),
00784             __METHOD__, array( 'FOR UPDATE' ) );
00785         $dirty = false;
00786         foreach ( $res as $row ) {
00787             if ( $row->bt_moved ) {
00788                 # This row has already been moved, remove it
00789                 $this->parent->debug( "TRX: conflict detected in old_id={$row->bt_text_id}" );
00790                 unset( $this->texts[$row->bt_text_id] );
00791                 $dirty = true;
00792             }
00793         }
00794 
00795         // Recompress the blob if necessary
00796         if ( $dirty ) {
00797             if ( !count( $this->texts ) ) {
00798                 // All have been moved already
00799                 if ( $originalCount > 1 ) {
00800                     // This is suspcious, make noise
00801                     $this->critical( "Warning: concurrent operation detected, are there two conflicting " .
00802                         "processes running, doing the same job?" );
00803                 }
00804 
00805                 return;
00806             }
00807             $this->recompress();
00808         }
00809 
00810         // Insert the data into the destination cluster
00811         $targetCluster = $this->parent->getTargetCluster();
00812         $store = $this->parent->store;
00813         $targetDB = $store->getMaster( $targetCluster );
00814         $targetDB->clearFlag( DBO_TRX ); // we manage the transactions
00815         $targetDB->begin( __METHOD__ );
00816         $baseUrl = $this->parent->store->store( $targetCluster, serialize( $this->cgz ) );
00817 
00818         // Write the new URLs to the blob_tracking table
00819         foreach ( $this->referrers as $textId => $hash ) {
00820             $url = $baseUrl . '/' . $hash;
00821             $dbw->update( 'blob_tracking',
00822                 array( 'bt_new_url' => $url ),
00823                 array(
00824                     'bt_text_id' => $textId,
00825                     'bt_moved' => 0, # Check for concurrent conflicting update
00826                 ),
00827                 __METHOD__
00828             );
00829         }
00830 
00831         $targetDB->commit( __METHOD__ );
00832         // Critical section here: interruption at this point causes blob duplication
00833         // Reversing the order of the commits would cause data loss instead
00834         $dbw->commit( __METHOD__ );
00835 
00836         // Write the new URLs to the text table and set the moved flag
00837         if ( !$this->parent->copyOnly ) {
00838             foreach ( $this->referrers as $textId => $hash ) {
00839                 $url = $baseUrl . '/' . $hash;
00840                 $this->parent->moveTextRow( $textId, $url );
00841             }
00842         }
00843     }
00844 }