3 * Version of LockManager that uses a quorum from peer servers for locks.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup LockManager
25 * Version of LockManager that uses a quorum from peer servers for locks.
26 * The resource space can also be sharded into separate peer groups.
28 * @ingroup LockManager
31 abstract class QuorumLockManager
extends LockManager
{
32 /** @var Array Map of bucket indexes to peer server lists */
33 protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...))
36 * @see LockManager::doLock()
41 final protected function doLock( array $paths, $type ) {
42 $status = Status
::newGood();
44 $pathsToLock = array(); // (bucket => paths)
45 // Get locks that need to be acquired (buckets => locks)...
46 foreach ( $paths as $path ) {
47 if ( isset( $this->locksHeld
[$path][$type] ) ) {
48 ++
$this->locksHeld
[$path][$type];
50 $bucket = $this->getBucketFromPath( $path );
51 $pathsToLock[$bucket][] = $path;
55 $lockedPaths = array(); // files locked in this attempt
56 // Attempt to acquire these locks...
57 foreach ( $pathsToLock as $bucket => $paths ) {
58 // Try to acquire the locks for this bucket
59 $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) );
60 if ( !$status->isOK() ) {
61 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
64 // Record these locks as active
65 foreach ( $paths as $path ) {
66 $this->locksHeld
[$path][$type] = 1; // locked
68 // Keep track of what locks were made in this attempt
69 $lockedPaths = array_merge( $lockedPaths, $paths );
76 * @see LockManager::doUnlock()
81 final protected function doUnlock( array $paths, $type ) {
82 $status = Status
::newGood();
84 $pathsToUnlock = array();
85 foreach ( $paths as $path ) {
86 if ( !isset( $this->locksHeld
[$path][$type] ) ) {
87 $status->warning( 'lockmanager-notlocked', $path );
89 --$this->locksHeld
[$path][$type];
90 // Reference count the locks held and release locks when zero
91 if ( $this->locksHeld
[$path][$type] <= 0 ) {
92 unset( $this->locksHeld
[$path][$type] );
93 $bucket = $this->getBucketFromPath( $path );
94 $pathsToUnlock[$bucket][] = $path;
96 if ( !count( $this->locksHeld
[$path] ) ) {
97 unset( $this->locksHeld
[$path] ); // no SH or EX locks left for key
102 // Remove these specific locks if possible, or at least release
103 // all locks once this process is currently not holding any locks.
104 foreach ( $pathsToUnlock as $bucket => $paths ) {
105 $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) );
107 if ( !count( $this->locksHeld
) ) {
108 $status->merge( $this->releaseAllLocks() );
115 * Attempt to acquire locks with the peers for a bucket.
116 * This is all or nothing; if any key is locked then this totally fails.
118 * @param $bucket integer
119 * @param array $paths List of resource keys to lock
120 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
123 final protected function doLockingRequestBucket( $bucket, array $paths, $type ) {
124 $status = Status
::newGood();
126 $yesVotes = 0; // locks made on trustable servers
127 $votesLeft = count( $this->srvsByBucket
[$bucket] ); // remaining peers
128 $quorum = floor( $votesLeft / 2 +
1 ); // simple majority
129 // Get votes for each peer, in order, until we have enough...
130 foreach ( $this->srvsByBucket
[$bucket] as $lockSrv ) {
131 if ( !$this->isServerUp( $lockSrv ) ) {
133 $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
134 continue; // server down?
136 // Attempt to acquire the lock on this peer
137 $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) );
138 if ( !$status->isOK() ) {
139 return $status; // vetoed; resource locked
141 ++
$yesVotes; // success for this peer
142 if ( $yesVotes >= $quorum ) {
143 return $status; // lock obtained
146 $votesNeeded = $quorum - $yesVotes;
147 if ( $votesNeeded > $votesLeft ) {
148 break; // short-circuit
151 // At this point, we must not have met the quorum
152 $status->setResult( false );
158 * Attempt to release locks with the peers for a bucket
160 * @param $bucket integer
161 * @param array $paths List of resource keys to lock
162 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
165 final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
166 $status = Status
::newGood();
168 foreach ( $this->srvsByBucket
[$bucket] as $lockSrv ) {
169 if ( !$this->isServerUp( $lockSrv ) ) {
170 $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
171 // Attempt to release the lock on this peer
173 $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) );
181 * Get the bucket for resource path.
182 * This should avoid throwing any exceptions.
184 * @param $path string
187 protected function getBucketFromPath( $path ) {
188 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
189 return (int)base_convert( $prefix, 16, 10 ) %
count( $this->srvsByBucket
);
193 * Check if a lock server is up
195 * @param $lockSrv string
198 abstract protected function isServerUp( $lockSrv );
201 * Get a connection to a lock server and acquire locks on $paths
203 * @param $lockSrv string
204 * @param $paths array
205 * @param $type integer
208 abstract protected function getLocksOnServer( $lockSrv, array $paths, $type );
211 * Get a connection to a lock server and release locks on $paths.
213 * Subclasses must effectively implement this or releaseAllLocks().
215 * @param $lockSrv string
216 * @param $paths array
217 * @param $type integer
220 abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type );
223 * Release all locks that this session is holding.
225 * Subclasses must effectively implement this or freeLocksOnServer().
229 abstract protected function releaseAllLocks();