* given, giving a callable function which will generate a suitable cache object.
*/
$wgObjectCaches = [
- CACHE_NONE => [ 'class' => 'EmptyBagOStuff' ],
+ CACHE_NONE => [ 'class' => 'EmptyBagOStuff', 'reportDupes' => false ],
CACHE_DB => [ 'class' => 'SqlBagOStuff', 'loggroup' => 'SQLBagOStuff' ],
CACHE_ANYTHING => [ 'factory' => 'ObjectCache::newAnything' ],
'loggroup' => 'SQLBagOStuff'
],
- 'apc' => [ 'class' => 'APCBagOStuff' ],
- 'xcache' => [ 'class' => 'XCacheBagOStuff' ],
- 'wincache' => [ 'class' => 'WinCacheBagOStuff' ],
+ 'apc' => [ 'class' => 'APCBagOStuff', 'reportDupes' => false ],
+ 'xcache' => [ 'class' => 'XCacheBagOStuff', 'reportDupes' => false ],
+ 'wincache' => [ 'class' => 'WinCacheBagOStuff', 'reportDupes' => false ],
'memcached-php' => [ 'class' => 'MemcachedPhpBagOStuff', 'loggroup' => 'memcached' ],
'memcached-pecl' => [ 'class' => 'MemcachedPeclBagOStuff', 'loggroup' => 'memcached' ],
- 'hash' => [ 'class' => 'HashBagOStuff' ],
+ 'hash' => [ 'class' => 'HashBagOStuff', 'reportDupes' => false ],
];
/**
/** @var LoggerInterface */
protected $logger;
+ /** @var callback|null */
+ protected $asyncHandler;
+
/** @var bool */
private $debugMode = false;
+ /** @var array */
+ private $duplicateKeyLookups = [];
+
+ /** @var bool */
+ private $reportDupes = false;
+
+ /** @var bool */
+ private $dupeTrackScheduled = false;
+
/** Possible values for getLastError() */
const ERR_NONE = 0; // no error
const ERR_NO_RESPONSE = 1; // no response
const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
+ /**
+ * $params include:
+ * - logger: Psr\Log\LoggerInterface instance
+ * - keyspace: Default keyspace for $this->makeKey()
+ * - asyncHandler: Callable to use for scheduling tasks after the web request ends.
+ * In CLI mode, it should run the task immediately.
+ * - reportDupes: Whether to emit warning log messages for all keys that were
+ * requested more than once (requires an asyncHandler).
+ * @param array $params
+ */
public function __construct( array $params = [] ) {
if ( isset( $params['logger'] ) ) {
$this->setLogger( $params['logger'] );
if ( isset( $params['keyspace'] ) ) {
$this->keyspace = $params['keyspace'];
}
+
+ $this->asyncHandler = isset( $params['asyncHandler'] )
+ ? $params['asyncHandler']
+ : null;
+
+ if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
+ $this->reportDupes = true;
+ }
}
/**
// B/C for ( $key, &$casToken = null, $flags = 0 )
$flags = is_int( $oldFlags ) ? $oldFlags : $flags;
+ $this->trackDuplicateKeys( $key );
+
return $this->doGet( $key, $flags );
}
+ /**
+ * Track the number of times that a given key has been used.
+ * @param string $key
+ */
+ private function trackDuplicateKeys( $key ) {
+ if ( !$this->reportDupes ) {
+ return;
+ }
+
+ if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
+ // Track that we have seen this key. This N-1 counting style allows
+ // easy filtering with array_filter() later.
+ $this->duplicateKeyLookups[$key] = 0;
+ } else {
+ $this->duplicateKeyLookups[$key] += 1;
+
+ if ( $this->dupeTrackScheduled === false ) {
+ $this->dupeTrackScheduled = true;
+ // Schedule a callback that logs keys processed more than once by get().
+ call_user_func( $this->asyncHandler, function () {
+ $dups = array_filter( $this->duplicateKeyLookups );
+ foreach ( $dups as $key => $count ) {
+ $this->logger->warning(
+ 'Duplicate get(): "{key}" fetched {count} times',
+ // Count is N-1 of the actual lookup count
+ [ 'key' => $key, 'count' => $count + 1, ]
+ );
+ }
+ } );
+ }
+ }
+ }
+
/**
* @param string $key
* @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
protected $caches;
/** @var bool Use async secondary writes */
protected $asyncWrites = false;
- /** @var callback|null */
- protected $asyncHandler;
/** Idiom for "write to all backends" */
const ALL = INF;
* safe to use for modules when cached values: are immutable,
* invalidation uses logical TTLs, invalidation uses etag/timestamp
* validation against the DB, or merge() is used to handle races.
- * - asyncHandler: callable that takes a callback and runs it after the
- * current web request ends. In CLI mode, it should run it immediately.
* @param array $params
* @throws InvalidArgumentException
*/
}
}
- $this->asyncHandler = isset( $params['asyncHandler'] )
- ? $params['asyncHandler']
- : null;
$this->asyncWrites = (
isset( $params['replication'] ) &&
$params['replication'] === 'async' &&
$params[3] = $this->msg( 'rightsnone' )->text();
}
if ( count( $newGroups ) ) {
- // Array_values is used here because of bug 42211
+ // Array_values is used here because of T44211
// see use of array_unique in UserrightsPage::doSaveUserGroups on $newGroups.
$params[4] = $lang->listToText( array_values( $newGroups ) );
} else {
$params[4] = $this->msg( 'rightsnone' )->text();
}
+ $params[5] = $userName;
+
return $params;
}
} elseif ( isset( $params['class'] ) ) {
$class = $params['class'];
// Automatically set the 'async' update handler
- if ( $class === 'MultiWriteBagOStuff' ) {
- $params['asyncHandler'] = isset( $params['asyncHandler'] )
- ? $params['asyncHandler']
- : 'DeferredUpdates::addCallableUpdate';
- }
+ $params['asyncHandler'] = isset( $params['asyncHandler'] )
+ ? $params['asyncHandler']
+ : 'DeferredUpdates::addCallableUpdate';
+ // Enable reportDupes by default
+ $params['reportDupes'] = isset( $params['reportDupes'] )
+ ? $params['reportDupes']
+ : true;
// Do b/c logic for MemcachedBagOStuff
if ( is_subclass_of( $class, 'MemcachedBagOStuff' ) ) {
if ( !isset( $params['servers'] ) ) {
'bbc-latn' => 'Batak Toba', # Batak Toba
'bcc' => 'جهلسری بلوچی', # Southern Balochi
'bcl' => 'Bikol Central', # Bikol: Central Bicolano language
- 'be' => 'беларуская', # Belarusian normative
+ 'be' => 'беларуская', # Belarusian normative
'be-tarask' => "беларуская (тарашкевіца)\xE2\x80\x8E", # Belarusian in Taraskievica orthography
'be-x-old' => "беларуская (тарашкевіца)\xE2\x80\x8E", # (be-tarask compat)
'bg' => 'български', # Bulgarian
'so' => 'Soomaaliga', # Somali
'sq' => 'shqip', # Albanian
'sr' => 'српски / srpski', # Serbian (multiple scripts - defaults to Cyrillic)
- 'sr-ec' => "српски (ћирилица)\xE2\x80\x8E", # Serbian Cyrillic ekavian
- 'sr-el' => "srpski (latinica)\xE2\x80\x8E", # Serbian Latin ekavian
+ 'sr-ec' => "српски (ћирилица)\xE2\x80\x8E", # Serbian Cyrillic ekavian
+ 'sr-el' => "srpski (latinica)\xE2\x80\x8E", # Serbian Latin ekavian
'srn' => 'Sranantongo', # Sranan Tongo
'ss' => 'SiSwati', # Swati
'st' => 'Sesotho', # Southern Sotho
'ug-latn' => 'Uyghurche', # Uyghur (Latin script)
'uk' => 'українська', # Ukrainian
'ur' => 'اردو', # Urdu
- 'uz' => 'oʻzbekcha/ўзбекча', # Uzbek (multiple scripts - defaults to Latin)
- 'uz-cyrl' => 'ўзбекча', # Uzbek Cyrillic
- 'uz-latn' => 'oʻzbekcha', # Uzbek Latin (default)
+ 'uz' => 'oʻzbekcha/ўзбекча', # Uzbek (multiple scripts - defaults to Latin)
+ 'uz-cyrl' => 'ўзбекча', # Uzbek Cyrillic
+ 'uz-latn' => 'oʻzbekcha', # Uzbek Latin (default)
've' => 'Tshivenda', # Venda
'vec' => 'vèneto', # Venetian
'vep' => 'vepsän kel’', # Veps
"logentry-protect-protect-cascade": "$1 {{GENDER:$2|protected}} $3 $4 [cascading]",
"logentry-protect-modify": "$1 {{GENDER:$2|changed}} protection level for $3 $4",
"logentry-protect-modify-cascade": "$1 {{GENDER:$2|changed}} protection level for $3 $4 [cascading]",
- "logentry-rights-rights": "$1 {{GENDER:$2|changed}} group membership for {{GENDER:$3|$3}} from $4 to $5",
+ "logentry-rights-rights": "$1 {{GENDER:$2|changed}} group membership for {{GENDER:$6|$3}} from $4 to $5",
"logentry-rights-rights-legacy": "$1 {{GENDER:$2|changed}} group membership for $3",
"logentry-rights-autopromote": "$1 was automatically {{GENDER:$2|promoted}} from $4 to $5",
"logentry-upload-upload": "$1 {{GENDER:$2|uploaded}} $3",
"logentry-protect-protect-cascade": "{{Logentry|[[Special:Log/protect]]}}\n\n* $4 - protect expiry (formatted with {{msg-mw|protect-summary-desc}}, multiple possible)\nFor word \"cascading\" see {{msg-mw|protect-summary-cascade}}",
"logentry-protect-modify": "{{Logentry|[[Special:Log/protect]]}}\n\n* $4 - protect expiry (formatted with {{msg-mw|protect-summary-desc}}, multiple possible)",
"logentry-protect-modify-cascade": "{{Logentry|[[Special:Log/protect]]}}\n\n* $4 - protect expiry (formatted with {{msg-mw|protect-summary-desc}}, multiple possible)\nFor word \"cascading\" see {{msg-mw|protect-summary-cascade}}",
- "logentry-rights-rights": "* $1 - username\n* $2 - (see below)\n* $3 - username, also used for GENDER support\n* $4 - list of user groups or {{msg-mw|Rightsnone}}\n* $5 - list of user groups or {{msg-mw|Rightsnone}}\n----\n{{Logentry|[[Special:Log/rights]]}}",
+ "logentry-rights-rights": "* $1 - (see below)\n* $2 - (see below)\n* $3 - target user, like $1\n* $4 - list of user groups or {{msg-mw|Rightsnone}}\n* $5 - list of user groups or {{msg-mw|Rightsnone}}\n* $6 - target user, can be used with GENDER\n----\n{{Logentry|[[Special:Log/rights]]}}",
"logentry-rights-rights-legacy": "* $1 - username\n* $2 - (see below)\n* $3 - username\n----\n{{Logentry|[[Special:Log/rights]]}}",
"logentry-rights-autopromote": "* $1 - username\n* $2 - (see below)\n* $3 - (see below)\n* $4 - comma separated list of old user groups or {{msg-mw|Rightsnone}}\n* $5 - comma separated list of new user groups\n----\n{{Logentry|[[Special:Log/rights]]}}",
"logentry-upload-upload": "{{Logentry|[[Special:Log/upload]]}}",
$this->assertType( 'ScopedCallback', $value1, 'First reentrant call returned lock' );
$this->assertType( 'ScopedCallback', $value1, 'Second reentrant call returned lock' );
}
+
+ /**
+ * @covers BagOStuff::__construct
+ * @covers BagOStuff::trackDuplicateKeys
+ */
+ public function testReportDupes() {
+ $logger = $this->getMock( 'Psr\Log\NullLogger' );
+ $logger->expects( $this->once() )
+ ->method( 'warning' )
+ ->with( 'Duplicate get(): "{key}" fetched {count} times', [
+ 'key' => 'foo',
+ 'count' => 2,
+ ] );
+
+ $cache = new HashBagOStuff( [
+ 'reportDupes' => true,
+ 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
+ 'logger' => $logger,
+ ] );
+ $cache->get( 'foo' );
+ $cache->get( 'bar' );
+ $cache->get( 'foo' );
+
+ DeferredUpdates::doUpdates();
+ }
}