3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
22 * Simple lock management based on server-local temporary files.
24 * All locks are non-blocking, which avoids deadlocks.
26 * This should work fine for small sites running from a single web server.
27 * Do not use this with 'lockDirectory' set to an NFS mount unless the
28 * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
29 * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
31 * @ingroup LockManager
34 class FSLockManager
extends LockManager
{
35 /** @var array Mapping of lock types to the type actually used */
36 protected $lockTypeMap = [
37 self
::LOCK_SH
=> self
::LOCK_SH
,
38 self
::LOCK_UW
=> self
::LOCK_SH
,
39 self
::LOCK_EX
=> self
::LOCK_EX
42 /** @var string Global dir for all servers */
45 /** @var array Map of (locked key => lock file handle) */
46 protected $handles = [];
52 * Construct a new instance from configuration.
54 * @param array $config Includes:
55 * - lockDirectory : Directory containing the lock files
57 public function __construct( array $config ) {
58 parent
::__construct( $config );
60 $this->lockDir
= $config['lockDirectory'];
61 $this->isWindows
= ( PHP_OS_FAMILY
=== 'Windows' );
65 * @see LockManager::doLock()
70 protected function doLock( array $paths, $type ) {
71 $status = StatusValue
::newGood();
73 $lockedPaths = []; // files locked in this attempt
74 foreach ( $paths as $path ) {
75 $status->merge( $this->doSingleLock( $path, $type ) );
76 if ( $status->isOK() ) {
77 $lockedPaths[] = $path;
79 // Abort and unlock everything
80 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
90 * @see LockManager::doUnlock()
95 protected function doUnlock( array $paths, $type ) {
96 $status = StatusValue
::newGood();
98 foreach ( $paths as $path ) {
99 $status->merge( $this->doSingleUnlock( $path, $type ) );
106 * Lock a single resource key
108 * @param string $path
110 * @return StatusValue
112 protected function doSingleLock( $path, $type ) {
113 $status = StatusValue
::newGood();
115 if ( isset( $this->locksHeld
[$path][$type] ) ) {
116 ++
$this->locksHeld
[$path][$type];
117 } elseif ( isset( $this->locksHeld
[$path][self
::LOCK_EX
] ) ) {
118 $this->locksHeld
[$path][$type] = 1;
120 if ( isset( $this->handles
[$path] ) ) {
121 $handle = $this->handles
[$path];
123 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
124 $handle = @fopen
( $this->getLockPath( $path ), 'a+' );
125 if ( !$handle && !is_dir( $this->lockDir
) ) {
126 // Create the lock directory and try again
127 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
128 if ( @mkdir
( $this->lockDir
, 0777, true ) ) {
129 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
130 $handle = @fopen
( $this->getLockPath( $path ), 'a+' );
132 $this->logger
->error( "Cannot create directory '{$this->lockDir}'." );
137 // Either a shared or exclusive lock
138 $lock = ( $type == self
::LOCK_SH
) ? LOCK_SH
: LOCK_EX
;
139 if ( flock( $handle, $lock | LOCK_NB
) ) {
140 // Record this lock as active
141 $this->locksHeld
[$path][$type] = 1;
142 $this->handles
[$path] = $handle;
145 $status->fatal( 'lockmanager-fail-conflict' );
148 $status->fatal( 'lockmanager-fail-openlock', $path );
156 * Unlock a single resource key
158 * @param string $path
160 * @return StatusValue
162 protected function doSingleUnlock( $path, $type ) {
163 $status = StatusValue
::newGood();
165 if ( !isset( $this->locksHeld
[$path] ) ) {
166 $status->warning( 'lockmanager-notlocked', $path );
167 } elseif ( !isset( $this->locksHeld
[$path][$type] ) ) {
168 $status->warning( 'lockmanager-notlocked', $path );
170 $handlesToClose = [];
171 --$this->locksHeld
[$path][$type];
172 if ( $this->locksHeld
[$path][$type] <= 0 ) {
173 unset( $this->locksHeld
[$path][$type] );
175 if ( $this->locksHeld
[$path] === [] ) {
176 unset( $this->locksHeld
[$path] ); // no locks on this path
177 if ( isset( $this->handles
[$path] ) ) {
178 $handlesToClose[] = $this->handles
[$path];
179 unset( $this->handles
[$path] );
182 // Unlock handles to release locks and delete
183 // any lock files that end up with no locks on them...
184 if ( $this->isWindows
) {
185 // Windows: for any process, including this one,
186 // calling unlink() on a locked file will fail
187 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
188 $status->merge( $this->pruneKeyLockFiles( $path ) );
190 // Unix: unlink() can be used on files currently open by this
191 // process and we must do so in order to avoid race conditions
192 $status->merge( $this->pruneKeyLockFiles( $path ) );
193 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
201 * @param string $path
202 * @param array $handlesToClose
203 * @return StatusValue
205 private function closeLockHandles( $path, array $handlesToClose ) {
206 $status = StatusValue
::newGood();
207 foreach ( $handlesToClose as $handle ) {
208 if ( !flock( $handle, LOCK_UN
) ) {
209 $status->fatal( 'lockmanager-fail-releaselock', $path );
211 if ( !fclose( $handle ) ) {
212 $status->warning( 'lockmanager-fail-closelock', $path );
220 * @param string $path
221 * @return StatusValue
223 private function pruneKeyLockFiles( $path ) {
224 $status = StatusValue
::newGood();
225 if ( !isset( $this->locksHeld
[$path] ) ) {
226 # No locks are held for the lock file anymore
227 if ( !unlink( $this->getLockPath( $path ) ) ) {
228 $status->warning( 'lockmanager-fail-deletelock', $path );
230 unset( $this->handles
[$path] );
237 * Get the path to the lock file for a key
238 * @param string $path
241 protected function getLockPath( $path ) {
242 return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
246 * Make sure remaining locks get cleared
248 public function __destruct() {
249 while ( count( $this->locksHeld
) ) {
250 foreach ( $this->locksHeld
as $path => $locks ) {
251 $this->doSingleUnlock( $path, self
::LOCK_EX
);
252 $this->doSingleUnlock( $path, self
::LOCK_SH
);