case RC_NEW:
$type = 'new';
break;
- case RC_MOVE:
+ case RC_MOVE: // obsolete
$type = 'move';
break;
case RC_LOG:
case RC_EXTERNAL:
$type = 'external';
break;
- case RC_MOVE_OVER_REDIRECT:
+ case RC_MOVE_OVER_REDIRECT: // obsolete
$type = 'move over redirect';
break;
default:
// 1.24
array( 'addField', 'page_props', 'pp_sortkey', 'patch-pp_sortkey.sql' ),
array( 'dropField', 'recentchanges', 'rc_cur_time', 'patch-drop-rc_cur_time.sql' ),
+ array( 'addIndex', 'watchlist', 'wl_user_notificationtimestamp', 'patch-watchlist-user-notificationtimestamp-index.sql' ),
);
}
array( 'addPgIndex', 'recentchanges', 'rc_timestamp_bot', '(rc_timestamp) WHERE rc_bot = 0' ),
array( 'addPgIndex', 'templatelinks', 'templatelinks_from', '(tl_from)' ),
array( 'addPgIndex', 'watchlist', 'wl_user', '(wl_user)' ),
+ array( 'addPgIndex', 'watchlist', 'wl_user_notificationtimestamp', '(wl_user, wl_notificationtimestamp)' ),
array( 'addPgIndex', 'logging', 'logging_user_type_time',
'(log_user, log_type, log_timestamp)' ),
array( 'addPgIndex', 'logging', 'logging_page_id_time', '(log_page,log_timestamp)' ),
// 1.24
array( 'addField', 'page_props', 'pp_sortkey', 'patch-pp_sortkey.sql' ),
array( 'dropField', 'recentchanges', 'rc_cur_time', 'patch-drop-rc_cur_time.sql' ),
+ array( 'addIndex', 'watchlist', 'wl_user_notificationtimestamp', 'patch-watchlist-user-notificationtimestamp-index.sql' ),
);
}
$this->mProperties[$name] = $value;
}
+ /**
+ * @param string $name The property name to look up.
+ *
+ * @return mixed|false The value previously set using setProperty(). False if null or no value
+ * was set for the given property name.
+ *
+ * @note You need to use getProperties() to check for boolean and null properties.
+ */
public function getProperty( $name ) {
return isset( $this->mProperties[$name] ) ? $this->mProperties[$name] : false;
}
*
* @param string $key The key to look up.
*
- * @return mixed The value previously set for the given key using setExtensionData( $key ),
+ * @return mixed|null The value previously set for the given key using setExtensionData()
* or null if no value was set for this key.
*/
public function getExtensionData( $key ) {
/** @var string All workers with the same key share the lock */
protected $key;
- /** @var int Maximum number of workers doing the task simultaneously */
+ /** @var int Maximum number of workers working on tasks with the same key simultaneously */
protected $workers;
+ /**
+ * Maximum number of workers working on this task type, regardless of key.
+ * 0 means unlimited. Max allowed value is 65536.
+ * The way the slot limit is enforced is overzealous - this option should be used with caution.
+ * @var int
+ */
+ protected $slots = 0;
/** @var int If this number of workers are already working/waiting, fail instead of wait */
protected $maxqueue;
/** @var float Maximum time in seconds to wait for the lock */
* @param string $key
*/
protected function __construct( $conf, $type, $key ) {
- $this->key = $key;
$this->workers = $conf['workers'];
$this->maxqueue = $conf['maxqueue'];
$this->timeout = $conf['timeout'];
+ if ( isset( $conf['slots'] ) ) {
+ $this->slots = $conf['slots'];
+ }
+
+ if ( $this->slots ) {
+ $key = $this->hashKeyIntoSlots( $key, $this->slots );
+ }
+ $this->key = $key;
}
/**
* @return Status value is one of Released/NotLocked/Error
*/
abstract public function release();
+
+ /**
+ * Given a key (any string) and the number of lots, returns a slot number (an integer from the [0..($slots-1)] range).
+ * This is used for a global limit on the number of instances of a given type that can acquire a lock.
+ * The hashing is deterministic so that PoolCounter::$workers is always an upper limit of how many instances with
+ * the same key can acquire a lock.
+ *
+ * @param string $key PoolCounter instance key (any string)
+ * @param int $slots the number of slots (max allowed value is 65536)
+ * @return int
+ */
+ protected function hashKeyIntoSlots( $key, $slots ) {
+ return hexdec( substr( sha1( $key ), 0, 4 ) ) % $slots;
+ }
}
// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps
// but there is no real reason not to expose it in other cases,
// and I can see how this may be potentially useful for clients.
'id' => $attrib['rc_id'],
- 'type' => $attrib['rc_type'],
+ 'type' => RecentChange::parseFromRCType( $attrib['rc_type'] ),
'namespace' => $rc->getTitle()->getNamespace(),
'title' => $rc->getTitle()->getPrefixedText(),
'comment' => $attrib['rc_comment'],
return $this->getContext()->getLanguage();
}
+ /**
+ * Shortcut to get main config object
+ * @return Config
+ * @since 1.24
+ */
+ public function getConfig() {
+ return $this->getContext()->getConfig();
+ }
+
/**
* Return the full title, including $par
*
'ymd',
'hijri',
'ISO 8601',
+ 'jMY',
);
/**
--- /dev/null
+--
+-- Creates the wl_user_notificationtimestamp index for the watchlist table
+--
+CREATE INDEX /*i*/wl_user_notificationtimestamp ON /*_*/watchlist (wl_user, wl_notificationtimestamp);
);
CREATE UNIQUE INDEX wl_user_namespace_title ON watchlist (wl_namespace, wl_title, wl_user);
CREATE INDEX wl_user ON watchlist (wl_user);
+CREATE INDEX wl_user_notificationtimestamp ON watchlist (wl_user, wl_notificationtimestamp);
CREATE TABLE interwiki (
CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title);
CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title);
+CREATE INDEX /*i*/wl_user_notificationtimestamp ON /*_*/watchlist (wl_user, wl_notificationtimestamp);
--
/*jshint node:true */
module.exports = function ( grunt ) {
grunt.loadNpmTasks( 'grunt-contrib-jshint' );
- grunt.loadNpmTasks( 'grunt-banana-checker' );
grunt.loadNpmTasks( 'grunt-contrib-watch' );
+ grunt.loadNpmTasks( 'grunt-banana-checker' );
grunt.loadNpmTasks( 'grunt-jscs-checker' );
grunt.loadNpmTasks( 'grunt-jsonlint' );
pkg: grunt.file.readJSON( __dirname + '/package.json' ),
jshint: {
options: {
- jshintrc: '.jshintrc'
+ jshintrc: true
},
- all: [ '*.js', '{includes,languages,resources,skins,tests}/**/*.js' ]
+ all: [
+ '*.js',
+ '{includes,languages,resources,skins,tests}/**/*.js'
+ ]
},
jscs: {
- // Known issues:
- // - https://github.com/mdevils/node-jscs/issues/277
- // - https://github.com/mdevils/node-jscs/issues/278
all: [
'<%= jshint.all %>',
// Auto-generated file with JSON (double quotes)
'.jshintignore',
'.jshintrc'
],
- tasks: ['test']
+ tasks: 'test'
}
} );
--- /dev/null
+<?php
+
+// We will use this class with getMockForAbstractClass to create a concrete mock class. That call will die if the
+// contructor is not public, unless we use disableOriginalConstructor(), in which case we could not test the constructor.
+abstract class PoolCounterAbstractMock extends PoolCounter {
+ public function __construct() {
+ call_user_func_array( 'parent::__construct', func_get_args() );
+ }
+}
+
+class PoolCounterTest extends MediaWikiTestCase {
+ public function testConstruct() {
+ $poolCounterConfig = array(
+ 'class' => 'PoolCounterMock',
+ 'timeout' => 10,
+ 'workers' => 10,
+ 'maxqueue' => 100,
+ );
+
+ $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' )
+ ->setConstructorArgs( array( $poolCounterConfig, 'testCounter', 'someKey' ) )
+ // don't mock anything - the proper syntax would be setMethods(null), but due to a PHPUnit bug that
+ // does not work with getMockForAbstractClass()
+ ->setMethods( array( 'idontexist' ) )
+ ->getMockForAbstractClass();
+ $this->assertInstanceOf( 'PoolCounter', $poolCounter );
+ }
+
+ public function testConstructWithSlots() {
+ $poolCounterConfig = array(
+ 'class' => 'PoolCounterMock',
+ 'timeout' => 10,
+ 'workers' => 10,
+ 'slots' => 2,
+ 'maxqueue' => 100,
+ );
+
+ $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' )
+ ->setConstructorArgs( array( $poolCounterConfig, 'testCounter', 'key' ) )
+ ->setMethods( array( 'idontexist' ) ) // don't mock anything
+ ->getMockForAbstractClass();
+ $this->assertInstanceOf( 'PoolCounter', $poolCounter );
+ }
+
+ public function testHashKeyIntoSlots() {
+ $poolCounter = $this->getMockBuilder( 'PoolCounterAbstractMock' )
+ // don't mock anything - the proper syntax would be setMethods(null), but due to a PHPUnit bug that
+ // does not work with getMockForAbstractClass()
+ ->setMethods( array( 'idontexist' ) )
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $hashKeyIntoSlots = new ReflectionMethod($poolCounter, 'hashKeyIntoSlots' );
+ $hashKeyIntoSlots->setAccessible( true );
+
+
+ $keysWithTwoSlots = $keysWithFiveSlots = array();
+ foreach ( range( 1, 100 ) as $i ) {
+ $keysWithTwoSlots[] = $hashKeyIntoSlots->invoke( $poolCounter, 'key ' . $i, 2 );
+ $keysWithFiveSlots[] = $hashKeyIntoSlots->invoke( $poolCounter, 'key ' . $i, 5 );
+ }
+
+ $this->assertArrayEquals( range( 0, 1 ), array_unique( $keysWithTwoSlots ) );
+ $this->assertArrayEquals( range( 0, 4 ), array_unique( $keysWithFiveSlots ) );
+
+ // make sure it is deterministic
+ $this->assertEquals(
+ $hashKeyIntoSlots->invoke( $poolCounter, 'asdfgh', 1000 ),
+ $hashKeyIntoSlots->invoke( $poolCounter, 'asdfgh', 1000 )
+ );
+ }
+}