Move RedisLockManager to /libs
authorAaron Schulz <aschulz@wikimedia.org>
Fri, 23 Sep 2016 07:30:31 +0000 (00:30 -0700)
committerAaron Schulz <aschulz@wikimedia.org>
Fri, 23 Sep 2016 07:30:47 +0000 (00:30 -0700)
Change-Id: Ia476f117243cf0f6984e373d460b24db18584704

autoload.php
includes/filebackend/lockmanager/MySqlLockManager.php
includes/filebackend/lockmanager/RedisLockManager.php [deleted file]
includes/libs/lockmanager/DBLockManager.php
includes/libs/lockmanager/LockManager.php
includes/libs/lockmanager/RedisLockManager.php [new file with mode: 0644]

index 2b70387..b3747fe 100644 (file)
@@ -1140,7 +1140,7 @@ $wgAutoloadLocalClasses = [
        'RedisBagOStuff' => __DIR__ . '/includes/objectcache/RedisBagOStuff.php',
        'RedisConnRef' => __DIR__ . '/includes/libs/redis/RedisConnRef.php',
        'RedisConnectionPool' => __DIR__ . '/includes/libs/redis/RedisConnectionPool.php',
-       'RedisLockManager' => __DIR__ . '/includes/filebackend/lockmanager/RedisLockManager.php',
+       'RedisLockManager' => __DIR__ . '/includes/libs/lockmanager/RedisLockManager.php',
        'RedisPubSubFeedEngine' => __DIR__ . '/includes/rcfeed/RedisPubSubFeedEngine.php',
        'RefreshFileHeaders' => __DIR__ . '/maintenance/refreshFileHeaders.php',
        'RefreshImageMetadata' => __DIR__ . '/maintenance/refreshImageMetadata.php',
index 441ffea..125b5e2 100644 (file)
@@ -15,6 +15,12 @@ class MySqlLockManager extends DBLockManager {
                self::LOCK_EX => self::LOCK_EX
        ];
 
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->session = substr( $this->session, 0, 31 ); // fit to field
+       }
+
        protected function initConnection( $lockDb, IDatabase $db ) {
                # Let this transaction see lock rows from other transactions
                $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
diff --git a/includes/filebackend/lockmanager/RedisLockManager.php b/includes/filebackend/lockmanager/RedisLockManager.php
deleted file mode 100644 (file)
index 267aece..0000000
+++ /dev/null
@@ -1,282 +0,0 @@
-<?php
-/**
- * Version of LockManager based on using redis servers.
- *
- * 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 LockManager
- */
-use Psr\Log\LoggerInterface;
-
-/**
- * Manage locks using redis servers.
- *
- * Version of LockManager based on using redis servers.
- * This is meant for multi-wiki systems that may share files.
- * All locks are non-blocking, which avoids deadlocks.
- *
- * All lock requests for a resource, identified by a hash string, will map to one
- * bucket. Each bucket maps to one or several peer servers, each running redis.
- * A majority of peers must agree for a lock to be acquired.
- *
- * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
- *
- * @ingroup LockManager
- * @since 1.22
- */
-class RedisLockManager extends QuorumLockManager {
-       /** @var array Mapping of lock types to the type actually used */
-       protected $lockTypeMap = [
-               self::LOCK_SH => self::LOCK_SH,
-               self::LOCK_UW => self::LOCK_SH,
-               self::LOCK_EX => self::LOCK_EX
-       ];
-
-       /** @var RedisConnectionPool */
-       protected $redisPool;
-
-       /** @var array Map server names to hostname/IP and port numbers */
-       protected $lockServers = [];
-
-       /** @var LoggerInterface */
-       protected $logger;
-       /** @var string Random UUID */
-       protected $session = '';
-
-       /**
-        * Construct a new instance from configuration.
-        *
-        * @param array $config Parameters include:
-        *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
-        *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
-        *                    each having an odd-numbered list of server names (peers) as values.
-        *   - redisConfig  : Configuration for RedisConnectionPool::__construct().
-        * @throws Exception
-        */
-       public function __construct( array $config ) {
-               parent::__construct( $config );
-
-               $this->lockServers = $config['lockServers'];
-               // Sanitize srvsByBucket config to prevent PHP errors
-               $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
-               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
-
-               $config['redisConfig']['serializer'] = 'none';
-               $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
-
-               $this->session = wfRandomString( 32 );
-               $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
-       }
-
-       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = StatusValue::newGood();
-
-               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
-
-               $server = $this->lockServers[$lockSrv];
-               $conn = $this->redisPool->getConnection( $server, $this->logger );
-               if ( !$conn ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-
-                       return $status;
-               }
-
-               $pathsByKey = []; // (type:hash => path) map
-               foreach ( $pathsByType as $type => $paths ) {
-                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
-                       foreach ( $paths as $path ) {
-                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
-                       }
-               }
-
-               try {
-                       static $script =
-<<<LUA
-                       local failed = {}
-                       -- Load input params (e.g. session, ttl, time of request)
-                       local rSession, rTTL, rTime = unpack(ARGV)
-                       -- Check that all the locks can be acquired
-                       for i,requestKey in ipairs(KEYS) do
-                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
-                               local keyIsFree = true
-                               local currentLocks = redis.call('hKeys',resourceKey)
-                               for i,lockKey in ipairs(currentLocks) do
-                                       -- Get the type and session of this lock
-                                       local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
-                                       -- Check any locks that are not owned by this session
-                                       if session ~= rSession then
-                                               local lockExpiry = redis.call('hGet',resourceKey,lockKey)
-                                               if 1*lockExpiry < 1*rTime then
-                                                       -- Lock is stale, so just prune it out
-                                                       redis.call('hDel',resourceKey,lockKey)
-                                               elseif rType == 'EX' or type == 'EX' then
-                                                       keyIsFree = false
-                                                       break
-                                               end
-                                       end
-                               end
-                               if not keyIsFree then
-                                       failed[#failed+1] = requestKey
-                               end
-                       end
-                       -- If all locks could be acquired, then do so
-                       if #failed == 0 then
-                               for i,requestKey in ipairs(KEYS) do
-                                       local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
-                                       redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
-                                       -- In addition to invalidation logic, be sure to garbage collect
-                                       redis.call('expire',resourceKey,rTTL)
-                               end
-                       end
-                       return failed
-LUA;
-                       $res = $conn->luaEval( $script,
-                               array_merge(
-                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
-                                       [
-                                               $this->session, // ARGV[1]
-                                               $this->lockTTL, // ARGV[2]
-                                               time() // ARGV[3]
-                                       ]
-                               ),
-                               count( $pathsByKey ) # number of first argument(s) that are keys
-                       );
-               } catch ( RedisException $e ) {
-                       $res = false;
-                       $this->redisPool->handleError( $conn, $e );
-               }
-
-               if ( $res === false ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
-                       }
-               } else {
-                       foreach ( $res as $key ) {
-                               $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
-               $status = StatusValue::newGood();
-
-               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
-
-               $server = $this->lockServers[$lockSrv];
-               $conn = $this->redisPool->getConnection( $server, $this->logger );
-               if ( !$conn ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $path );
-                       }
-
-                       return $status;
-               }
-
-               $pathsByKey = []; // (type:hash => path) map
-               foreach ( $pathsByType as $type => $paths ) {
-                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
-                       foreach ( $paths as $path ) {
-                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
-                       }
-               }
-
-               try {
-                       static $script =
-<<<LUA
-                       local failed = {}
-                       -- Load input params (e.g. session)
-                       local rSession = unpack(ARGV)
-                       for i,requestKey in ipairs(KEYS) do
-                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
-                               local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
-                               if released > 0 then
-                                       -- Remove the whole structure if it is now empty
-                                       if redis.call('hLen',resourceKey) == 0 then
-                                               redis.call('del',resourceKey)
-                                       end
-                               else
-                                       failed[#failed+1] = requestKey
-                               end
-                       end
-                       return failed
-LUA;
-                       $res = $conn->luaEval( $script,
-                               array_merge(
-                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
-                                       [
-                                               $this->session, // ARGV[1]
-                                       ]
-                               ),
-                               count( $pathsByKey ) # number of first argument(s) that are keys
-                       );
-               } catch ( RedisException $e ) {
-                       $res = false;
-                       $this->redisPool->handleError( $conn, $e );
-               }
-
-               if ( $res === false ) {
-                       foreach ( $pathList as $path ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $path );
-                       }
-               } else {
-                       foreach ( $res as $key ) {
-                               $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
-                       }
-               }
-
-               return $status;
-       }
-
-       protected function releaseAllLocks() {
-               return StatusValue::newGood(); // not supported
-       }
-
-       protected function isServerUp( $lockSrv ) {
-               $conn = $this->redisPool->getConnection( $this->lockServers[$lockSrv], $this->logger );
-
-               return (bool)$conn;
-       }
-
-       /**
-        * @param string $path
-        * @param string $type One of (EX,SH)
-        * @return string
-        */
-       protected function recordKeyForPath( $path, $type ) {
-               return implode( ':',
-                       [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
-       }
-
-       /**
-        * Make sure remaining locks get cleared for sanity
-        */
-       function __destruct() {
-               while ( count( $this->locksHeld ) ) {
-                       $pathsByType = [];
-                       foreach ( $this->locksHeld as $path => $locks ) {
-                               foreach ( $locks as $type => $count ) {
-                                       $pathsByType[$type][] = $path;
-                               }
-                       }
-                       $this->unlockByType( $pathsByType );
-               }
-       }
-}
index c36ff48..b058146 100644 (file)
@@ -43,8 +43,6 @@ abstract class DBLockManager extends QuorumLockManager {
 
        protected $lockExpiry; // integer number of seconds
        protected $safeDelay; // integer number of seconds
-
-       protected $session = 0; // random integer
        /** @var IDatabase[] Map Database connections (DB name => Database) */
        protected $conns = [];
 
@@ -91,12 +89,6 @@ abstract class DBLockManager extends QuorumLockManager {
                $this->statusCache = isset( $config['srvCache'] )
                        ? $config['srvCache']
                        : new HashBagOStuff();
-
-               $random = [];
-               for ( $i = 1; $i <= 5; ++$i ) {
-                       $random[] = mt_rand( 0, 0xFFFFFFF );
-               }
-               $this->session = substr( md5( implode( '-', $random ) ), 0, 31 );
        }
 
        /**
index 80add5b..42391a0 100644 (file)
@@ -3,6 +3,7 @@
  * @defgroup LockManager Lock management
  * @ingroup FileBackend
  */
+use Psr\Log\LoggerInterface;
 
 /**
  * Resource locking handling.
@@ -43,6 +44,9 @@
  * @since 1.19
  */
 abstract class LockManager {
+       /** @var LoggerInterface */
+       protected $logger;
+
        /** @var array Mapping of lock types to the type actually used */
        protected $lockTypeMap = [
                self::LOCK_SH => self::LOCK_SH,
@@ -56,6 +60,9 @@ abstract class LockManager {
        protected $domain; // string; domain (usually wiki ID)
        protected $lockTTL; // integer; maximum time locks can be held
 
+       /** @var string Random 32-char hex number */
+       protected $session;
+
        /** Lock types; stronger locks have higher values */
        const LOCK_SH = 1; // shared lock (for reads)
        const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
@@ -79,6 +86,14 @@ abstract class LockManager {
                        $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
                        $this->lockTTL = max( 5 * 60, 2 * (int)$met );
                }
+
+               $random = [];
+               for ( $i = 1; $i <= 5; ++$i ) {
+                       $random[] = mt_rand( 0, 0xFFFFFFF );
+               }
+               $this->session = md5( implode( '-', $random ) );
+
+               $this->logger = isset( $config['logger'] ) ? $config['logger'] : new \Psr\Log\NullLogger();
        }
 
        /**
diff --git a/includes/libs/lockmanager/RedisLockManager.php b/includes/libs/lockmanager/RedisLockManager.php
new file mode 100644 (file)
index 0000000..a9f5ca3
--- /dev/null
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Version of LockManager based on using redis servers.
+ *
+ * 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 LockManager
+ */
+
+/**
+ * Manage locks using redis servers.
+ *
+ * Version of LockManager based on using redis servers.
+ * This is meant for multi-wiki systems that may share files.
+ * All locks are non-blocking, which avoids deadlocks.
+ *
+ * All lock requests for a resource, identified by a hash string, will map to one
+ * bucket. Each bucket maps to one or several peer servers, each running redis.
+ * A majority of peers must agree for a lock to be acquired.
+ *
+ * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
+ *
+ * @ingroup LockManager
+ * @since 1.22
+ */
+class RedisLockManager extends QuorumLockManager {
+       /** @var array Mapping of lock types to the type actually used */
+       protected $lockTypeMap = [
+               self::LOCK_SH => self::LOCK_SH,
+               self::LOCK_UW => self::LOCK_SH,
+               self::LOCK_EX => self::LOCK_EX
+       ];
+
+       /** @var RedisConnectionPool */
+       protected $redisPool;
+
+       /** @var array Map server names to hostname/IP and port numbers */
+       protected $lockServers = [];
+
+       /**
+        * Construct a new instance from configuration.
+        *
+        * @param array $config Parameters include:
+        *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
+        *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
+        *                    each having an odd-numbered list of server names (peers) as values.
+        *   - redisConfig  : Configuration for RedisConnectionPool::__construct().
+        * @throws Exception
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+
+               $this->lockServers = $config['lockServers'];
+               // Sanitize srvsByBucket config to prevent PHP errors
+               $this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
+               $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
+
+               $config['redisConfig']['serializer'] = 'none';
+               $this->redisPool = RedisConnectionPool::singleton( $config['redisConfig'] );
+       }
+
+       protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
+               $server = $this->lockServers[$lockSrv];
+               $conn = $this->redisPool->getConnection( $server, $this->logger );
+               if ( !$conn ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+
+                       return $status;
+               }
+
+               $pathsByKey = []; // (type:hash => path) map
+               foreach ( $pathsByType as $type => $paths ) {
+                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
+                       foreach ( $paths as $path ) {
+                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
+                       }
+               }
+
+               try {
+                       static $script =
+<<<LUA
+                       local failed = {}
+                       -- Load input params (e.g. session, ttl, time of request)
+                       local rSession, rTTL, rTime = unpack(ARGV)
+                       -- Check that all the locks can be acquired
+                       for i,requestKey in ipairs(KEYS) do
+                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
+                               local keyIsFree = true
+                               local currentLocks = redis.call('hKeys',resourceKey)
+                               for i,lockKey in ipairs(currentLocks) do
+                                       -- Get the type and session of this lock
+                                       local _, _, type, session = string.find(lockKey,"(%w+):(%w+)")
+                                       -- Check any locks that are not owned by this session
+                                       if session ~= rSession then
+                                               local lockExpiry = redis.call('hGet',resourceKey,lockKey)
+                                               if 1*lockExpiry < 1*rTime then
+                                                       -- Lock is stale, so just prune it out
+                                                       redis.call('hDel',resourceKey,lockKey)
+                                               elseif rType == 'EX' or type == 'EX' then
+                                                       keyIsFree = false
+                                                       break
+                                               end
+                                       end
+                               end
+                               if not keyIsFree then
+                                       failed[#failed+1] = requestKey
+                               end
+                       end
+                       -- If all locks could be acquired, then do so
+                       if #failed == 0 then
+                               for i,requestKey in ipairs(KEYS) do
+                                       local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
+                                       redis.call('hSet',resourceKey,rType .. ':' .. rSession,rTime + rTTL)
+                                       -- In addition to invalidation logic, be sure to garbage collect
+                                       redis.call('expire',resourceKey,rTTL)
+                               end
+                       end
+                       return failed
+LUA;
+                       $res = $conn->luaEval( $script,
+                               array_merge(
+                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
+                                       [
+                                               $this->session, // ARGV[1]
+                                               $this->lockTTL, // ARGV[2]
+                                               time() // ARGV[3]
+                                       ]
+                               ),
+                               count( $pathsByKey ) # number of first argument(s) that are keys
+                       );
+               } catch ( RedisException $e ) {
+                       $res = false;
+                       $this->redisPool->handleError( $conn, $e );
+               }
+
+               if ( $res === false ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $path );
+                       }
+               } else {
+                       foreach ( $res as $key ) {
+                               $status->fatal( 'lockmanager-fail-acquirelock', $pathsByKey[$key] );
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
+               $status = StatusValue::newGood();
+
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
+               $server = $this->lockServers[$lockSrv];
+               $conn = $this->redisPool->getConnection( $server, $this->logger );
+               if ( !$conn ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+
+                       return $status;
+               }
+
+               $pathsByKey = []; // (type:hash => path) map
+               foreach ( $pathsByType as $type => $paths ) {
+                       $typeString = ( $type == LockManager::LOCK_SH ) ? 'SH' : 'EX';
+                       foreach ( $paths as $path ) {
+                               $pathsByKey[$this->recordKeyForPath( $path, $typeString )] = $path;
+                       }
+               }
+
+               try {
+                       static $script =
+<<<LUA
+                       local failed = {}
+                       -- Load input params (e.g. session)
+                       local rSession = unpack(ARGV)
+                       for i,requestKey in ipairs(KEYS) do
+                               local _, _, rType, resourceKey = string.find(requestKey,"(%w+):(%w+)$")
+                               local released = redis.call('hDel',resourceKey,rType .. ':' .. rSession)
+                               if released > 0 then
+                                       -- Remove the whole structure if it is now empty
+                                       if redis.call('hLen',resourceKey) == 0 then
+                                               redis.call('del',resourceKey)
+                                       end
+                               else
+                                       failed[#failed+1] = requestKey
+                               end
+                       end
+                       return failed
+LUA;
+                       $res = $conn->luaEval( $script,
+                               array_merge(
+                                       array_keys( $pathsByKey ), // KEYS[0], KEYS[1],...,KEYS[N]
+                                       [
+                                               $this->session, // ARGV[1]
+                                       ]
+                               ),
+                               count( $pathsByKey ) # number of first argument(s) that are keys
+                       );
+               } catch ( RedisException $e ) {
+                       $res = false;
+                       $this->redisPool->handleError( $conn, $e );
+               }
+
+               if ( $res === false ) {
+                       foreach ( $pathList as $path ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $path );
+                       }
+               } else {
+                       foreach ( $res as $key ) {
+                               $status->fatal( 'lockmanager-fail-releaselock', $pathsByKey[$key] );
+                       }
+               }
+
+               return $status;
+       }
+
+       protected function releaseAllLocks() {
+               return StatusValue::newGood(); // not supported
+       }
+
+       protected function isServerUp( $lockSrv ) {
+               $conn = $this->redisPool->getConnection( $this->lockServers[$lockSrv], $this->logger );
+
+               return (bool)$conn;
+       }
+
+       /**
+        * @param string $path
+        * @param string $type One of (EX,SH)
+        * @return string
+        */
+       protected function recordKeyForPath( $path, $type ) {
+               return implode( ':',
+                       [ __CLASS__, 'locks', "$type:" . $this->sha1Base36Absolute( $path ) ] );
+       }
+
+       /**
+        * Make sure remaining locks get cleared for sanity
+        */
+       function __destruct() {
+               while ( count( $this->locksHeld ) ) {
+                       $pathsByType = [];
+                       foreach ( $this->locksHeld as $path => $locks ) {
+                               foreach ( $locks as $type => $count ) {
+                                       $pathsByType[$type][] = $path;
+                               }
+                       }
+                       $this->unlockByType( $pathsByType );
+               }
+       }
+}