MediaWiki master
QuorumLockManager.php
Go to the documentation of this file.
1<?php
7
8use LogicException;
10
22abstract class QuorumLockManager extends LockManager {
24 protected $srvsByBucket = []; // (bucket index => (lsrv1, lsrv2, ...))
25
27 protected $degradedBuckets = []; // (bucket index => UNIX timestamp)
28
30 final protected function doLockByType( array $pathsByType ) {
31 $status = StatusValue::newGood();
32
33 $pathsByTypeByBucket = []; // (bucket => type => paths)
34 // Get locks that need to be acquired (buckets => locks)...
35 foreach ( $pathsByType as $type => $paths ) {
36 foreach ( $paths as $path ) {
37 if ( isset( $this->locksHeld[$path][$type] ) ) {
38 ++$this->locksHeld[$path][$type];
39 } else {
40 $bucket = $this->getBucketFromPath( $path );
41 $pathsByTypeByBucket[$bucket][$type][] = $path;
42 }
43 }
44 }
45
46 // Acquire locks in each bucket in bucket order to reduce contention. Any blocking
47 // mutexes during the acquisition step will not involve circular waiting on buckets.
48 ksort( $pathsByTypeByBucket );
49
50 $lockedPaths = []; // files locked in this attempt (type => paths)
51 // Attempt to acquire these locks...
52 foreach ( $pathsByTypeByBucket as $bucket => $bucketPathsByType ) {
53 // Try to acquire the locks for this bucket
54 $status->merge( $this->doLockingRequestBucket( $bucket, $bucketPathsByType ) );
55 if ( !$status->isOK() ) {
56 $status->merge( $this->doUnlockByType( $lockedPaths ) );
57
58 return $status;
59 }
60 // Record these locks as active
61 foreach ( $bucketPathsByType as $type => $paths ) {
62 foreach ( $paths as $path ) {
63 $this->locksHeld[$path][$type] = 1; // locked
64 // Keep track of what locks were made in this attempt
65 $lockedPaths[$type][] = $path;
66 }
67 }
68 }
69
70 return $status;
71 }
72
80 protected function doUnlockByType( array $pathsByType ) {
81 $status = StatusValue::newGood();
82
83 $pathsByTypeByBucket = []; // (bucket => type => paths)
84 foreach ( $pathsByType as $type => $paths ) {
85 foreach ( $paths as $path ) {
86 if ( !isset( $this->locksHeld[$path][$type] ) ) {
87 $status->warning( 'lockmanager-notlocked', $path );
88 } else {
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 $pathsByTypeByBucket[$bucket][$type][] = $path;
95 }
96 if ( $this->locksHeld[$path] === [] ) {
97 unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
98 }
99 }
100 }
101 }
102
103 // Remove these specific locks if possible, or at least release
104 // all locks once this process is currently not holding any locks.
105 foreach ( $pathsByTypeByBucket as $bucket => $bucketPathsByType ) {
106 $status->merge( $this->doUnlockingRequestBucket( $bucket, $bucketPathsByType ) );
107 }
108 if ( $this->locksHeld === [] ) {
109 $status->merge( $this->releaseAllLocks() );
110 $this->degradedBuckets = []; // safe to retry the normal quorum
111 }
112
113 return $status;
114 }
115
124 final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
125 return $this->collectPledgeQuorum(
126 $bucket,
127 function ( $lockSrv ) use ( $pathsByType ) {
128 return $this->getLocksOnServer( $lockSrv, $pathsByType );
129 }
130 );
131 }
132
140 final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
141 return $this->releasePledges(
142 $bucket,
143 function ( $lockSrv ) use ( $pathsByType ) {
144 return $this->freeLocksOnServer( $lockSrv, $pathsByType );
145 }
146 );
147 }
148
157 final protected function collectPledgeQuorum( $bucket, callable $callback ) {
158 $status = StatusValue::newGood();
159
160 $yesVotes = 0; // locks made on trustable servers
161 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
162 $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
163 // Get votes for each peer, in order, until we have enough...
164 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
165 if ( !$this->isServerUp( $lockSrv ) ) {
166 --$votesLeft;
167 $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
168 $this->degradedBuckets[$bucket] = time();
169 continue; // server down?
170 }
171 // Attempt to acquire the lock on this peer
172 $status->merge( $callback( $lockSrv ) );
173 if ( !$status->isOK() ) {
174 return $status; // vetoed; resource locked
175 }
176 ++$yesVotes; // success for this peer
177 if ( $yesVotes >= $quorum ) {
178 return $status; // lock obtained
179 }
180 --$votesLeft;
181 $votesNeeded = $quorum - $yesVotes;
182 if ( $votesNeeded > $votesLeft ) {
183 break; // short-circuit
184 }
185 }
186 // At this point, we must not have met the quorum
187 $status->setResult( false );
188
189 return $status;
190 }
191
199 final protected function releasePledges( $bucket, callable $callback ) {
200 $status = StatusValue::newGood();
201
202 $yesVotes = 0; // locks freed on trustable servers
203 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
204 $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
205 $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
206 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
207 if ( !$this->isServerUp( $lockSrv ) ) {
208 $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
209 } else {
210 // Attempt to release the lock on this peer
211 $status->merge( $callback( $lockSrv ) );
212 ++$yesVotes; // success for this peer
213 // Normally the first peers form the quorum, and the others are ignored.
214 // Ignore them in this case, but not when an alternative quorum was used.
215 if ( $yesVotes >= $quorum && !$isDegraded ) {
216 break; // lock released
217 }
218 }
219 }
220 // Set a bad StatusValue if the quorum was not met.
221 // Assumes the same "up" servers as during the acquire step.
222 $status->setResult( $yesVotes >= $quorum );
223
224 return $status;
225 }
226
234 protected function getBucketFromPath( $path ) {
235 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
236 return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
237 }
238
246 abstract protected function isServerUp( $lockSrv );
247
255 abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
256
266 abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
267
275 abstract protected function releaseAllLocks();
276
278 final protected function doLock( array $paths, $type ) {
279 // @phan-suppress-previous-line PhanPluginNeverReturnMethod
280 throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
281 }
282
284 final protected function doUnlock( array $paths, $type ) {
285 // @phan-suppress-previous-line PhanPluginNeverReturnMethod
286 throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
287 }
288}
290class_alias( QuorumLockManager::class, 'QuorumLockManager' );
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Resource locking handling.
Base class for lock managers that use a quorum of peer servers for locks.
releasePledges( $bucket, callable $callback)
Attempt to release pledges with the peers for a bucket.
isServerUp( $lockSrv)
Check if a lock server is up.
doUnlockingRequestBucket( $bucket, array $pathsByType)
Attempt to release locks with the peers for a bucket.
doLockingRequestBucket( $bucket, array $pathsByType)
Attempt to acquire locks with the peers for a bucket.
releaseAllLocks()
Release all locks that this session is holding.
freeLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and release locks on $paths.
array $srvsByBucket
Map of bucket indexes to peer server lists.
collectPledgeQuorum( $bucket, callable $callback)
Attempt to acquire pledges with the peers for a bucket.
doLockByType(array $pathsByType)
LockManager::lockByType() to override StatusValue 1.22
array $degradedBuckets
Map of degraded buckets.
getBucketFromPath( $path)
Get the bucket for resource path.
doUnlock(array $paths, $type)
Unlock resources with the given keys and lock type.StatusValue
doLock(array $paths, $type)
Lock resources with the given keys and lock type.StatusValue
getLocksOnServer( $lockSrv, array $pathsByType)
Get a connection to a lock server and acquire locks.