]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/libs/lockmanager/QuorumLockManager.php
MediaWiki 1.30.2
[autoinstalls/mediawiki.git] / includes / libs / lockmanager / QuorumLockManager.php
1 <?php
2 /**
3  * Version of LockManager that uses a quorum from peer servers for locks.
4  *
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.
9  *
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.
14  *
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
19  *
20  * @file
21  * @ingroup LockManager
22  */
23
24 /**
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.
27  *
28  * @ingroup LockManager
29  * @since 1.20
30  */
31 abstract class QuorumLockManager extends LockManager {
32         /** @var array Map of bucket indexes to peer server lists */
33         protected $srvsByBucket = []; // (bucket index => (lsrv1, lsrv2, ...))
34
35         /** @var array Map of degraded buckets */
36         protected $degradedBuckets = []; // (bucket index => UNIX timestamp)
37
38         final protected function doLock( array $paths, $type ) {
39                 return $this->doLockByType( [ $type => $paths ] );
40         }
41
42         final protected function doUnlock( array $paths, $type ) {
43                 return $this->doUnlockByType( [ $type => $paths ] );
44         }
45
46         protected function doLockByType( array $pathsByType ) {
47                 $status = StatusValue::newGood();
48
49                 $pathsToLock = []; // (bucket => type => paths)
50                 // Get locks that need to be acquired (buckets => locks)...
51                 foreach ( $pathsByType as $type => $paths ) {
52                         foreach ( $paths as $path ) {
53                                 if ( isset( $this->locksHeld[$path][$type] ) ) {
54                                         ++$this->locksHeld[$path][$type];
55                                 } else {
56                                         $bucket = $this->getBucketFromPath( $path );
57                                         $pathsToLock[$bucket][$type][] = $path;
58                                 }
59                         }
60                 }
61
62                 $lockedPaths = []; // files locked in this attempt (type => paths)
63                 // Attempt to acquire these locks...
64                 foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
65                         // Try to acquire the locks for this bucket
66                         $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
67                         if ( !$status->isOK() ) {
68                                 $status->merge( $this->doUnlockByType( $lockedPaths ) );
69
70                                 return $status;
71                         }
72                         // Record these locks as active
73                         foreach ( $pathsToLockByType as $type => $paths ) {
74                                 foreach ( $paths as $path ) {
75                                         $this->locksHeld[$path][$type] = 1; // locked
76                                         // Keep track of what locks were made in this attempt
77                                         $lockedPaths[$type][] = $path;
78                                 }
79                         }
80                 }
81
82                 return $status;
83         }
84
85         protected function doUnlockByType( array $pathsByType ) {
86                 $status = StatusValue::newGood();
87
88                 $pathsToUnlock = []; // (bucket => type => paths)
89                 foreach ( $pathsByType as $type => $paths ) {
90                         foreach ( $paths as $path ) {
91                                 if ( !isset( $this->locksHeld[$path][$type] ) ) {
92                                         $status->warning( 'lockmanager-notlocked', $path );
93                                 } else {
94                                         --$this->locksHeld[$path][$type];
95                                         // Reference count the locks held and release locks when zero
96                                         if ( $this->locksHeld[$path][$type] <= 0 ) {
97                                                 unset( $this->locksHeld[$path][$type] );
98                                                 $bucket = $this->getBucketFromPath( $path );
99                                                 $pathsToUnlock[$bucket][$type][] = $path;
100                                         }
101                                         if ( !count( $this->locksHeld[$path] ) ) {
102                                                 unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
103                                         }
104                                 }
105                         }
106                 }
107
108                 // Remove these specific locks if possible, or at least release
109                 // all locks once this process is currently not holding any locks.
110                 foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
111                         $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
112                 }
113                 if ( !count( $this->locksHeld ) ) {
114                         $status->merge( $this->releaseAllLocks() );
115                         $this->degradedBuckets = []; // safe to retry the normal quorum
116                 }
117
118                 return $status;
119         }
120
121         /**
122          * Attempt to acquire locks with the peers for a bucket.
123          * This is all or nothing; if any key is locked then this totally fails.
124          *
125          * @param int $bucket
126          * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
127          * @return StatusValue
128          */
129         final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
130                 return $this->collectPledgeQuorum(
131                         $bucket,
132                         function ( $lockSrv ) use ( $pathsByType ) {
133                                 return $this->getLocksOnServer( $lockSrv, $pathsByType );
134                         }
135                 );
136         }
137
138         /**
139          * Attempt to release locks with the peers for a bucket
140          *
141          * @param int $bucket
142          * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
143          * @return StatusValue
144          */
145         final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
146                 return $this->releasePledges(
147                         $bucket,
148                         function ( $lockSrv ) use ( $pathsByType ) {
149                                 return $this->freeLocksOnServer( $lockSrv, $pathsByType );
150                         }
151                 );
152         }
153
154         /**
155          * Attempt to acquire pledges with the peers for a bucket.
156          * This is all or nothing; if any key is already pledged then this totally fails.
157          *
158          * @param int $bucket
159          * @param callable $callback Pledge method taking a server name and yeilding a StatusValue
160          * @return StatusValue
161          */
162         final protected function collectPledgeQuorum( $bucket, callable $callback ) {
163                 $status = StatusValue::newGood();
164
165                 $yesVotes = 0; // locks made on trustable servers
166                 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
167                 $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
168                 // Get votes for each peer, in order, until we have enough...
169                 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
170                         if ( !$this->isServerUp( $lockSrv ) ) {
171                                 --$votesLeft;
172                                 $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
173                                 $this->degradedBuckets[$bucket] = time();
174                                 continue; // server down?
175                         }
176                         // Attempt to acquire the lock on this peer
177                         $status->merge( $callback( $lockSrv ) );
178                         if ( !$status->isOK() ) {
179                                 return $status; // vetoed; resource locked
180                         }
181                         ++$yesVotes; // success for this peer
182                         if ( $yesVotes >= $quorum ) {
183                                 return $status; // lock obtained
184                         }
185                         --$votesLeft;
186                         $votesNeeded = $quorum - $yesVotes;
187                         if ( $votesNeeded > $votesLeft ) {
188                                 break; // short-circuit
189                         }
190                 }
191                 // At this point, we must not have met the quorum
192                 $status->setResult( false );
193
194                 return $status;
195         }
196
197         /**
198          * Attempt to release pledges with the peers for a bucket
199          *
200          * @param int $bucket
201          * @param callable $callback Pledge method taking a server name and yeilding a StatusValue
202          * @return StatusValue
203          */
204         final protected function releasePledges( $bucket, callable $callback ) {
205                 $status = StatusValue::newGood();
206
207                 $yesVotes = 0; // locks freed on trustable servers
208                 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
209                 $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
210                 $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
211                 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
212                         if ( !$this->isServerUp( $lockSrv ) ) {
213                                 $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
214                         } else {
215                                 // Attempt to release the lock on this peer
216                                 $status->merge( $callback( $lockSrv ) );
217                                 ++$yesVotes; // success for this peer
218                                 // Normally the first peers form the quorum, and the others are ignored.
219                                 // Ignore them in this case, but not when an alternative quorum was used.
220                                 if ( $yesVotes >= $quorum && !$isDegraded ) {
221                                         break; // lock released
222                                 }
223                         }
224                 }
225                 // Set a bad StatusValue if the quorum was not met.
226                 // Assumes the same "up" servers as during the acquire step.
227                 $status->setResult( $yesVotes >= $quorum );
228
229                 return $status;
230         }
231
232         /**
233          * Get the bucket for resource path.
234          * This should avoid throwing any exceptions.
235          *
236          * @param string $path
237          * @return int
238          */
239         protected function getBucketFromPath( $path ) {
240                 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
241                 return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
242         }
243
244         /**
245          * Check if a lock server is up.
246          * This should process cache results to reduce RTT.
247          *
248          * @param string $lockSrv
249          * @return bool
250          */
251         abstract protected function isServerUp( $lockSrv );
252
253         /**
254          * Get a connection to a lock server and acquire locks
255          *
256          * @param string $lockSrv
257          * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
258          * @return StatusValue
259          */
260         abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
261
262         /**
263          * Get a connection to a lock server and release locks on $paths.
264          *
265          * Subclasses must effectively implement this or releaseAllLocks().
266          *
267          * @param string $lockSrv
268          * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
269          * @return StatusValue
270          */
271         abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
272
273         /**
274          * Release all locks that this session is holding.
275          *
276          * Subclasses must effectively implement this or freeLocksOnServer().
277          *
278          * @return StatusValue
279          */
280         abstract protected function releaseAllLocks();
281 }