[LockManager] Renamed getBucketFromKey() -> getBucketFromPath().
[lhc/web/wiklou.git] / includes / filebackend / 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 = array(); // (bucket index => (lsrv1, lsrv2, ...))
34
35 /**
36 * @see LockManager::doLock()
37 * @param $paths array
38 * @param $type int
39 * @return Status
40 */
41 final protected function doLock( array $paths, $type ) {
42 $status = Status::newGood();
43
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];
49 } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
50 $this->locksHeld[$path][$type] = 1;
51 } else {
52 $bucket = $this->getBucketFromPath( $path );
53 $pathsToLock[$bucket][] = $path;
54 }
55 }
56
57 $lockedPaths = array(); // files locked in this attempt
58 // Attempt to acquire these locks...
59 foreach ( $pathsToLock as $bucket => $paths ) {
60 // Try to acquire the locks for this bucket
61 $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) );
62 if ( !$status->isOK() ) {
63 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
64 return $status;
65 }
66 // Record these locks as active
67 foreach ( $paths as $path ) {
68 $this->locksHeld[$path][$type] = 1; // locked
69 }
70 // Keep track of what locks were made in this attempt
71 $lockedPaths = array_merge( $lockedPaths, $paths );
72 }
73
74 return $status;
75 }
76
77 /**
78 * @see LockManager::doUnlock()
79 * @param $paths array
80 * @param $type int
81 * @return Status
82 */
83 final protected function doUnlock( array $paths, $type ) {
84 $status = Status::newGood();
85
86 $pathsToUnlock = array();
87 foreach ( $paths as $path ) {
88 if ( !isset( $this->locksHeld[$path][$type] ) ) {
89 $status->warning( 'lockmanager-notlocked', $path );
90 } else {
91 --$this->locksHeld[$path][$type];
92 // Reference count the locks held and release locks when zero
93 if ( $this->locksHeld[$path][$type] <= 0 ) {
94 unset( $this->locksHeld[$path][$type] );
95 $bucket = $this->getBucketFromPath( $path );
96 $pathsToUnlock[$bucket][] = $path;
97 }
98 if ( !count( $this->locksHeld[$path] ) ) {
99 unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
100 }
101 }
102 }
103
104 // Remove these specific locks if possible, or at least release
105 // all locks once this process is currently not holding any locks.
106 foreach ( $pathsToUnlock as $bucket => $paths ) {
107 $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) );
108 }
109 if ( !count( $this->locksHeld ) ) {
110 $status->merge( $this->releaseAllLocks() );
111 }
112
113 return $status;
114 }
115
116 /**
117 * Attempt to acquire locks with the peers for a bucket.
118 * This is all or nothing; if any key is locked then this totally fails.
119 *
120 * @param $bucket integer
121 * @param $paths Array List of resource keys to lock
122 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
123 * @return Status
124 */
125 final protected function doLockingRequestBucket( $bucket, array $paths, $type ) {
126 $status = Status::newGood();
127
128 $yesVotes = 0; // locks made on trustable servers
129 $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
130 $quorum = floor( $votesLeft/2 + 1 ); // simple majority
131 // Get votes for each peer, in order, until we have enough...
132 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
133 if ( !$this->isServerUp( $lockSrv ) ) {
134 --$votesLeft;
135 $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
136 continue; // server down?
137 }
138 // Attempt to acquire the lock on this peer
139 $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) );
140 if ( !$status->isOK() ) {
141 return $status; // vetoed; resource locked
142 }
143 ++$yesVotes; // success for this peer
144 if ( $yesVotes >= $quorum ) {
145 return $status; // lock obtained
146 }
147 --$votesLeft;
148 $votesNeeded = $quorum - $yesVotes;
149 if ( $votesNeeded > $votesLeft ) {
150 break; // short-circuit
151 }
152 }
153 // At this point, we must not have met the quorum
154 $status->setResult( false );
155
156 return $status;
157 }
158
159 /**
160 * Attempt to release locks with the peers for a bucket
161 *
162 * @param $bucket integer
163 * @param $paths Array List of resource keys to lock
164 * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH
165 * @return Status
166 */
167 final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) {
168 $status = Status::newGood();
169
170 foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
171 if ( !$this->isServerUp( $lockSrv ) ) {
172 $status->fatal( 'lockmanager-fail-svr-release', $lockSrv );
173 // Attempt to release the lock on this peer
174 } else {
175 $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) );
176 }
177 }
178
179 return $status;
180 }
181
182 /**
183 * Get the bucket for resource path.
184 * This should avoid throwing any exceptions.
185 *
186 * @param $path string
187 * @return integer
188 */
189 protected function getBucketFromPath( $path ) {
190 $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
191 return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
192 }
193
194 /**
195 * Check if a lock server is up
196 *
197 * @param $lockSrv string
198 * @return bool
199 */
200 abstract protected function isServerUp( $lockSrv );
201
202 /**
203 * Get a connection to a lock server and acquire locks on $paths
204 *
205 * @param $lockSrv string
206 * @param $paths array
207 * @param $type integer
208 * @return Status
209 */
210 abstract protected function getLocksOnServer( $lockSrv, array $paths, $type );
211
212 /**
213 * Get a connection to a lock server and release locks on $paths.
214 *
215 * Subclasses must effectively implement this or releaseAllLocks().
216 *
217 * @param $lockSrv string
218 * @param $paths array
219 * @param $type integer
220 * @return Status
221 */
222 abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type );
223
224 /**
225 * Release all locks that this session is holding.
226 *
227 * Subclasses must effectively implement this or freeLocksOnServer().
228 *
229 * @return Status
230 */
231 abstract protected function releaseAllLocks();
232 }