Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / libs / lockmanager / FSLockManager.php
blobfff895a903f7d43c8bcf492565e8d83380b2d67a
1 <?php
2 /**
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
18 * @file
21 /**
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
32 * @since 1.19
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 */
43 protected $lockDir;
45 /** @var array Map of (locked key => lock file handle) */
46 protected $handles = [];
48 /** @var bool */
49 protected $isWindows;
51 /**
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' );
64 /**
65 * @see LockManager::doLock()
66 * @param array $paths
67 * @param int $type
68 * @return StatusValue
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;
78 } else {
79 // Abort and unlock everything
80 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
82 return $status;
86 return $status;
89 /**
90 * @see LockManager::doUnlock()
91 * @param array $paths
92 * @param int $type
93 * @return StatusValue
95 protected function doUnlock( array $paths, $type ) {
96 $status = StatusValue::newGood();
98 foreach ( $paths as $path ) {
99 $status->merge( $this->doSingleUnlock( $path, $type ) );
102 return $status;
106 * Lock a single resource key
108 * @param string $path
109 * @param int $type
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;
119 } else {
120 if ( isset( $this->handles[$path] ) ) {
121 $handle = $this->handles[$path];
122 } else {
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+' );
131 } else {
132 $this->logger->error( "Cannot create directory '{$this->lockDir}'." );
136 if ( $handle ) {
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;
143 } else {
144 fclose( $handle );
145 $status->fatal( 'lockmanager-fail-conflict' );
147 } else {
148 $status->fatal( 'lockmanager-fail-openlock', $path );
152 return $status;
156 * Unlock a single resource key
158 * @param string $path
159 * @param int $type
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 );
169 } else {
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 ) );
189 } else {
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 ) );
197 return $status;
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 );
216 return $status;
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] );
233 return $status;
237 * Get the path to the lock file for a key
238 * @param string $path
239 * @return string
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 );