Move WAN cache classes under a new wancache/ directory
authorAaron Schulz <aschulz@wikimedia.org>
Fri, 2 Aug 2019 03:06:27 +0000 (23:06 -0400)
committerAaron Schulz <aschulz@wikimedia.org>
Fri, 2 Aug 2019 18:46:23 +0000 (14:46 -0400)
Change-Id: I6837761ebca7557e029e1f65beca738266e48efb

autoload.php
includes/libs/objectcache/WANObjectCacheReaper.php [deleted file]
includes/libs/objectcache/WinCacheBagOStuff.php [deleted file]
includes/libs/objectcache/wancache/WANObjectCacheReaper.php [new file with mode: 0644]
includes/libs/objectcache/wancache/WinCacheBagOStuff.php [new file with mode: 0644]

index 43e2646..65b4e23 100644 (file)
@@ -1594,7 +1594,7 @@ $wgAutoloadLocalClasses = [
        'VirtualRESTServiceClient' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTServiceClient.php',
        'WANCacheReapUpdate' => __DIR__ . '/includes/deferred/WANCacheReapUpdate.php',
        'WANObjectCache' => __DIR__ . '/includes/libs/objectcache/WANObjectCache.php',
-       'WANObjectCacheReaper' => __DIR__ . '/includes/libs/objectcache/WANObjectCacheReaper.php',
+       'WANObjectCacheReaper' => __DIR__ . '/includes/libs/objectcache/wancache/WANObjectCacheReaper.php',
        'WantedCategoriesPage' => __DIR__ . '/includes/specials/SpecialWantedcategories.php',
        'WantedFilesPage' => __DIR__ . '/includes/specials/SpecialWantedfiles.php',
        'WantedPagesPage' => __DIR__ . '/includes/specials/SpecialWantedpages.php',
@@ -1706,7 +1706,7 @@ $wgAutoloadLocalClasses = [
        'WikitextContent' => __DIR__ . '/includes/content/WikitextContent.php',
        'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php',
        'WikitextLogFormatter' => __DIR__ . '/includes/logging/WikitextLogFormatter.php',
-       'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/WinCacheBagOStuff.php',
+       'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/wancache/WinCacheBagOStuff.php',
        'WithoutInterwikiPage' => __DIR__ . '/includes/specials/SpecialWithoutinterwiki.php',
        'WordLevelDiff' => __DIR__ . '/includes/diff/WordLevelDiff.php',
        'WrapOldPasswords' => __DIR__ . '/maintenance/wrapOldPasswords.php',
diff --git a/includes/libs/objectcache/WANObjectCacheReaper.php b/includes/libs/objectcache/WANObjectCacheReaper.php
deleted file mode 100644 (file)
index fb8a754..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Cache
- */
-
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use Wikimedia\ScopedCallback;
-
-/**
- * Class for scanning through chronological, log-structured data or change logs
- * and locally purging cache keys related to entities that appear in this data.
- *
- * This is useful for repairing cache when purges are missed by using a reliable
- * stream, such as Kafka or a replicated MySQL table. Purge loss between datacenters
- * is expected to be more common than within them.
- *
- * @since 1.28
- */
-class WANObjectCacheReaper implements LoggerAwareInterface {
-       /** @var WANObjectCache */
-       protected $cache;
-       /** @var BagOStuff */
-       protected $store;
-       /** @var callable */
-       protected $logChunkCallback;
-       /** @var callable */
-       protected $keyListCallback;
-       /** @var LoggerInterface */
-       protected $logger;
-
-       /** @var string */
-       protected $channel;
-       /** @var int */
-       protected $initialStartWindow;
-
-       /**
-        * @param WANObjectCache $cache Cache to reap bad keys from
-        * @param BagOStuff $store Cache to store positions use for locking
-        * @param callable $logCallback Callback taking arguments:
-        *          - The starting position as a UNIX timestamp
-        *          - The starting unique ID used for breaking timestamp collisions or null
-        *          - The ending position as a UNIX timestamp
-        *          - The maximum number of results to return
-        *        It returns a list of maps of (key: cache key, pos: UNIX timestamp, id: unique ID)
-        *        for each key affected, with the corrosponding event timestamp/ID information.
-        *        The events should be in ascending order, by (timestamp,id).
-        * @param callable $keyCallback Callback taking arguments:
-        *          - The WANObjectCache instance
-        *          - An object from the event log
-        *        It should return a list of WAN cache keys.
-        *        The callback must fully duck-type test the object, since can be any model class.
-        * @param array $params Additional options:
-        *          - channel: the name of the update event stream.
-        *          - initialStartWindow: seconds back in time to start if the position is lost.
-        *            Default: 1 hour.
-        *          - logger: an SPL monolog instance [optional]
-        */
-       public function __construct(
-               WANObjectCache $cache,
-               BagOStuff $store,
-               callable $logCallback,
-               callable $keyCallback,
-               array $params
-       ) {
-               $this->cache = $cache;
-               $this->store = $store;
-
-               $this->logChunkCallback = $logCallback;
-               $this->keyListCallback = $keyCallback;
-               if ( isset( $params['channel'] ) ) {
-                       $this->channel = $params['channel'];
-               } else {
-                       throw new UnexpectedValueException( "No channel specified." );
-               }
-
-               $this->initialStartWindow = $params['initialStartWindow'] ?? 3600;
-               $this->logger = $params['logger'] ?? new NullLogger();
-       }
-
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * Check and reap stale keys based on a chunk of events
-        *
-        * @param int $n Number of events
-        * @return int Number of keys checked
-        */
-       final public function invoke( $n = 100 ) {
-               $posKey = $this->store->makeGlobalKey( 'WANCache', 'reaper', $this->channel );
-               $scopeLock = $this->store->getScopedLock( "$posKey:busy", 0 );
-               if ( !$scopeLock ) {
-                       return 0;
-               }
-
-               $now = time();
-               $status = $this->store->get( $posKey );
-               if ( !$status ) {
-                       $status = [ 'pos' => $now - $this->initialStartWindow, 'id' => null ];
-               }
-
-               // Get events for entities who's keys tombstones/hold-off should have expired by now
-               $events = call_user_func_array(
-                       $this->logChunkCallback,
-                       [ $status['pos'], $status['id'], $now - WANObjectCache::HOLDOFF_TTL - 1, $n ]
-               );
-
-               $event = null;
-               $keyEvents = [];
-               foreach ( $events as $event ) {
-                       $keys = call_user_func_array(
-                               $this->keyListCallback,
-                               [ $this->cache, $event['item'] ]
-                       );
-                       foreach ( $keys as $key ) {
-                               unset( $keyEvents[$key] ); // use only the latest per key
-                               $keyEvents[$key] = [
-                                       'pos' => $event['pos'],
-                                       'id' => $event['id']
-                               ];
-                       }
-               }
-
-               $purgeCount = 0;
-               $lastOkEvent = null;
-               foreach ( $keyEvents as $key => $keyEvent ) {
-                       if ( !$this->cache->reap( $key, $keyEvent['pos'] ) ) {
-                               break;
-                       }
-                       ++$purgeCount;
-                       $lastOkEvent = $event;
-               }
-
-               if ( $lastOkEvent ) {
-                       $ok = $this->store->merge(
-                               $posKey,
-                               function ( $bag, $key, $curValue ) use ( $lastOkEvent ) {
-                                       if ( !$curValue ) {
-                                               // Use new position
-                                       } else {
-                                               $curCoord = [ $curValue['pos'], $curValue['id'] ];
-                                               $newCoord = [ $lastOkEvent['pos'], $lastOkEvent['id'] ];
-                                               if ( $newCoord < $curCoord ) {
-                                                       // Keep prior position instead of rolling it back
-                                                       return $curValue;
-                                               }
-                                       }
-
-                                       return [
-                                               'pos' => $lastOkEvent['pos'],
-                                               'id' => $lastOkEvent['id'],
-                                               'ctime' => $curValue ? $curValue['ctime'] : date( 'c' )
-                                       ];
-                               },
-                               IExpiringStore::TTL_INDEFINITE
-                       );
-
-                       $pos = $lastOkEvent['pos'];
-                       $id = $lastOkEvent['id'];
-                       if ( $ok ) {
-                               $this->logger->info( "Updated cache reap position ($pos, $id)." );
-                       } else {
-                               $this->logger->error( "Could not update cache reap position ($pos, $id)." );
-                       }
-               }
-
-               ScopedCallback::consume( $scopeLock );
-
-               return $purgeCount;
-       }
-
-       /**
-        * @return array|bool Returns (pos, id) map or false if not set
-        */
-       public function getState() {
-               $posKey = $this->store->makeGlobalKey( 'WANCache', 'reaper', $this->channel );
-
-               return $this->store->get( $posKey );
-       }
-}
diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php
deleted file mode 100644 (file)
index 0e4e3fb..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-<?php
-/**
- * Object caching using WinCache.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Cache
- */
-
-/**
- * Wrapper for WinCache object caching functions; identical interface
- * to the APC wrapper
- *
- * @ingroup Cache
- */
-class WinCacheBagOStuff extends MediumSpecificBagOStuff {
-       protected function doGet( $key, $flags = 0, &$casToken = null ) {
-               $casToken = null;
-
-               $blob = wincache_ucache_get( $key );
-               if ( !is_string( $blob ) && !is_int( $blob ) ) {
-                       return false;
-               }
-
-               $value = $this->unserialize( $blob );
-               if ( $value !== false ) {
-                       $casToken = (string)$blob; // don't bother hashing this
-               }
-
-               return $value;
-       }
-
-       protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
-               if ( !wincache_lock( $key ) ) { // optimize with FIFO lock
-                       return false;
-               }
-
-               $curCasToken = null; // passed by reference
-               $this->doGet( $key, self::READ_LATEST, $curCasToken );
-               if ( $casToken === $curCasToken ) {
-                       $success = $this->set( $key, $value, $exptime, $flags );
-               } else {
-                       $this->logger->info(
-                               __METHOD__ . ' failed due to race condition for {key}.',
-                               [ 'key' => $key ]
-                       );
-
-                       $success = false; // mismatched or failed
-               }
-
-               wincache_unlock( $key );
-
-               return $success;
-       }
-
-       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
-               $result = wincache_ucache_set( $key, $this->serialize( $value ), $exptime );
-
-               // false positive, wincache_ucache_set returns an empty array
-               // in some circumstances.
-               // @phan-suppress-next-line PhanTypeComparisonToArray
-               return ( $result === [] || $result === true );
-       }
-
-       public function add( $key, $value, $exptime = 0, $flags = 0 ) {
-               if ( wincache_ucache_exists( $key ) ) {
-                       return false; // avoid warnings
-               }
-
-               $result = wincache_ucache_add( $key, $this->serialize( $value ), $exptime );
-
-               // false positive, wincache_ucache_add returns an empty array
-               // in some circumstances.
-               // @phan-suppress-next-line PhanTypeComparisonToArray
-               return ( $result === [] || $result === true );
-       }
-
-       protected function doDelete( $key, $flags = 0 ) {
-               wincache_ucache_delete( $key );
-
-               return true;
-       }
-
-       /**
-        * Construct a cache key.
-        *
-        * @since 1.27
-        * @param string $keyspace
-        * @param array $args
-        * @return string
-        */
-       public function makeKeyInternal( $keyspace, $args ) {
-               // WinCache keys have a maximum length of 150 characters. From that,
-               // subtract the number of characters we need for the keyspace and for
-               // the separator character needed for each argument. To handle some
-               // custom prefixes used by thing like WANObjectCache, limit to 125.
-               // NOTE: Same as in memcached, except the max key length there is 255.
-               $charsLeft = 125 - strlen( $keyspace ) - count( $args );
-
-               $args = array_map(
-                       function ( $arg ) use ( &$charsLeft ) {
-                               // 33 = 32 characters for the MD5 + 1 for the '#' prefix.
-                               if ( $charsLeft > 33 && strlen( $arg ) > $charsLeft ) {
-                                       $arg = '#' . md5( $arg );
-                               }
-
-                               $charsLeft -= strlen( $arg );
-                               return $arg;
-                       },
-                       $args
-               );
-
-               if ( $charsLeft < 0 ) {
-                       return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) );
-               }
-
-               return $keyspace . ':' . implode( ':', $args );
-       }
-
-       /**
-        * Increase stored value of $key by $value while preserving its original TTL
-        * @param string $key Key to increase
-        * @param int $value Value to add to $key (Default 1)
-        * @return int|bool New value or false on failure
-        */
-       public function incr( $key, $value = 1 ) {
-               if ( !wincache_lock( $key ) ) { // optimize with FIFO lock
-                       return false;
-               }
-
-               $n = $this->doGet( $key );
-               if ( $this->isInteger( $n ) ) {
-                       $n = max( $n + (int)$value, 0 );
-                       $oldTTL = wincache_ucache_info( false, $key )["ucache_entries"][1]["ttl_seconds"];
-                       $this->set( $key, $n, $oldTTL );
-               } else {
-                       $n = false;
-               }
-
-               wincache_unlock( $key );
-
-               return $n;
-       }
-}
diff --git a/includes/libs/objectcache/wancache/WANObjectCacheReaper.php b/includes/libs/objectcache/wancache/WANObjectCacheReaper.php
new file mode 100644 (file)
index 0000000..fb8a754
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Cache
+ */
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\ScopedCallback;
+
+/**
+ * Class for scanning through chronological, log-structured data or change logs
+ * and locally purging cache keys related to entities that appear in this data.
+ *
+ * This is useful for repairing cache when purges are missed by using a reliable
+ * stream, such as Kafka or a replicated MySQL table. Purge loss between datacenters
+ * is expected to be more common than within them.
+ *
+ * @since 1.28
+ */
+class WANObjectCacheReaper implements LoggerAwareInterface {
+       /** @var WANObjectCache */
+       protected $cache;
+       /** @var BagOStuff */
+       protected $store;
+       /** @var callable */
+       protected $logChunkCallback;
+       /** @var callable */
+       protected $keyListCallback;
+       /** @var LoggerInterface */
+       protected $logger;
+
+       /** @var string */
+       protected $channel;
+       /** @var int */
+       protected $initialStartWindow;
+
+       /**
+        * @param WANObjectCache $cache Cache to reap bad keys from
+        * @param BagOStuff $store Cache to store positions use for locking
+        * @param callable $logCallback Callback taking arguments:
+        *          - The starting position as a UNIX timestamp
+        *          - The starting unique ID used for breaking timestamp collisions or null
+        *          - The ending position as a UNIX timestamp
+        *          - The maximum number of results to return
+        *        It returns a list of maps of (key: cache key, pos: UNIX timestamp, id: unique ID)
+        *        for each key affected, with the corrosponding event timestamp/ID information.
+        *        The events should be in ascending order, by (timestamp,id).
+        * @param callable $keyCallback Callback taking arguments:
+        *          - The WANObjectCache instance
+        *          - An object from the event log
+        *        It should return a list of WAN cache keys.
+        *        The callback must fully duck-type test the object, since can be any model class.
+        * @param array $params Additional options:
+        *          - channel: the name of the update event stream.
+        *          - initialStartWindow: seconds back in time to start if the position is lost.
+        *            Default: 1 hour.
+        *          - logger: an SPL monolog instance [optional]
+        */
+       public function __construct(
+               WANObjectCache $cache,
+               BagOStuff $store,
+               callable $logCallback,
+               callable $keyCallback,
+               array $params
+       ) {
+               $this->cache = $cache;
+               $this->store = $store;
+
+               $this->logChunkCallback = $logCallback;
+               $this->keyListCallback = $keyCallback;
+               if ( isset( $params['channel'] ) ) {
+                       $this->channel = $params['channel'];
+               } else {
+                       throw new UnexpectedValueException( "No channel specified." );
+               }
+
+               $this->initialStartWindow = $params['initialStartWindow'] ?? 3600;
+               $this->logger = $params['logger'] ?? new NullLogger();
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Check and reap stale keys based on a chunk of events
+        *
+        * @param int $n Number of events
+        * @return int Number of keys checked
+        */
+       final public function invoke( $n = 100 ) {
+               $posKey = $this->store->makeGlobalKey( 'WANCache', 'reaper', $this->channel );
+               $scopeLock = $this->store->getScopedLock( "$posKey:busy", 0 );
+               if ( !$scopeLock ) {
+                       return 0;
+               }
+
+               $now = time();
+               $status = $this->store->get( $posKey );
+               if ( !$status ) {
+                       $status = [ 'pos' => $now - $this->initialStartWindow, 'id' => null ];
+               }
+
+               // Get events for entities who's keys tombstones/hold-off should have expired by now
+               $events = call_user_func_array(
+                       $this->logChunkCallback,
+                       [ $status['pos'], $status['id'], $now - WANObjectCache::HOLDOFF_TTL - 1, $n ]
+               );
+
+               $event = null;
+               $keyEvents = [];
+               foreach ( $events as $event ) {
+                       $keys = call_user_func_array(
+                               $this->keyListCallback,
+                               [ $this->cache, $event['item'] ]
+                       );
+                       foreach ( $keys as $key ) {
+                               unset( $keyEvents[$key] ); // use only the latest per key
+                               $keyEvents[$key] = [
+                                       'pos' => $event['pos'],
+                                       'id' => $event['id']
+                               ];
+                       }
+               }
+
+               $purgeCount = 0;
+               $lastOkEvent = null;
+               foreach ( $keyEvents as $key => $keyEvent ) {
+                       if ( !$this->cache->reap( $key, $keyEvent['pos'] ) ) {
+                               break;
+                       }
+                       ++$purgeCount;
+                       $lastOkEvent = $event;
+               }
+
+               if ( $lastOkEvent ) {
+                       $ok = $this->store->merge(
+                               $posKey,
+                               function ( $bag, $key, $curValue ) use ( $lastOkEvent ) {
+                                       if ( !$curValue ) {
+                                               // Use new position
+                                       } else {
+                                               $curCoord = [ $curValue['pos'], $curValue['id'] ];
+                                               $newCoord = [ $lastOkEvent['pos'], $lastOkEvent['id'] ];
+                                               if ( $newCoord < $curCoord ) {
+                                                       // Keep prior position instead of rolling it back
+                                                       return $curValue;
+                                               }
+                                       }
+
+                                       return [
+                                               'pos' => $lastOkEvent['pos'],
+                                               'id' => $lastOkEvent['id'],
+                                               'ctime' => $curValue ? $curValue['ctime'] : date( 'c' )
+                                       ];
+                               },
+                               IExpiringStore::TTL_INDEFINITE
+                       );
+
+                       $pos = $lastOkEvent['pos'];
+                       $id = $lastOkEvent['id'];
+                       if ( $ok ) {
+                               $this->logger->info( "Updated cache reap position ($pos, $id)." );
+                       } else {
+                               $this->logger->error( "Could not update cache reap position ($pos, $id)." );
+                       }
+               }
+
+               ScopedCallback::consume( $scopeLock );
+
+               return $purgeCount;
+       }
+
+       /**
+        * @return array|bool Returns (pos, id) map or false if not set
+        */
+       public function getState() {
+               $posKey = $this->store->makeGlobalKey( 'WANCache', 'reaper', $this->channel );
+
+               return $this->store->get( $posKey );
+       }
+}
diff --git a/includes/libs/objectcache/wancache/WinCacheBagOStuff.php b/includes/libs/objectcache/wancache/WinCacheBagOStuff.php
new file mode 100644 (file)
index 0000000..0e4e3fb
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+/**
+ * Object caching using WinCache.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Cache
+ */
+
+/**
+ * Wrapper for WinCache object caching functions; identical interface
+ * to the APC wrapper
+ *
+ * @ingroup Cache
+ */
+class WinCacheBagOStuff extends MediumSpecificBagOStuff {
+       protected function doGet( $key, $flags = 0, &$casToken = null ) {
+               $casToken = null;
+
+               $blob = wincache_ucache_get( $key );
+               if ( !is_string( $blob ) && !is_int( $blob ) ) {
+                       return false;
+               }
+
+               $value = $this->unserialize( $blob );
+               if ( $value !== false ) {
+                       $casToken = (string)$blob; // don't bother hashing this
+               }
+
+               return $value;
+       }
+
+       protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
+               if ( !wincache_lock( $key ) ) { // optimize with FIFO lock
+                       return false;
+               }
+
+               $curCasToken = null; // passed by reference
+               $this->doGet( $key, self::READ_LATEST, $curCasToken );
+               if ( $casToken === $curCasToken ) {
+                       $success = $this->set( $key, $value, $exptime, $flags );
+               } else {
+                       $this->logger->info(
+                               __METHOD__ . ' failed due to race condition for {key}.',
+                               [ 'key' => $key ]
+                       );
+
+                       $success = false; // mismatched or failed
+               }
+
+               wincache_unlock( $key );
+
+               return $success;
+       }
+
+       protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+               $result = wincache_ucache_set( $key, $this->serialize( $value ), $exptime );
+
+               // false positive, wincache_ucache_set returns an empty array
+               // in some circumstances.
+               // @phan-suppress-next-line PhanTypeComparisonToArray
+               return ( $result === [] || $result === true );
+       }
+
+       public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+               if ( wincache_ucache_exists( $key ) ) {
+                       return false; // avoid warnings
+               }
+
+               $result = wincache_ucache_add( $key, $this->serialize( $value ), $exptime );
+
+               // false positive, wincache_ucache_add returns an empty array
+               // in some circumstances.
+               // @phan-suppress-next-line PhanTypeComparisonToArray
+               return ( $result === [] || $result === true );
+       }
+
+       protected function doDelete( $key, $flags = 0 ) {
+               wincache_ucache_delete( $key );
+
+               return true;
+       }
+
+       /**
+        * Construct a cache key.
+        *
+        * @since 1.27
+        * @param string $keyspace
+        * @param array $args
+        * @return string
+        */
+       public function makeKeyInternal( $keyspace, $args ) {
+               // WinCache keys have a maximum length of 150 characters. From that,
+               // subtract the number of characters we need for the keyspace and for
+               // the separator character needed for each argument. To handle some
+               // custom prefixes used by thing like WANObjectCache, limit to 125.
+               // NOTE: Same as in memcached, except the max key length there is 255.
+               $charsLeft = 125 - strlen( $keyspace ) - count( $args );
+
+               $args = array_map(
+                       function ( $arg ) use ( &$charsLeft ) {
+                               // 33 = 32 characters for the MD5 + 1 for the '#' prefix.
+                               if ( $charsLeft > 33 && strlen( $arg ) > $charsLeft ) {
+                                       $arg = '#' . md5( $arg );
+                               }
+
+                               $charsLeft -= strlen( $arg );
+                               return $arg;
+                       },
+                       $args
+               );
+
+               if ( $charsLeft < 0 ) {
+                       return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) );
+               }
+
+               return $keyspace . ':' . implode( ':', $args );
+       }
+
+       /**
+        * Increase stored value of $key by $value while preserving its original TTL
+        * @param string $key Key to increase
+        * @param int $value Value to add to $key (Default 1)
+        * @return int|bool New value or false on failure
+        */
+       public function incr( $key, $value = 1 ) {
+               if ( !wincache_lock( $key ) ) { // optimize with FIFO lock
+                       return false;
+               }
+
+               $n = $this->doGet( $key );
+               if ( $this->isInteger( $n ) ) {
+                       $n = max( $n + (int)$value, 0 );
+                       $oldTTL = wincache_ucache_info( false, $key )["ucache_entries"][1]["ttl_seconds"];
+                       $this->set( $key, $n, $oldTTL );
+               } else {
+                       $n = false;
+               }
+
+               wincache_unlock( $key );
+
+               return $n;
+       }
+}