Use job queue for deletion of pages with many revisions
authorBill Pirkle <bpirkle@wikimedia.org>
Tue, 28 Aug 2018 22:01:48 +0000 (17:01 -0500)
committerBill Pirkle <bpirkle@wikimedia.org>
Thu, 4 Oct 2018 00:16:14 +0000 (19:16 -0500)
Pages with many revisions experience transaction size exceptions,
due to archiving revisions.  Use the job queue to split the work
into batches and avoid exceptions.

Bug: T198176
Change-Id: Ie800fb5a46be837ac91b24b9402ee90b0355d6cd

autoload.php
includes/DefaultSettings.php
includes/jobqueue/jobs/DeletePageJob.php [new file with mode: 0644]
includes/page/Article.php
includes/page/WikiPage.php
includes/specials/SpecialMovepage.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/deleteBatch.php

index a0f5056..2fda8be 100644 (file)
@@ -384,6 +384,7 @@ $wgAutoloadLocalClasses = [
        'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php',
        'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php',
        'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php',
+       'DeletePageJob' => __DIR__ . '/includes/jobqueue/jobs/DeletePageJob.php',
        'DeleteSelfExternals' => __DIR__ . '/maintenance/deleteSelfExternals.php',
        'DeletedContribsPager' => __DIR__ . '/includes/specials/pagers/DeletedContribsPager.php',
        'DeletedContributionsPage' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php',
index 2668cd7..a695e76 100644 (file)
@@ -5534,6 +5534,12 @@ $wgAvailableRights = [];
  */
 $wgDeleteRevisionsLimit = 0;
 
+/**
+ * Page deletions with > this number of revisions will use the job queue.
+ * Revisions will be archived in batches of (at most) this size, one batch per job.
+ */
+$wgDeleteRevisionsBatchSize = 1000;
+
 /**
  * The maximum number of edits a user can have and
  * can still be hidden by users with the hideuser permission.
@@ -7518,6 +7524,7 @@ $wgServiceWiringFiles = [
  * or (since 1.30) a callback to use for creating the job object.
  */
 $wgJobClasses = [
+       'deletePage' => DeletePageJob::class,
        'refreshLinks' => RefreshLinksJob::class,
        'deleteLinks' => DeleteLinksJob::class,
        'htmlCacheUpdate' => HTMLCacheUpdateJob::class,
diff --git a/includes/jobqueue/jobs/DeletePageJob.php b/includes/jobqueue/jobs/DeletePageJob.php
new file mode 100644 (file)
index 0000000..9b5cef4
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Class DeletePageJob
+ */
+class DeletePageJob extends Job {
+       public function __construct( $title, $params ) {
+               parent::__construct( 'deletePage', $title, $params );
+       }
+
+       /**
+        * Execute the job
+        *
+        * @return bool
+        */
+       public function run() {
+               // Failure to load the page is not job failure.
+               // A parallel deletion operation may have already completed the page deletion.
+               $wikiPage = WikiPage::newFromID( $this->params['wikiPageId'] );
+               if ( $wikiPage ) {
+                       $wikiPage->doDeleteArticleBatched(
+                               $this->params['reason'],
+                               $this->params['suppress'],
+                               User::newFromId( $this->params['userId'] ),
+                               json_decode( $this->params['tags'] ),
+                               $this->params['logsubtype'],
+                               false,
+                               $this->getRequestId() );
+               }
+               return true;
+       }
+}
index 4a689d3..db96cf4 100644 (file)
@@ -2053,25 +2053,31 @@ class Article implements Page {
         * Perform a deletion and output success or failure messages
         * @param string $reason
         * @param bool $suppress
+        * @param bool $immediate false allows deleting over time via the job queue
+        * @throws FatalError
+        * @throws MWException
         */
-       public function doDelete( $reason, $suppress = false ) {
+       public function doDelete( $reason, $suppress = false, $immediate = false ) {
                $error = '';
                $context = $this->getContext();
                $outputPage = $context->getOutput();
                $user = $context->getUser();
-               $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user );
+               $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user,
+                       [], 'delete', $immediate );
 
-               if ( $status->isGood() ) {
+               if ( $status->isOK() ) {
                        $deleted = $this->getTitle()->getPrefixedText();
 
                        $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
                        $outputPage->setRobotPolicy( 'noindex,nofollow' );
 
-                       $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
-
-                       $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
-
-                       Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
+                       if ( $status->isGood() ) {
+                               $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
+                               $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
+                               Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
+                       } else {
+                               $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
+                       }
 
                        $outputPage->returnToMain( false );
                } else {
@@ -2297,10 +2303,10 @@ class Article implements Page {
         */
        public function doDeleteArticleReal(
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
-               $tags = []
+               $tags = [], $immediate = false
        ) {
                return $this->mPage->doDeleteArticleReal(
-                       $reason, $suppress, $u1, $u2, $error, $user, $tags
+                       $reason, $suppress, $u1, $u2, $error, $user, $tags, 'delete', $immediate
                );
        }
 
@@ -2826,12 +2832,16 @@ class Article implements Page {
         * @param int|null $u1 Unused
         * @param bool|null $u2 Unused
         * @param string &$error
+        * @param bool $immediate false allows deleting over time via the job queue
         * @return bool
+        * @throws FatalError
+        * @throws MWException
         */
        public function doDeleteArticle(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = ''
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', $immediate = false
        ) {
-               return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error );
+               return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error,
+                       null, $immediate );
        }
 
        /**
index 7c97465..7c0450d 100644 (file)
@@ -2512,6 +2512,28 @@ class WikiPage implements Page, IDBAccessObject {
                return implode( ':', $bits );
        }
 
+       /**
+        * Determines if deletion of this page would be batched (executed over time by the job queue)
+        * or not (completed in the same request as the delete call).
+        *
+        * It is unlikely but possible that an edit from another request could push the page over the
+        * batching threshold after this function is called, but before the caller acts upon the
+        * return value.  Callers must decide for themselves how to deal with this.  $safetyMargin
+        * is provided as an unreliable but situationally useful help for some common cases.
+        *
+        * @param int $safetyMargin Added to the revision count when checking for batching
+        * @return bool True if deletion would be batched, false otherwise
+        */
+       public function isBatchedDelete( $safetyMargin = 0 ) {
+               global $wgDeleteRevisionsBatchSize;
+
+               $dbr = wfGetDB( DB_REPLICA );
+               $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
+               $revCount += $safetyMargin;
+
+               return $revCount >= $wgDeleteRevisionsBatchSize;
+       }
+
        /**
         * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
         * backwards compatibility, if you care about error reporting you should use
@@ -2526,13 +2548,20 @@ class WikiPage implements Page, IDBAccessObject {
         * @param bool|null $u2 Unused
         * @param array|string &$error Array of errors to append to
         * @param User|null $user The deleting user
+        * @param bool $immediate false allows deleting over time via the job queue
         * @return bool True if successful
+        * @throws FatalError
+        * @throws MWException
         */
        public function doDeleteArticle(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+               $immediate = false
        ) {
-               $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
-               return $status->isGood();
+               $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
+                       [], 'delete', $immediate );
+
+               // Returns true if the page was actually deleted, or is scheduled for deletion
+               return $status->isOK();
        }
 
        /**
@@ -2550,27 +2579,23 @@ class WikiPage implements Page, IDBAccessObject {
         * @param User|null $deleter The deleting user
         * @param array $tags Tags to apply to the deletion action
         * @param string $logsubtype
+        * @param bool $immediate false allows deleting over time via the job queue
         * @return Status Status object; if successful, $status->value is the log_id of the
         *   deletion log entry. If the page couldn't be deleted because it wasn't
         *   found, $status is a non-fatal 'cannotdelete' error
+        * @throws FatalError
+        * @throws MWException
         */
        public function doDeleteArticleReal(
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
-               $tags = [], $logsubtype = 'delete'
+               $tags = [], $logsubtype = 'delete', $immediate = false
        ) {
-               global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
-                       $wgActorTableSchemaMigrationStage, $wgMultiContentRevisionSchemaMigrationStage;
+               global $wgUser;
 
                wfDebug( __METHOD__ . "\n" );
 
                $status = Status::newGood();
 
-               if ( $this->mTitle->getDBkey() === '' ) {
-                       $status->error( 'cannotdelete',
-                               wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
-                       return $status;
-               }
-
                // Avoid PHP 7.1 warning of passing $this by reference
                $wikiPage = $this;
 
@@ -2585,6 +2610,26 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
+               return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
+                       $logsubtype, $immediate );
+       }
+
+       /**
+        * Back-end article deletion
+        *
+        * Only invokes batching via the job queue if necessary per $wgDeleteRevisionsBatchSize.
+        * Deletions can often be completed inline without involving the job queue.
+        *
+        * Potentially called many times per deletion operation for pages with many revisions.
+        */
+       public function doDeleteArticleBatched(
+               $reason, $suppress, User $deleter, $tags,
+               $logsubtype, $immediate = false, $webRequestId = null
+       ) {
+               wfDebug( __METHOD__ . "\n" );
+
+               $status = Status::newGood();
+
                $dbw = wfGetDB( DB_MASTER );
                $dbw->startAtomic( __METHOD__ );
 
@@ -2603,11 +2648,7 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
-               // Given the lock above, we can be confident in the title and page ID values
-               $namespace = $this->getTitle()->getNamespace();
-               $dbKey = $this->getTitle()->getDBkey();
-
-               // At this point we are now comitted to returning an OK
+               // At this point we are now committed to returning an OK
                // status unless some DB query error or other exception comes up.
                // This way callers don't have to call rollback() if $status is bad
                // unless they actually try to catch exceptions (which is rare).
@@ -2623,6 +2664,133 @@ class WikiPage implements Page, IDBAccessObject {
                        $content = null;
                }
 
+               // Archive revisions.  In immediate mode, archive all revisions.  Otherwise, archive
+               // one batch of revisions and defer archival of any others to the job queue.
+               $explictTrxLogged = false;
+               while ( true ) {
+                       $done = $this->archiveRevisions( $dbw, $id, $suppress );
+                       if ( $done || !$immediate ) {
+                               break;
+                       }
+                       $dbw->endAtomic( __METHOD__ );
+                       if ( $dbw->explicitTrxActive() ) {
+                               // Explict transactions may never happen here in practice.  Log to be sure.
+                               if ( !$explictTrxLogged ) {
+                                       $explictTrxLogged = true;
+                                       LoggerFactory::getInstance( 'wfDebug' )->debug(
+                                               'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
+                                               'title' => $this->getTitle()->getText(),
+                                       ] );
+                               }
+                               continue;
+                       }
+                       if ( $dbw->trxLevel() ) {
+                               $dbw->commit();
+                       }
+                       $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                       $lbFactory->waitForReplication();
+                       $dbw->startAtomic( __METHOD__ );
+               }
+
+               // If done archiving, also delete the article.
+               if ( !$done ) {
+                       $dbw->endAtomic( __METHOD__ );
+
+                       $jobParams = [
+                               'wikiPageId' => $id,
+                               'requestId' => $webRequestId ?? WebRequest::getRequestId(),
+                               'reason' => $reason,
+                               'suppress' => $suppress,
+                               'userId' => $deleter->getId(),
+                               'tags' => json_encode( $tags ),
+                               'logsubtype' => $logsubtype,
+                       ];
+
+                       $job = new DeletePageJob( $this->getTitle(), $jobParams );
+                       JobQueueGroup::singleton()->push( $job );
+
+                       $status->warning( 'delete-scheduled',
+                               wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+               } else {
+                       // Get archivedRevisionCount by db query, because there's no better alternative.
+                       // Jobs cannot pass a count of archived revisions to the next job, because additional
+                       // deletion operations can be started while the first is running.  Jobs from each
+                       // gracefully interleave, but would not know about each other's count.  Deduplication
+                       // in the job queue to avoid simultaneous deletion operations would add overhead.
+                       // Number of archived revisions cannot be known beforehand, because edits can be made
+                       // while deletion operations are being processed, changing the number of archivals.
+                       $archivedRevisionCount = $dbw->selectRowCount(
+                               'archive', '1', [ 'ar_page_id' => $id ], __METHOD__
+                       );
+
+                       // Clone the title and wikiPage, so we have the information we need when
+                       // we log and run the ArticleDeleteComplete hook.
+                       $logTitle = clone $this->mTitle;
+                       $wikiPageBeforeDelete = clone $this;
+
+                       // Now that it's safely backed up, delete it
+                       $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+
+                       // Log the deletion, if the page was suppressed, put it in the suppression log instead
+                       $logtype = $suppress ? 'suppress' : 'delete';
+
+                       $logEntry = new ManualLogEntry( $logtype, $logsubtype );
+                       $logEntry->setPerformer( $deleter );
+                       $logEntry->setTarget( $logTitle );
+                       $logEntry->setComment( $reason );
+                       $logEntry->setTags( $tags );
+                       $logid = $logEntry->insert();
+
+                       $dbw->onTransactionPreCommitOrIdle(
+                               function () use ( $logEntry, $logid ) {
+                                       // T58776: avoid deadlocks (especially from FileDeleteForm)
+                                       $logEntry->publish( $logid );
+                               },
+                               __METHOD__
+                       );
+
+                       $dbw->endAtomic( __METHOD__ );
+
+                       $this->doDeleteUpdates( $id, $content, $revision, $deleter );
+
+                       Hooks::run( 'ArticleDeleteComplete', [
+                               &$wikiPageBeforeDelete,
+                               &$deleter,
+                               $reason,
+                               $id,
+                               $content,
+                               $logEntry,
+                               $archivedRevisionCount
+                       ] );
+                       $status->value = $logid;
+
+                       // Show log excerpt on 404 pages rather than just a link
+                       $cache = MediaWikiServices::getInstance()->getMainObjectStash();
+                       $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
+                       $cache->set( $key, 1, $cache::TTL_DAY );
+               }
+
+               return $status;
+       }
+
+       /**
+        * Archives revisions as part of page deletion.
+        *
+        * @param IDatabase $dbw
+        * @param int $id
+        * @param bool $suppress Suppress all revisions and log the deletion in
+        *   the suppression log instead of the deletion log
+        * @return bool
+        */
+       protected function archiveRevisions( $dbw, $id, $suppress ) {
+               global $wgContentHandlerUseDB, $wgMultiContentRevisionSchemaMigrationStage,
+                       $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage,
+                       $wgDeleteRevisionsBatchSize;
+
+               // Given the lock above, we can be confident in the title and page ID values
+               $namespace = $this->getTitle()->getNamespace();
+               $dbKey = $this->getTitle()->getDBkey();
+
                $commentStore = CommentStore::getStore();
                $actorMigration = ActorMigration::newMigration();
 
@@ -2669,13 +2837,14 @@ class WikiPage implements Page, IDBAccessObject {
                        }
                }
 
-                       // Get all of the page revisions
+               // Get as many of the page revisions as we are allowed to.  The +1 lets us recognize the
+               // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
                $res = $dbw->select(
                        $revQuery['tables'],
                        $revQuery['fields'],
                        [ 'rev_page' => $id ],
                        __METHOD__,
-                       [],
+                       [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
                        $revQuery['joins']
                );
 
@@ -2686,16 +2855,22 @@ class WikiPage implements Page, IDBAccessObject {
                /** @var int[] Revision IDs of edits that were made by IPs */
                $ipRevIds = [];
 
+               $done = true;
                foreach ( $res as $row ) {
+                       if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
+                               $done = false;
+                               break;
+                       }
+
                        $comment = $commentStore->getComment( 'rev_comment', $row );
                        $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
                        $rowInsert = [
-                               'ar_namespace'  => $namespace,
-                               'ar_title'      => $dbKey,
-                               'ar_timestamp'  => $row->rev_timestamp,
-                               'ar_minor_edit' => $row->rev_minor_edit,
-                               'ar_rev_id'     => $row->rev_id,
-                               'ar_parent_id'  => $row->rev_parent_id,
+                                       'ar_namespace'  => $namespace,
+                                       'ar_title'      => $dbKey,
+                                       'ar_timestamp'  => $row->rev_timestamp,
+                                       'ar_minor_edit' => $row->rev_minor_edit,
+                                       'ar_rev_id'     => $row->rev_id,
+                                       'ar_parent_id'  => $row->rev_parent_id,
                                        /**
                                         * ar_text_id should probably not be written to when the multi content schema has
                                         * been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no
@@ -2704,11 +2879,11 @@ class WikiPage implements Page, IDBAccessObject {
                                         * Task: https://phabricator.wikimedia.org/T190148
                                         * Copying the value from the revision table should not lead to any issues for now.
                                         */
-                               'ar_len'        => $row->rev_len,
-                               'ar_page_id'    => $id,
-                               'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
-                               'ar_sha1'       => $row->rev_sha1,
-                       ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+                                       'ar_len'        => $row->rev_len,
+                                       'ar_page_id'    => $id,
+                                       'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
+                                       'ar_sha1'       => $row->rev_sha1,
+                               ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
                                + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
 
                        if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
@@ -2729,70 +2904,27 @@ class WikiPage implements Page, IDBAccessObject {
                                $ipRevIds[] = $row->rev_id;
                        }
                }
-               // Copy them into the archive table
-               $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
-               // Save this so we can pass it to the ArticleDeleteComplete hook.
-               $archivedRevisionCount = $dbw->affectedRows();
 
-               // Clone the title and wikiPage, so we have the information we need when
-               // we log and run the ArticleDeleteComplete hook.
-               $logTitle = clone $this->mTitle;
-               $wikiPageBeforeDelete = clone $this;
+               // This conditional is just a sanity check
+               if ( count( $revids ) > 0 ) {
+                       // Copy them into the archive table
+                       $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
 
-               // Now that it's safely backed up, delete it
-               $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
-               $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
-               if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
-                       $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
-               }
-               if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
-                       $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
-               }
+                       $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
+                       if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
+                       }
+                       if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+                               $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
+                       }
 
-               // Also delete records from ip_changes as applicable.
-               if ( count( $ipRevIds ) > 0 ) {
-                       $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
+                       // Also delete records from ip_changes as applicable.
+                       if ( count( $ipRevIds ) > 0 ) {
+                               $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
+                       }
                }
 
-               // Log the deletion, if the page was suppressed, put it in the suppression log instead
-               $logtype = $suppress ? 'suppress' : 'delete';
-
-               $logEntry = new ManualLogEntry( $logtype, $logsubtype );
-               $logEntry->setPerformer( $deleter );
-               $logEntry->setTarget( $logTitle );
-               $logEntry->setComment( $reason );
-               $logEntry->setTags( $tags );
-               $logid = $logEntry->insert();
-
-               $dbw->onTransactionPreCommitOrIdle(
-                       function () use ( $logEntry, $logid ) {
-                               // T58776: avoid deadlocks (especially from FileDeleteForm)
-                               $logEntry->publish( $logid );
-                       },
-                       __METHOD__
-               );
-
-               $dbw->endAtomic( __METHOD__ );
-
-               $this->doDeleteUpdates( $id, $content, $revision, $deleter );
-
-               Hooks::run( 'ArticleDeleteComplete', [
-                       &$wikiPageBeforeDelete,
-                       &$deleter,
-                       $reason,
-                       $id,
-                       $content,
-                       $logEntry,
-                       $archivedRevisionCount
-               ] );
-               $status->value = $logid;
-
-               // Show log excerpt on 404 pages rather than just a link
-               $cache = MediaWikiServices::getInstance()->getMainObjectStash();
-               $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
-               $cache->set( $key, 1, $cache::TTL_DAY );
-
-               return $status;
+               return $done;
        }
 
        /**
index 464be4f..2f6dc03 100644 (file)
@@ -547,6 +547,15 @@ class MovePageForm extends UnlistedSpecialPage {
                                return;
                        }
 
+                       $page = WikiPage::factory( $nt );
+
+                       // Small safety margin to guard against concurrent edits
+                       if ( $page->isBatchedDelete( 5 ) ) {
+                               $this->showForm( [ [ 'movepage-delete-first' ] ] );
+
+                               return;
+                       }
+
                        $reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
 
                        // Delete an associated image if there is
@@ -559,7 +568,6 @@ class MovePageForm extends UnlistedSpecialPage {
                        }
 
                        $error = ''; // passed by ref
-                       $page = WikiPage::factory( $nt );
                        $deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
                        if ( !$deleteStatus->isGood() ) {
                                $this->showForm( $deleteStatus->getErrorsArray() );
index 30a9699..f651e25 100644 (file)
        "badarticleerror": "This action cannot be performed on this page.",
        "cannotdelete": "The page or file \"$1\" could not be deleted.\nIt may have already been deleted by someone else.",
        "cannotdelete-title": "Cannot delete page \"$1\"",
+       "delete-scheduled": "The page \"$1\" is scheduled for deletion.\nPlease be patient.",
        "delete-hook-aborted": "Deletion aborted by hook.\nIt gave no explanation.",
        "no-null-revision": "Could not create new null revision for page \"$1\"",
        "badtitle": "Bad title",
        "movepage-moved": "<strong>\"$1\" has been moved to \"$2\"</strong>",
        "movepage-moved-redirect": "A redirect has been created.",
        "movepage-moved-noredirect": "The creation of a redirect has been suppressed.",
+       "movepage-delete-first": "The target page has too many revisions to delete as part of a page move.  Please first delete the page manually, then try again.",
        "articleexists": "A page of that name already exists, or the name you have chosen is not valid.\nPlease choose another name.",
        "cantmove-titleprotected": "You cannot move a page to this location because the new title has been protected from creation.",
        "movetalk": "Move associated talk page",
index eeb0a1c..75839a6 100644 (file)
        "badarticleerror": "Used as error message in moving page.\n\nSee also:\n* {{msg-mw|Articleexists}}\n* {{msg-mw|Bad-target-model}}",
        "cannotdelete": "Error message in deleting. Parameters:\n* $1 - page name or file name",
        "cannotdelete-title": "Title of error page when the user cannot delete a page. Parameters:\n* $1 - the page name",
+       "delete-scheduled": "Warning message shown when page deletion is deferred to the job queue, and therefore is not immediate.",
        "delete-hook-aborted": "Error message shown when an extension hook prevents a page deletion, but does not provide an error message.",
        "no-null-revision": "Error message shown when no null revision could be created to reflect a protection level change.\n\nAbout \"null revision\":\n* Create a new null-revision for insertion into a page's history. This will not re-save the text, but simply refer to the text from the previous version.\n* Such revisions can for instance identify page rename operations and other such meta-modifications.\n\nParameters:\n* $1 - page title",
        "badtitle": "The page title when a user requested a page with invalid page name. The content will be {{msg-mw|badtitletext}}.",
        "movepage-moved": "Message displayed after successfully moving a page from source to target name.\n\nParameters:\n* $1 - the source page as a link with display name\n* $2 - the target page as a link with display name\n* $3 - (optional) the source page name without a link\n* $4 - (optional) the target page name without a link\nSee also:\n* {{msg-mw|Movepage-moved-redirect}}\n* {{msg-mw|Movepage-moved-noredirect}}",
        "movepage-moved-redirect": "See also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-noredirect}}",
        "movepage-moved-noredirect": "The message is shown after pagemove if checkbox \"{{int:move-leave-redirect}}\" was unselected before moving.\n\nSee also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-redirect}}",
+       "movepage-delete-first": "Error message shown when trying to move a page and delete the existing page by that name, but the existing page has too many revisions.",
        "articleexists": "Used as error message when moving a page.\n\nSee also:\n* {{msg-mw|Badarticleerror}}\n* {{msg-mw|Bad-target-model}}",
        "cantmove-titleprotected": "Used as error message when moving a page.",
        "movetalk": "The text of the checkbox to watch the associated talk page to the page you are moving. This only appears when the talk page is not empty. Used in [[Special:MovePage]].\n\nSee also:\n* {{msg-mw|Move-page-legend|legend for the form}}\n* {{msg-mw|newtitle|label for new title}}\n* {{msg-mw|Movereason|label for textarea}}\n* {{msg-mw|Move-leave-redirect|label for checkbox}}\n* {{msg-mw|Fix-double-redirects|label for checkbox}}\n* {{msg-mw|Move-subpages|label for checkbox}}\n* {{msg-mw|Move-talk-subpages|label for checkbox}}\n* {{msg-mw|Move-watch|label for checkbox}}",
index 0f3c506..9e35687 100644 (file)
@@ -108,7 +108,7 @@ class DeleteBatch extends Maintenance {
                        }
                        $page = WikiPage::factory( $title );
                        $error = '';
-                       $success = $page->doDeleteArticle( $reason, false, 0, true, $error, $user );
+                       $success = $page->doDeleteArticle( $reason, false, null, null, $error, $user, true );
                        if ( $success ) {
                                $this->output( " Deleted!\n" );
                        } else {