MediaWiki
REL1_22
|
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, and recompresses them in the process. Restartable. 00031 00032 Options: 00033 --procs <procs> Set the number of child processes (default 1) 00034 --copy-only Copy only, do not update the text table. Restart without this option to complete. 00035 --debug-log <file> Log debugging data to the specified file 00036 --info-log <file> Log progress messages to the specified file 00037 --critical-log <file> Log error messages to the specified file 00038 "; 00039 exit( 1 ); 00040 } 00041 00042 $job = RecompressTracked::newFromCommandLine( $args, $options ); 00043 $job->execute(); 00044 00051 class RecompressTracked { 00052 public $destClusters; 00053 public $batchSize = 1000; 00054 public $orphanBatchSize = 1000; 00055 public $reportingInterval = 10; 00056 public $numProcs = 1; 00057 public $useDiff, $pageBlobClass, $orphanBlobClass; 00058 public $slavePipes, $slaveProcs, $prevSlaveId; 00059 public $copyOnly = false; 00060 public $isChild = false; 00061 public $slaveId = false; 00062 public $noCount = false; 00063 public $debugLog, $infoLog, $criticalLog; 00064 public $store; 00065 00066 static $optionsWithArgs = array( 'procs', 'slave-id', 'debug-log', 'info-log', 'critical-log' ); 00067 static $cmdLineOptionMap = array( 00068 'no-count' => 'noCount', 00069 'procs' => 'numProcs', 00070 'copy-only' => 'copyOnly', 00071 'child' => 'isChild', 00072 'slave-id' => 'slaveId', 00073 'debug-log' => 'debugLog', 00074 'info-log' => 'infoLog', 00075 'critical-log' => 'criticalLog', 00076 ); 00077 00078 static function getOptionsWithArgs() { 00079 return self::$optionsWithArgs; 00080 } 00081 00082 static function newFromCommandLine( $args, $options ) { 00083 $jobOptions = array( 'destClusters' => $args ); 00084 foreach ( self::$cmdLineOptionMap as $cmdOption => $classOption ) { 00085 if ( isset( $options[$cmdOption] ) ) { 00086 $jobOptions[$classOption] = $options[$cmdOption]; 00087 } 00088 } 00089 return new self( $jobOptions ); 00090 } 00091 00092 function __construct( $options ) { 00093 foreach ( $options as $name => $value ) { 00094 $this->$name = $value; 00095 } 00096 $this->store = new ExternalStoreDB; 00097 if ( !$this->isChild ) { 00098 $GLOBALS['wgDebugLogPrefix'] = "RCT M: "; 00099 } elseif ( $this->slaveId !== false ) { 00100 $GLOBALS['wgDebugLogPrefix'] = "RCT {$this->slaveId}: "; 00101 } 00102 $this->useDiff = function_exists( 'xdiff_string_bdiff' ); 00103 $this->pageBlobClass = $this->useDiff ? 'DiffHistoryBlob' : 'ConcatenatedGzipHistoryBlob'; 00104 $this->orphanBlobClass = 'ConcatenatedGzipHistoryBlob'; 00105 } 00106 00107 function debug( $msg ) { 00108 wfDebug( "$msg\n" ); 00109 if ( $this->debugLog ) { 00110 $this->logToFile( $msg, $this->debugLog ); 00111 } 00112 00113 } 00114 00115 function info( $msg ) { 00116 echo "$msg\n"; 00117 if ( $this->infoLog ) { 00118 $this->logToFile( $msg, $this->infoLog ); 00119 } 00120 } 00121 00122 function critical( $msg ) { 00123 echo "$msg\n"; 00124 if ( $this->criticalLog ) { 00125 $this->logToFile( $msg, $this->criticalLog ); 00126 } 00127 } 00128 00129 function logToFile( $msg, $file ) { 00130 $header = '[' . date( 'd\TH:i:s' ) . '] ' . wfHostname() . ' ' . posix_getpid(); 00131 if ( $this->slaveId !== false ) { 00132 $header .= "({$this->slaveId})"; 00133 } 00134 $header .= ' ' . wfWikiID(); 00135 wfErrorLog( sprintf( "%-50s %s\n", $header, $msg ), $file ); 00136 } 00137 00143 function syncDBs() { 00144 $dbw = wfGetDB( DB_MASTER ); 00145 $dbr = wfGetDB( DB_SLAVE ); 00146 $pos = $dbw->getMasterPos(); 00147 $dbr->masterPosWait( $pos, 100000 ); 00148 } 00149 00153 function execute() { 00154 if ( $this->isChild ) { 00155 $this->executeChild(); 00156 } else { 00157 $this->executeParent(); 00158 } 00159 } 00160 00164 function executeParent() { 00165 if ( !$this->checkTrackingTable() ) { 00166 return; 00167 } 00168 00169 $this->syncDBs(); 00170 $this->startSlaveProcs(); 00171 $this->doAllPages(); 00172 $this->doAllOrphans(); 00173 $this->killSlaveProcs(); 00174 } 00175 00180 function checkTrackingTable() { 00181 $dbr = wfGetDB( DB_SLAVE ); 00182 if ( !$dbr->tableExists( 'blob_tracking' ) ) { 00183 $this->critical( "Error: blob_tracking table does not exist" ); 00184 return false; 00185 } 00186 $row = $dbr->selectRow( 'blob_tracking', '*', false, __METHOD__ ); 00187 if ( !$row ) { 00188 $this->info( "Warning: blob_tracking table contains no rows, skipping this wiki." ); 00189 return false; 00190 } 00191 return true; 00192 } 00193 00200 function startSlaveProcs() { 00201 $cmd = 'php ' . wfEscapeShellArg( __FILE__ ); 00202 foreach ( self::$cmdLineOptionMap as $cmdOption => $classOption ) { 00203 if ( $cmdOption == 'slave-id' ) { 00204 continue; 00205 } elseif ( in_array( $cmdOption, self::$optionsWithArgs ) && isset( $this->$classOption ) ) { 00206 $cmd .= " --$cmdOption " . wfEscapeShellArg( $this->$classOption ); 00207 } elseif ( $this->$classOption ) { 00208 $cmd .= " --$cmdOption"; 00209 } 00210 } 00211 $cmd .= ' --child' . 00212 ' --wiki ' . wfEscapeShellArg( wfWikiID() ) . 00213 ' ' . call_user_func_array( 'wfEscapeShellArg', $this->destClusters ); 00214 00215 $this->slavePipes = $this->slaveProcs = array(); 00216 for ( $i = 0; $i < $this->numProcs; $i++ ) { 00217 $pipes = false; 00218 $spec = array( 00219 array( 'pipe', 'r' ), 00220 array( 'file', 'php://stdout', 'w' ), 00221 array( 'file', 'php://stderr', 'w' ) 00222 ); 00223 wfSuppressWarnings(); 00224 $proc = proc_open( "$cmd --slave-id $i", $spec, $pipes ); 00225 wfRestoreWarnings(); 00226 if ( !$proc ) { 00227 $this->critical( "Error opening slave process: $cmd" ); 00228 exit( 1 ); 00229 } 00230 $this->slaveProcs[$i] = $proc; 00231 $this->slavePipes[$i] = $pipes[0]; 00232 } 00233 $this->prevSlaveId = -1; 00234 } 00235 00239 function killSlaveProcs() { 00240 $this->info( "Waiting for slave processes to finish..." ); 00241 for ( $i = 0; $i < $this->numProcs; $i++ ) { 00242 $this->dispatchToSlave( $i, 'quit' ); 00243 } 00244 for ( $i = 0; $i < $this->numProcs; $i++ ) { 00245 $status = proc_close( $this->slaveProcs[$i] ); 00246 if ( $status ) { 00247 $this->critical( "Warning: child #$i exited with status $status" ); 00248 } 00249 } 00250 $this->info( "Done." ); 00251 } 00252 00257 function dispatch( /*...*/ ) { 00258 $args = func_get_args(); 00259 $pipes = $this->slavePipes; 00260 $numPipes = stream_select( $x = array(), $pipes, $y = array(), 3600 ); 00261 if ( !$numPipes ) { 00262 $this->critical( "Error waiting to write to slaves. Aborting" ); 00263 exit( 1 ); 00264 } 00265 for ( $i = 0; $i < $this->numProcs; $i++ ) { 00266 $slaveId = ( $i + $this->prevSlaveId + 1 ) % $this->numProcs; 00267 if ( isset( $pipes[$slaveId] ) ) { 00268 $this->prevSlaveId = $slaveId; 00269 $this->dispatchToSlave( $slaveId, $args ); 00270 return; 00271 } 00272 } 00273 $this->critical( "Unreachable" ); 00274 exit( 1 ); 00275 } 00276 00280 function dispatchToSlave( $slaveId, $args ) { 00281 $args = (array)$args; 00282 $cmd = implode( ' ', $args ); 00283 fwrite( $this->slavePipes[$slaveId], "$cmd\n" ); 00284 } 00285 00289 function doAllPages() { 00290 $dbr = wfGetDB( DB_SLAVE ); 00291 $i = 0; 00292 $startId = 0; 00293 if ( $this->noCount ) { 00294 $numPages = '[unknown]'; 00295 } else { 00296 $numPages = $dbr->selectField( 'blob_tracking', 00297 'COUNT(DISTINCT bt_page)', 00298 # A condition is required so that this query uses the index 00299 array( 'bt_moved' => 0 ), 00300 __METHOD__ 00301 ); 00302 } 00303 if ( $this->copyOnly ) { 00304 $this->info( "Copying pages..." ); 00305 } else { 00306 $this->info( "Moving pages..." ); 00307 } 00308 while ( true ) { 00309 $res = $dbr->select( 'blob_tracking', 00310 array( 'bt_page' ), 00311 array( 00312 'bt_moved' => 0, 00313 'bt_page > ' . $dbr->addQuotes( $startId ) 00314 ), 00315 __METHOD__, 00316 array( 00317 'DISTINCT', 00318 'ORDER BY' => 'bt_page', 00319 'LIMIT' => $this->batchSize, 00320 ) 00321 ); 00322 if ( !$res->numRows() ) { 00323 break; 00324 } 00325 foreach ( $res as $row ) { 00326 $this->dispatch( 'doPage', $row->bt_page ); 00327 $i++; 00328 } 00329 $startId = $row->bt_page; 00330 $this->report( 'pages', $i, $numPages ); 00331 } 00332 $this->report( 'pages', $i, $numPages ); 00333 if ( $this->copyOnly ) { 00334 $this->info( "All page copies queued." ); 00335 } else { 00336 $this->info( "All page moves queued." ); 00337 } 00338 } 00339 00343 function report( $label, $current, $end ) { 00344 $this->numBatches++; 00345 if ( $current == $end || $this->numBatches >= $this->reportingInterval ) { 00346 $this->numBatches = 0; 00347 $this->info( "$label: $current / $end" ); 00348 $this->waitForSlaves(); 00349 } 00350 } 00351 00355 function doAllOrphans() { 00356 $dbr = wfGetDB( DB_SLAVE ); 00357 $startId = 0; 00358 $i = 0; 00359 if ( $this->noCount ) { 00360 $numOrphans = '[unknown]'; 00361 } else { 00362 $numOrphans = $dbr->selectField( 'blob_tracking', 00363 'COUNT(DISTINCT bt_text_id)', 00364 array( 'bt_moved' => 0, 'bt_page' => 0 ), 00365 __METHOD__ ); 00366 if ( !$numOrphans ) { 00367 return; 00368 } 00369 } 00370 if ( $this->copyOnly ) { 00371 $this->info( "Copying orphans..." ); 00372 } else { 00373 $this->info( "Moving orphans..." ); 00374 } 00375 00376 while ( true ) { 00377 $res = $dbr->select( 'blob_tracking', 00378 array( 'bt_text_id' ), 00379 array( 00380 'bt_moved' => 0, 00381 'bt_page' => 0, 00382 'bt_text_id > ' . $dbr->addQuotes( $startId ) 00383 ), 00384 __METHOD__, 00385 array( 00386 'DISTINCT', 00387 'ORDER BY' => 'bt_text_id', 00388 'LIMIT' => $this->batchSize 00389 ) 00390 ); 00391 if ( !$res->numRows() ) { 00392 break; 00393 } 00394 $ids = array(); 00395 foreach ( $res as $row ) { 00396 $ids[] = $row->bt_text_id; 00397 $i++; 00398 } 00399 // Need to send enough orphan IDs to the child at a time to fill a blob, 00400 // so orphanBatchSize needs to be at least ~100. 00401 // batchSize can be smaller or larger. 00402 while ( count( $ids ) > $this->orphanBatchSize ) { 00403 $args = array_slice( $ids, 0, $this->orphanBatchSize ); 00404 $ids = array_slice( $ids, $this->orphanBatchSize ); 00405 array_unshift( $args, 'doOrphanList' ); 00406 call_user_func_array( array( $this, 'dispatch' ), $args ); 00407 } 00408 if ( count( $ids ) ) { 00409 $args = $ids; 00410 array_unshift( $args, 'doOrphanList' ); 00411 call_user_func_array( array( $this, 'dispatch' ), $args ); 00412 } 00413 00414 $startId = $row->bt_text_id; 00415 $this->report( 'orphans', $i, $numOrphans ); 00416 } 00417 $this->report( 'orphans', $i, $numOrphans ); 00418 $this->info( "All orphans queued." ); 00419 } 00420 00424 function executeChild() { 00425 $this->debug( 'starting' ); 00426 $this->syncDBs(); 00427 00428 while ( !feof( STDIN ) ) { 00429 $line = rtrim( fgets( STDIN ) ); 00430 if ( $line == '' ) { 00431 continue; 00432 } 00433 $this->debug( $line ); 00434 $args = explode( ' ', $line ); 00435 $cmd = array_shift( $args ); 00436 switch ( $cmd ) { 00437 case 'doPage': 00438 $this->doPage( intval( $args[0] ) ); 00439 break; 00440 case 'doOrphanList': 00441 $this->doOrphanList( array_map( 'intval', $args ) ); 00442 break; 00443 case 'quit': 00444 return; 00445 } 00446 $this->waitForSlaves(); 00447 } 00448 } 00449 00453 function doPage( $pageId ) { 00454 $title = Title::newFromId( $pageId ); 00455 if ( $title ) { 00456 $titleText = $title->getPrefixedText(); 00457 } else { 00458 $titleText = '[deleted]'; 00459 } 00460 $dbr = wfGetDB( DB_SLAVE ); 00461 00462 // Finish any incomplete transactions 00463 if ( !$this->copyOnly ) { 00464 $this->finishIncompleteMoves( array( 'bt_page' => $pageId ) ); 00465 $this->syncDBs(); 00466 } 00467 00468 $startId = 0; 00469 $trx = new CgzCopyTransaction( $this, $this->pageBlobClass ); 00470 00471 while ( true ) { 00472 $res = $dbr->select( 00473 array( 'blob_tracking', 'text' ), 00474 '*', 00475 array( 00476 'bt_page' => $pageId, 00477 'bt_text_id > ' . $dbr->addQuotes( $startId ), 00478 'bt_moved' => 0, 00479 'bt_new_url IS NULL', 00480 'bt_text_id=old_id', 00481 ), 00482 __METHOD__, 00483 array( 00484 'ORDER BY' => 'bt_text_id', 00485 'LIMIT' => $this->batchSize 00486 ) 00487 ); 00488 if ( !$res->numRows() ) { 00489 break; 00490 } 00491 00492 $lastTextId = 0; 00493 foreach ( $res as $row ) { 00494 if ( $lastTextId == $row->bt_text_id ) { 00495 // Duplicate (null edit) 00496 continue; 00497 } 00498 $lastTextId = $row->bt_text_id; 00499 // Load the text 00500 $text = Revision::getRevisionText( $row ); 00501 if ( $text === false ) { 00502 $this->critical( "Error loading {$row->bt_rev_id}/{$row->bt_text_id}" ); 00503 continue; 00504 } 00505 00506 // Queue it 00507 if ( !$trx->addItem( $text, $row->bt_text_id ) ) { 00508 $this->debug( "$titleText: committing blob with " . $trx->getSize() . " items" ); 00509 $trx->commit(); 00510 $trx = new CgzCopyTransaction( $this, $this->pageBlobClass ); 00511 $this->waitForSlaves(); 00512 } 00513 } 00514 $startId = $row->bt_text_id; 00515 } 00516 00517 $this->debug( "$titleText: committing blob with " . $trx->getSize() . " items" ); 00518 $trx->commit(); 00519 } 00520 00531 function moveTextRow( $textId, $url ) { 00532 if ( $this->copyOnly ) { 00533 $this->critical( "Internal error: can't call moveTextRow() in --copy-only mode" ); 00534 exit( 1 ); 00535 } 00536 $dbw = wfGetDB( DB_MASTER ); 00537 $dbw->begin( __METHOD__ ); 00538 $dbw->update( 'text', 00539 array( // set 00540 'old_text' => $url, 00541 'old_flags' => 'external,utf-8', 00542 ), 00543 array( // where 00544 'old_id' => $textId 00545 ), 00546 __METHOD__ 00547 ); 00548 $dbw->update( 'blob_tracking', 00549 array( 'bt_moved' => 1 ), 00550 array( 'bt_text_id' => $textId ), 00551 __METHOD__ 00552 ); 00553 $dbw->commit( __METHOD__ ); 00554 } 00555 00564 function finishIncompleteMoves( $conds ) { 00565 $dbr = wfGetDB( DB_SLAVE ); 00566 00567 $startId = 0; 00568 $conds = array_merge( $conds, array( 00569 'bt_moved' => 0, 00570 'bt_new_url IS NOT NULL' 00571 ) ); 00572 while ( true ) { 00573 $res = $dbr->select( 'blob_tracking', 00574 '*', 00575 array_merge( $conds, array( 'bt_text_id > ' . $dbr->addQuotes( $startId ) ) ), 00576 __METHOD__, 00577 array( 00578 'ORDER BY' => 'bt_text_id', 00579 'LIMIT' => $this->batchSize, 00580 ) 00581 ); 00582 if ( !$res->numRows() ) { 00583 break; 00584 } 00585 $this->debug( 'Incomplete: ' . $res->numRows() . ' rows' ); 00586 foreach ( $res as $row ) { 00587 $this->moveTextRow( $row->bt_text_id, $row->bt_new_url ); 00588 if ( $row->bt_text_id % 10 == 0 ) { 00589 $this->waitForSlaves(); 00590 } 00591 } 00592 $startId = $row->bt_text_id; 00593 } 00594 } 00595 00600 function getTargetCluster() { 00601 $cluster = next( $this->destClusters ); 00602 if ( $cluster === false ) { 00603 $cluster = reset( $this->destClusters ); 00604 } 00605 return $cluster; 00606 } 00607 00613 function getExtDB( $cluster ) { 00614 $lb = wfGetLBFactory()->getExternalLB( $cluster ); 00615 return $lb->getConnection( DB_MASTER ); 00616 } 00617 00621 function doOrphanList( $textIds ) { 00622 // Finish incomplete moves 00623 if ( !$this->copyOnly ) { 00624 $this->finishIncompleteMoves( array( 'bt_text_id' => $textIds ) ); 00625 $this->syncDBs(); 00626 } 00627 00628 $trx = new CgzCopyTransaction( $this, $this->orphanBlobClass ); 00629 00630 $res = wfGetDB( DB_SLAVE )->select( 00631 array( 'text', 'blob_tracking' ), 00632 array( 'old_id', 'old_text', 'old_flags' ), 00633 array( 00634 'old_id' => $textIds, 00635 'bt_text_id=old_id', 00636 'bt_moved' => 0, 00637 ), 00638 __METHOD__, 00639 array( 'DISTINCT' ) 00640 ); 00641 00642 foreach ( $res as $row ) { 00643 $text = Revision::getRevisionText( $row ); 00644 if ( $text === false ) { 00645 $this->critical( "Error: cannot load revision text for old_id={$row->old_id}" ); 00646 continue; 00647 } 00648 00649 if ( !$trx->addItem( $text, $row->old_id ) ) { 00650 $this->debug( "[orphan]: committing blob with " . $trx->getSize() . " rows" ); 00651 $trx->commit(); 00652 $trx = new CgzCopyTransaction( $this, $this->orphanBlobClass ); 00653 $this->waitForSlaves(); 00654 } 00655 } 00656 $this->debug( "[orphan]: committing blob with " . $trx->getSize() . " rows" ); 00657 $trx->commit(); 00658 } 00659 00663 function waitForSlaves() { 00664 $lb = wfGetLB(); 00665 while ( true ) { 00666 list( $host, $maxLag ) = $lb->getMaxLag(); 00667 if ( $maxLag < 2 ) { 00668 break; 00669 } 00670 sleep( 5 ); 00671 } 00672 } 00673 } 00674 00678 class CgzCopyTransaction { 00679 public $parent; 00680 public $blobClass; 00681 public $cgz; 00682 public $referrers; 00683 00687 function __construct( $parent, $blobClass ) { 00688 $this->blobClass = $blobClass; 00689 $this->cgz = false; 00690 $this->texts = array(); 00691 $this->parent = $parent; 00692 } 00693 00701 function addItem( $text, $textId ) { 00702 if ( !$this->cgz ) { 00703 $class = $this->blobClass; 00704 $this->cgz = new $class; 00705 } 00706 $hash = $this->cgz->addItem( $text ); 00707 $this->referrers[$textId] = $hash; 00708 $this->texts[$textId] = $text; 00709 return $this->cgz->isHappy(); 00710 } 00711 00712 function getSize() { 00713 return count( $this->texts ); 00714 } 00715 00719 function recompress() { 00720 $class = $this->blobClass; 00721 $this->cgz = new $class; 00722 $this->referrers = array(); 00723 foreach ( $this->texts as $textId => $text ) { 00724 $hash = $this->cgz->addItem( $text ); 00725 $this->referrers[$textId] = $hash; 00726 } 00727 } 00728 00734 function commit() { 00735 $originalCount = count( $this->texts ); 00736 if ( !$originalCount ) { 00737 return; 00738 } 00739 00740 // Check to see if the target text_ids have been moved already. 00741 // 00742 // We originally read from the slave, so this can happen when a single 00743 // text_id is shared between multiple pages. It's rare, but possible 00744 // if a delete/move/undelete cycle splits up a null edit. 00745 // 00746 // We do a locking read to prevent closer-run race conditions. 00747 $dbw = wfGetDB( DB_MASTER ); 00748 $dbw->begin( __METHOD__ ); 00749 $res = $dbw->select( 'blob_tracking', 00750 array( 'bt_text_id', 'bt_moved' ), 00751 array( 'bt_text_id' => array_keys( $this->referrers ) ), 00752 __METHOD__, array( 'FOR UPDATE' ) ); 00753 $dirty = false; 00754 foreach ( $res as $row ) { 00755 if ( $row->bt_moved ) { 00756 # This row has already been moved, remove it 00757 $this->parent->debug( "TRX: conflict detected in old_id={$row->bt_text_id}" ); 00758 unset( $this->texts[$row->bt_text_id] ); 00759 $dirty = true; 00760 } 00761 } 00762 00763 // Recompress the blob if necessary 00764 if ( $dirty ) { 00765 if ( !count( $this->texts ) ) { 00766 // All have been moved already 00767 if ( $originalCount > 1 ) { 00768 // This is suspcious, make noise 00769 $this->critical( "Warning: concurrent operation detected, are there two conflicting " . 00770 "processes running, doing the same job?" ); 00771 } 00772 return; 00773 } 00774 $this->recompress(); 00775 } 00776 00777 // Insert the data into the destination cluster 00778 $targetCluster = $this->parent->getTargetCluster(); 00779 $store = $this->parent->store; 00780 $targetDB = $store->getMaster( $targetCluster ); 00781 $targetDB->clearFlag( DBO_TRX ); // we manage the transactions 00782 $targetDB->begin( __METHOD__ ); 00783 $baseUrl = $this->parent->store->store( $targetCluster, serialize( $this->cgz ) ); 00784 00785 // Write the new URLs to the blob_tracking table 00786 foreach ( $this->referrers as $textId => $hash ) { 00787 $url = $baseUrl . '/' . $hash; 00788 $dbw->update( 'blob_tracking', 00789 array( 'bt_new_url' => $url ), 00790 array( 00791 'bt_text_id' => $textId, 00792 'bt_moved' => 0, # Check for concurrent conflicting update 00793 ), 00794 __METHOD__ 00795 ); 00796 } 00797 00798 $targetDB->commit( __METHOD__ ); 00799 // Critical section here: interruption at this point causes blob duplication 00800 // Reversing the order of the commits would cause data loss instead 00801 $dbw->commit( __METHOD__ ); 00802 00803 // Write the new URLs to the text table and set the moved flag 00804 if ( !$this->parent->copyOnly ) { 00805 foreach ( $this->referrers as $textId => $hash ) { 00806 $url = $baseUrl . '/' . $hash; 00807 $this->parent->moveTextRow( $textId, $url ); 00808 } 00809 } 00810 } 00811 }