Aaron Schulz has uploaded a new change for review.
https://gerrit.wikimedia.org/r/49799
Change subject: [LockManager] Added a RedisLockManager class.
......................................................................
[LockManager] Added a RedisLockManager class.
* Also made some tiny fixes to MemcLockManager.
Change-Id: I7ade74eb307a5075533f36836768af60f106a6b9
---
M includes/filebackend/lockmanager/MemcLockManager.php
A includes/filebackend/lockmanager/RedisLockManager.php
2 files changed, 261 insertions(+), 6 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/99/49799/1
diff --git a/includes/filebackend/lockmanager/MemcLockManager.php
b/includes/filebackend/lockmanager/MemcLockManager.php
index 099f11d..ab784bc 100644
--- a/includes/filebackend/lockmanager/MemcLockManager.php
+++ b/includes/filebackend/lockmanager/MemcLockManager.php
@@ -28,8 +28,8 @@
* 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 memcached.
+ * 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
memcached.
* A majority of peers must agree for a lock to be acquired.
*
* @ingroup LockManager
@@ -49,7 +49,7 @@
protected $serversUp = array(); // (server name => bool)
protected $lockExpiry; // integer; maximum time locks can be held
- protected $session = ''; // string; random SHA-1 UUID
+ protected $session = ''; // string; random UUID
/**
* Construct a new instance from configuration.
@@ -146,9 +146,15 @@
// If there were no lock conflicts, update all the lock
records...
if ( $status->isOK() ) {
- foreach ( $lockRecords as $locksKey => $locksHeld ) {
- $memc->set( $locksKey, $locksHeld );
- wfDebug( __METHOD__ . ": acquired lock on key
$locksKey.\n" );
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path );
+ $locksHeld = $lockRecords[$locksKey];
+ $ok = $memc->set( $locksKey, $locksHeld );
+ if ( !$ok ) {
+ $status->fatal(
'lockmanager-fail-acquirelock', $path );
+ } else {
+ wfDebug( __METHOD__ . ": acquired lock
on key $locksKey.\n" );
+ }
}
}
diff --git a/includes/filebackend/lockmanager/RedisLockManager.php
b/includes/filebackend/lockmanager/RedisLockManager.php
new file mode 100644
index 0000000..97cf0dc
--- /dev/null
+++ b/includes/filebackend/lockmanager/RedisLockManager.php
@@ -0,0 +1,249 @@
+<?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.
+ *
+ * @ingroup LockManager
+ * @since 1.21
+ */
+class RedisLockManager extends QuorumLockManager {
+ /** @var Array Mapping of lock types to the type actually used */
+ protected $lockTypeMap = array(
+ self::LOCK_SH => self::LOCK_SH,
+ self::LOCK_UW => self::LOCK_SH,
+ self::LOCK_EX => self::LOCK_EX
+ );
+
+ /** @var RedisConnectionPool */
+ protected $redisPool = array();
+ /** @var Array Map server names to hostname/IP and port numbers */
+ protected $lockServers = array();
+ /** @var Array */
+ protected $serversUp = array(); // (server name => bool)
+
+ protected $lockExpiry; // integer; maximum time locks can be held
+ protected $session = ''; // string; random UUID
+
+ /**
+ * Construct a new instance from configuration.
+ *
+ * $config paramaters 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().
+ *
+ * @param Array $config
+ * @throws MWException
+ */
+ 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
+
+ $this->redisPool = RedisConnectionPool::singleton(
$config['redisConfig'] );
+
+ $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
+ $this->lockExpiry = $met ? 2*(int)$met : 2*3600;
+
+ $this->session = wfRandomString( 32 );
+ }
+
+ /**
+ * @see QuorumLockManager::getLocksOnServer()
+ * @return Status
+ */
+ protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $server = $this->lockServers[$lockSrv];
+ $conn = $this->redisPool->getConnection( $server );
+ if ( !$conn ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-acquirelock',
$path );
+ }
+ return $status;
+ }
+
+ $keys = array_map( array( $this, 'recordKeyForPath' ), $paths
); // lock records
+ try {
+ $conn->watch( $keys );
+ // Fetch all the existing lock records...
+ $lockRecords = array_combine( $keys, $conn->mGet( $keys
) );
+
+ $now = time();
+ // Check if the requested locks conflict with existing
ones...
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path );
+ $locksHeld = is_array( $lockRecords[$locksKey] )
+ ? $lockRecords[$locksKey]
+ : array( self::LOCK_SH => array(),
self::LOCK_EX => array() ); // init
+ foreach ( $locksHeld[self::LOCK_EX] as $session
=> $expiry ) {
+ if ( $expiry < $now ) { // stale?
+ unset(
$locksHeld[self::LOCK_EX][$session] );
+ } elseif ( $session !== $this->session
) {
+ $status->fatal(
'lockmanager-fail-acquirelock', $path );
+ }
+ }
+ if ( $type === self::LOCK_EX ) {
+ foreach ( $locksHeld[self::LOCK_SH] as
$session => $expiry ) {
+ if ( $expiry < $now ) { //
stale?
+ unset(
$locksHeld[self::LOCK_SH][$session] );
+ } elseif ( $session !==
$this->session ) {
+ $status->fatal(
'lockmanager-fail-acquirelock', $path );
+ }
+ }
+ }
+ if ( $status->isOK() ) {
+ // Register the session in the lock
record array
+ $locksHeld[$type][$this->session] =
$now + $this->lockExpiry;
+ // We will update this record if none
of the other locks conflict
+ $lockRecords[$locksKey] = $locksHeld;
+ }
+ }
+
+ // If there were no lock conflicts, update all the lock
records...
+ if ( $status->isOK() ) {
+ $conn->multi(); // begin (atomic trx)
+ $conn->mSet( $lockRecords );
+ $res = $conn->exec(); // commit (atomic trx)
+ if ( $res === false ) {
+ foreach ( $paths as $path ) {
+ $status->fatal(
'lockmanager-fail-acquirelock', $path );
+ }
+ } else {
+ foreach ( $lockRecords as $locksKey =>
$locksHeld ) {
+ wfDebug( __METHOD__ . ":
acquired lock on key $locksKey.\n" );
+ }
+ }
+ }
+ } catch ( RedisException $e ) {
+ $this->redisPool->handleException( $server, $conn, $e );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::freeLocksOnServer()
+ * @return Status
+ */
+ protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
+ $status = Status::newGood();
+
+ $server = $this->lockServers[$lockSrv];
+ $conn = $this->redisPool->getConnection( $server );
+ if ( !$conn ) {
+ foreach ( $paths as $path ) {
+ $status->fatal( 'lockmanager-fail-releaselock',
$path );
+ }
+ return $status;
+ }
+
+ $keys = array_map( array( $this, 'recordKeyForPath' ), $paths
); // lock records
+ try {
+ $conn->watch( $keys );
+ // Fetch all the existing lock records...
+ $lockRecords = array_combine( $keys, $conn->mGet( $keys
) );
+
+ // Remove the requested locks from all records...
+ foreach ( $paths as $path ) {
+ $locksKey = $this->recordKeyForPath( $path );
// lock record
+ if ( !is_array( $lockRecords[$locksKey] ) ) {
+ unset( $lockRecords[$locksKey] );
+ continue; // nothing to do
+ }
+ $locksHeld = $lockRecords[$locksKey];
+ if ( is_array( $locksHeld ) && isset(
$locksHeld[$type] ) ) {
+ unset(
$locksHeld[$type][$this->session] );
+ }
+ }
+
+ // Update all lock records...
+ $conn->multi(); // begin (atomic trx)
+ $conn->mSet( $lockRecords );
+ $res = $conn->exec(); // commit (atomic trx)
+ if ( $res === false ) {
+ foreach ( $paths as $path ) {
+ $status->fatal(
'lockmanager-fail-releaselock', $path );
+ }
+ } else {
+ foreach ( $lockRecords as $locksKey =>
$locksHeld ) {
+ wfDebug( __METHOD__ . ": released lock
on key $locksKey.\n" );
+ }
+ }
+ } catch ( RedisException $e ) {
+ $this->redisPool->handleException( $server, $conn, $e );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @see QuorumLockManager::releaseAllLocks()
+ * @return Status
+ */
+ protected function releaseAllLocks() {
+ return Status::newGood(); // not supported
+ }
+
+ /**
+ * @see QuorumLockManager::isServerUp()
+ * @return bool
+ */
+ protected function isServerUp( $lockSrv ) {
+ return (bool)$this->redisPool->getConnection(
$this->lockServers[$lockSrv] );
+ }
+
+ /**
+ * @param $path string
+ * @return string
+ */
+ protected function recordKeyForPath( $path ) {
+ return implode( ':', array( __CLASS__, 'locks',
$this->sha1Base36Absolute( $path ) ) );
+ }
+
+ /**
+ * Make sure remaining locks get cleared for sanity
+ */
+ function __destruct() {
+ while ( count( $this->locksHeld ) ) {
+ foreach ( $this->locksHeld as $path => $locks ) {
+ $this->doUnlock( array( $path ), self::LOCK_EX
);
+ $this->doUnlock( array( $path ), self::LOCK_SH
);
+ }
+ }
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/49799
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I7ade74eb307a5075533f36836768af60f106a6b9
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Aaron Schulz <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits