From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- .../jobqueue/aggregator/JobQueueAggregator.php | 162 +++++++++++++++ .../jobqueue/aggregator/JobQueueAggregatorMemc.php | 125 ++++++++++++ .../aggregator/JobQueueAggregatorRedis.php | 218 +++++++++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 includes/jobqueue/aggregator/JobQueueAggregator.php create mode 100644 includes/jobqueue/aggregator/JobQueueAggregatorMemc.php create mode 100644 includes/jobqueue/aggregator/JobQueueAggregatorRedis.php (limited to 'includes/jobqueue/aggregator') diff --git a/includes/jobqueue/aggregator/JobQueueAggregator.php b/includes/jobqueue/aggregator/JobQueueAggregator.php new file mode 100644 index 00000000..8600eed9 --- /dev/null +++ b/includes/jobqueue/aggregator/JobQueueAggregator.php @@ -0,0 +1,162 @@ +doNotifyQueueEmpty( $wiki, $type ); + wfProfileOut( __METHOD__ ); + + return $ok; + } + + /** + * @see JobQueueAggregator::notifyQueueEmpty() + */ + abstract protected function doNotifyQueueEmpty( $wiki, $type ); + + /** + * Mark a queue as being non-empty + * + * @param string $wiki + * @param string $type + * @return bool Success + */ + final public function notifyQueueNonEmpty( $wiki, $type ) { + wfProfileIn( __METHOD__ ); + $ok = $this->doNotifyQueueNonEmpty( $wiki, $type ); + wfProfileOut( __METHOD__ ); + + return $ok; + } + + /** + * @see JobQueueAggregator::notifyQueueNonEmpty() + */ + abstract protected function doNotifyQueueNonEmpty( $wiki, $type ); + + /** + * Get the list of all of the queues with jobs + * + * @return array (job type => (list of wiki IDs)) + */ + final public function getAllReadyWikiQueues() { + wfProfileIn( __METHOD__ ); + $res = $this->doGetAllReadyWikiQueues(); + wfProfileOut( __METHOD__ ); + + return $res; + } + + /** + * @see JobQueueAggregator::getAllReadyWikiQueues() + */ + abstract protected function doGetAllReadyWikiQueues(); + + /** + * Purge all of the aggregator information + * + * @return bool Success + */ + final public function purge() { + wfProfileIn( __METHOD__ ); + $res = $this->doPurge(); + wfProfileOut( __METHOD__ ); + + return $res; + } + + /** + * @see JobQueueAggregator::purge() + */ + abstract protected function doPurge(); + + /** + * Get all databases that have a pending job. + * This poll all the queues and is this expensive. + * + * @return array (job type => (list of wiki IDs)) + */ + protected function findPendingWikiQueues() { + global $wgLocalDatabases; + + $pendingDBs = array(); // (job type => (db list)) + foreach ( $wgLocalDatabases as $db ) { + foreach ( JobQueueGroup::singleton( $db )->getQueuesWithJobs() as $type ) { + $pendingDBs[$type][] = $db; + } + } + + return $pendingDBs; + } +} diff --git a/includes/jobqueue/aggregator/JobQueueAggregatorMemc.php b/includes/jobqueue/aggregator/JobQueueAggregatorMemc.php new file mode 100644 index 00000000..ae266ef3 --- /dev/null +++ b/includes/jobqueue/aggregator/JobQueueAggregatorMemc.php @@ -0,0 +1,125 @@ +cache = isset( $params['objectCache'] ) + ? wfGetCache( $params['objectCache'] ) + : wfGetMainCache(); + $this->cacheTTL = isset( $params['cacheTTL'] ) ? $params['cacheTTL'] : 180; // 3 min + } + + /** + * @see JobQueueAggregator::doNotifyQueueEmpty() + */ + protected function doNotifyQueueEmpty( $wiki, $type ) { + $key = $this->getReadyQueueCacheKey(); + // Delist the queue from the "ready queue" list + if ( $this->cache->add( "$key:lock", 1, 60 ) ) { // lock + $curInfo = $this->cache->get( $key ); + if ( is_array( $curInfo ) && isset( $curInfo['pendingDBs'][$type] ) ) { + if ( in_array( $wiki, $curInfo['pendingDBs'][$type] ) ) { + $curInfo['pendingDBs'][$type] = array_diff( + $curInfo['pendingDBs'][$type], array( $wiki ) ); + $this->cache->set( $key, $curInfo ); + } + } + $this->cache->delete( "$key:lock" ); // unlock + } + + return true; + } + + /** + * @see JobQueueAggregator::doNotifyQueueNonEmpty() + */ + protected function doNotifyQueueNonEmpty( $wiki, $type ) { + return true; // updated periodically + } + + /** + * @see JobQueueAggregator::doAllGetReadyWikiQueues() + */ + protected function doGetAllReadyWikiQueues() { + $key = $this->getReadyQueueCacheKey(); + // If the cache entry wasn't present, is stale, or in .1% of cases otherwise, + // regenerate the cache. Use any available stale cache if another process is + // currently regenerating the pending DB information. + $pendingDbInfo = $this->cache->get( $key ); + if ( !is_array( $pendingDbInfo ) + || ( time() - $pendingDbInfo['timestamp'] ) > $this->cacheTTL + || mt_rand( 0, 999 ) == 0 + ) { + if ( $this->cache->add( "$key:rebuild", 1, 1800 ) ) { // lock + $pendingDbInfo = array( + 'pendingDBs' => $this->findPendingWikiQueues(), + 'timestamp' => time() + ); + for ( $attempts = 1; $attempts <= 25; ++$attempts ) { + if ( $this->cache->add( "$key:lock", 1, 60 ) ) { // lock + $this->cache->set( $key, $pendingDbInfo ); + $this->cache->delete( "$key:lock" ); // unlock + break; + } + } + $this->cache->delete( "$key:rebuild" ); // unlock + } + } + + return is_array( $pendingDbInfo ) + ? $pendingDbInfo['pendingDBs'] + : array(); // cache is both empty and locked + } + + /** + * @see JobQueueAggregator::doPurge() + */ + protected function doPurge() { + return $this->cache->delete( $this->getReadyQueueCacheKey() ); + } + + /** + * @return string + */ + private function getReadyQueueCacheKey() { + return "jobqueue:aggregator:ready-queues:v1"; // global + } +} diff --git a/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php b/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php new file mode 100644 index 00000000..db9e764c --- /dev/null +++ b/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php @@ -0,0 +1,218 @@ +servers = isset( $params['redisServers'] ) + ? $params['redisServers'] + : array( $params['redisServer'] ); // b/c + $params['redisConfig']['serializer'] = 'none'; + $this->redisPool = RedisConnectionPool::singleton( $params['redisConfig'] ); + } + + protected function doNotifyQueueEmpty( $wiki, $type ) { + $conn = $this->getConnection(); + if ( !$conn ) { + return false; + } + try { + $conn->hDel( $this->getReadyQueueKey(), $this->encQueueName( $type, $wiki ) ); + + return true; + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + + return false; + } + } + + protected function doNotifyQueueNonEmpty( $wiki, $type ) { + $conn = $this->getConnection(); + if ( !$conn ) { + return false; + } + try { + $conn->multi( Redis::PIPELINE ); + $conn->hSetNx( $this->getQueueTypesKey(), $type, 'enabled' ); + $conn->hSet( $this->getReadyQueueKey(), $this->encQueueName( $type, $wiki ), time() ); + $conn->exec(); + + return true; + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + + return false; + } + } + + protected function doGetAllReadyWikiQueues() { + $conn = $this->getConnection(); + if ( !$conn ) { + return array(); + } + try { + $map = $conn->hGetAll( $this->getReadyQueueKey() ); + + if ( is_array( $map ) && isset( $map['_epoch'] ) ) { + unset( $map['_epoch'] ); // ignore + $pendingDBs = array(); // (type => list of wikis) + foreach ( $map as $key => $time ) { + list( $type, $wiki ) = $this->dencQueueName( $key ); + $pendingDBs[$type][] = $wiki; + } + } else { + // Avoid duplicated effort + $rand = wfRandomString( 32 ); + $conn->multi( Redis::MULTI ); + $conn->setex( "{$rand}:lock", 3600, 1 ); + $conn->renamenx( "{$rand}:lock", $this->getReadyQueueKey() . ":lock" ); + if ( $conn->exec() !== array( true, true ) ) { // lock + $conn->delete( "{$rand}:lock" ); + return array(); // already in progress + } + + $pendingDBs = $this->findPendingWikiQueues(); // (type => list of wikis) + + $conn->multi( Redis::PIPELINE ); + $now = time(); + $map = array( '_epoch' => time() ); // dummy key for empty Redis collections + foreach ( $pendingDBs as $type => $wikis ) { + $conn->hSetNx( $this->getQueueTypesKey(), $type, 'enabled' ); + foreach ( $wikis as $wiki ) { + $map[$this->encQueueName( $type, $wiki )] = $now; + } + } + $conn->hMSet( $this->getReadyQueueKey(), $map ); + $conn->exec(); + + $conn->delete( $this->getReadyQueueKey() . ":lock" ); // unlock + } + + return $pendingDBs; + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + + return array(); + } + } + + protected function doPurge() { + $conn = $this->getConnection(); + if ( !$conn ) { + return false; + } + try { + $conn->delete( $this->getReadyQueueKey() ); + // leave key at getQueueTypesKey() alone + } catch ( RedisException $e ) { + $this->handleException( $conn, $e ); + + return false; + } + + return true; + } + + /** + * Get a connection to the server that handles all sub-queues for this queue + * + * @return RedisConnRef|bool Returns false on failure + * @throws MWException + */ + protected function getConnection() { + $conn = false; + foreach ( $this->servers as $server ) { + $conn = $this->redisPool->getConnection( $server ); + if ( $conn ) { + break; + } + } + + return $conn; + } + + /** + * @param RedisConnRef $conn + * @param RedisException $e + * @return void + */ + protected function handleException( RedisConnRef $conn, $e ) { + $this->redisPool->handleError( $conn, $e ); + } + + /** + * @return string + */ + private function getReadyQueueKey() { + return "jobqueue:aggregator:h-ready-queues:v2"; // global + } + + /** + * @return string + */ + private function getQueueTypesKey() { + return "jobqueue:aggregator:h-queue-types:v2"; // global + } + + /** + * @param string $type + * @param string $wiki + * @return string + */ + private function encQueueName( $type, $wiki ) { + return rawurlencode( $type ) . '/' . rawurlencode( $wiki ); + } + + /** + * @param string $name + * @return string + */ + private function dencQueueName( $name ) { + list( $type, $wiki ) = explode( '/', $name, 2 ); + + return array( rawurldecode( $type ), rawurldecode( $wiki ) ); + } +} -- cgit v1.2.3-54-g00ecf