From 99e6b43ab83329e286394b163fa74137344405b3 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Fri, 16 Jan 2015 17:02:10 -0800 Subject: [PATCH] Moved RecentChange::purgeExpiredChanges to a job * Also added a selectFieldValues helper method to the DB classes since this use case keeps coming up. Change-Id: I62cdbb497dc2c8fe4758e756d13688b85165e869 --- autoload.php | 1 + includes/DefaultSettings.php | 1 + includes/changes/RecentChange.php | 23 ------ includes/db/Database.php | 51 +++++++++++- .../jobqueue/jobs/RecentChangesUpdateJob.php | 81 +++++++++++++++++++ includes/page/WikiPage.php | 7 +- 6 files changed, 133 insertions(+), 31 deletions(-) create mode 100644 includes/jobqueue/jobs/RecentChangesUpdateJob.php diff --git a/autoload.php b/autoload.php index 46c8b01cc8..ab0102a065 100644 --- a/autoload.php +++ b/autoload.php @@ -938,6 +938,7 @@ $wgAutoloadLocalClasses = array( 'RebuildSitesCache' => __DIR__ . '/maintenance/rebuildSitesCache.php', 'RebuildTextIndex' => __DIR__ . '/maintenance/rebuildtextindex.php', 'RecentChange' => __DIR__ . '/includes/changes/RecentChange.php', + 'RecentChangesUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/RecentChangesUpdateJob.php', 'RecompressTracked' => __DIR__ . '/maintenance/storage/recompressTracked.php', 'RedirectSpecialArticle' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php', 'RedirectSpecialPage' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index ee462d84ad..be7d8c4ccd 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -6411,6 +6411,7 @@ $wgJobClasses = array( 'AssembleUploadChunks' => 'AssembleUploadChunksJob', 'PublishStashedFile' => 'PublishStashedFileJob', 'ThumbnailRender' => 'ThumbnailRenderJob', + 'recentChangesUpdate' => 'RecentChangesUpdateJob', 'null' => 'NullJob' ); diff --git a/includes/changes/RecentChange.php b/includes/changes/RecentChange.php index 86cd1d766e..b430bab91c 100644 --- a/includes/changes/RecentChange.php +++ b/includes/changes/RecentChange.php @@ -796,29 +796,6 @@ class RecentChange { return ChangesList::showCharacterDifference( $old, $new ); } - /** - * Purge expired changes from the recentchanges table - * @since 1.22 - */ - public static function purgeExpiredChanges() { - if ( wfReadOnly() ) { - return; - } - - $method = __METHOD__; - $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function () use ( $dbw, $method ) { - global $wgRCMaxAge; - - $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); - $dbw->delete( - 'recentchanges', - array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ), - $method - ); - } ); - } - private static function checkIPAddress( $ip ) { global $wgRequest; if ( $ip ) { diff --git a/includes/db/Database.php b/includes/db/Database.php index 054f27a909..cedd624520 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -1391,9 +1391,13 @@ abstract class DatabaseBase implements IDatabase { * * @return bool|mixed The value from the field, or false on failure. */ - public function selectField( $table, $var, $cond = '', $fname = __METHOD__, - $options = array() + public function selectField( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() ) { + if ( $var === '*' ) { // sanity + throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" ); + } + if ( !is_array( $options ) ) { $options = array( $options ); } @@ -1401,7 +1405,6 @@ abstract class DatabaseBase implements IDatabase { $options['LIMIT'] = 1; $res = $this->select( $table, $var, $cond, $fname, $options ); - if ( $res === false || !$this->numRows( $res ) ) { return false; } @@ -1415,6 +1418,48 @@ abstract class DatabaseBase implements IDatabase { } } + /** + * A SELECT wrapper which returns a list of single field values from result rows. + * + * Usually throws a DBQueryError on failure. If errors are explicitly + * ignored, returns false on failure. + * + * If no result rows are returned from the query, false is returned. + * + * @param string|array $table Table name. See DatabaseBase::select() for details. + * @param string $var The field name to select. This must be a valid SQL + * fragment: do not use unvalidated user input. + * @param string|array $cond The condition array. See DatabaseBase::select() for details. + * @param string $fname The function name of the caller. + * @param string|array $options The query options. See DatabaseBase::select() for details. + * + * @return bool|array The values from the field, or false on failure + * @since 1.25 + */ + public function selectFieldValues( + $table, $var, $cond = '', $fname = __METHOD__, $options = array() + ) { + if ( $var === '*' ) { // sanity + throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" ); + } + + if ( !is_array( $options ) ) { + $options = array( $options ); + } + + $res = $this->select( $table, $var, $cond, $fname, $options ); + if ( $res === false ) { + return false; + } + + $values = array(); + foreach ( $res as $row ) { + $values[] = $row->$var; + } + + return $values; + } + /** * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query. diff --git a/includes/jobqueue/jobs/RecentChangesUpdateJob.php b/includes/jobqueue/jobs/RecentChangesUpdateJob.php new file mode 100644 index 0000000000..9f22ba4a58 --- /dev/null +++ b/includes/jobqueue/jobs/RecentChangesUpdateJob.php @@ -0,0 +1,81 @@ +removeDuplicates = true; + } + + /** + * @return RecentChangesUpdateJob + */ + final public static function newPurgeJob() { + return new self( + SpecialPage::getTitleFor( 'Recentchanges' ), array( 'type' => 'purge' ) + ); + } + + public function run() { + if ( $this->params['type'] === 'purge' ) { + $this->purgeExpiredRows(); + } else { + throw new Exception( "Invalid 'type' parameter '{$this->params['type']}'." ); + } + + return true; + } + + protected function purgeExpiredRows() { + global $wgRCMaxAge; + + $dbw = wfGetDB( DB_MASTER ); + if ( !$dbw->lock( 'recentchanges-prune', __METHOD__, 1 ) ) { + return true; // already in progress + } + + $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); + do { + $rcIds = $dbw->selectFieldValues( 'recentchanges', + 'rc_id', + array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ), + __METHOD__, + array( 'LIMIT' => 100 ) // avoid slave lag + ); + if ( $rcIds ) { + $dbw->delete( 'recentchanges', array( 'rc_id' => $rcIds ), __METHOD__ ); + } + } while ( $rcIds ); + + $dbw->unlock( 'recentchanges-prune', __METHOD__ ); + } +} diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 8373dc01b1..dc84ea0258 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -2167,11 +2167,8 @@ class WikiPage implements Page, IDBAccessObject { Hooks::run( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { - if ( 0 == mt_rand( 0, 99 ) ) { - // Flush old entries from the `recentchanges` table; we do this on - // random requests so as to avoid an increase in writes for no good reason - RecentChange::purgeExpiredChanges(); - } + // Flush old entries from the `recentchanges` table + JobQueueGroup::singleton()->push( RecentChangesUpdateJob::newPurgeJob() ); } if ( !$this->exists() ) { -- 2.20.1