MediaWiki master
FSLockManager.php
Go to the documentation of this file.
1<?php
36 protected $lockTypeMap = [
37 self::LOCK_SH => self::LOCK_SH,
38 self::LOCK_UW => self::LOCK_SH,
39 self::LOCK_EX => self::LOCK_EX
40 ];
41
43 protected $lockDir;
44
46 protected $handles = [];
47
49 protected $isWindows;
50
57 public function __construct( array $config ) {
58 parent::__construct( $config );
59
60 $this->lockDir = $config['lockDirectory'];
61 $this->isWindows = ( PHP_OS_FAMILY === 'Windows' );
62 }
63
70 protected function doLock( array $paths, $type ) {
71 $status = StatusValue::newGood();
72
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 ) );
81
82 return $status;
83 }
84 }
85
86 return $status;
87 }
88
95 protected function doUnlock( array $paths, $type ) {
96 $status = StatusValue::newGood();
97
98 foreach ( $paths as $path ) {
99 $status->merge( $this->doSingleUnlock( $path, $type ) );
100 }
101
102 return $status;
103 }
104
112 protected function doSingleLock( $path, $type ) {
113 $status = StatusValue::newGood();
114
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}'." );
133 }
134 }
135 }
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' );
146 }
147 } else {
148 $status->fatal( 'lockmanager-fail-openlock', $path );
149 }
150 }
151
152 return $status;
153 }
154
162 protected function doSingleUnlock( $path, $type ) {
163 $status = StatusValue::newGood();
164
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] );
174 }
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] );
180 }
181 }
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 ) );
194 }
195 }
196
197 return $status;
198 }
199
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 );
210 }
211 if ( !fclose( $handle ) ) {
212 $status->warning( 'lockmanager-fail-closelock', $path );
213 }
214 }
215
216 return $status;
217 }
218
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 );
229 }
230 unset( $this->handles[$path] );
231 }
232
233 return $status;
234 }
235
241 protected function getLockPath( $path ) {
242 return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
243 }
244
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 );
253 }
254 }
255 }
256}
Simple lock management based on server-local temporary files.
doSingleLock( $path, $type)
Lock a single resource key.
string $lockDir
Global dir for all servers.
__destruct()
Make sure remaining locks get cleared.
doLock(array $paths, $type)
array $handles
Map of (locked key => lock file handle)
doSingleUnlock( $path, $type)
Unlock a single resource key.
__construct(array $config)
Construct a new instance from configuration.
getLockPath( $path)
Get the path to the lock file for a key.
array $lockTypeMap
Mapping of lock types to the type actually used.
doUnlock(array $paths, $type)
Resource locking handling.
const LOCK_SH
Lock types; stronger locks have higher values.
static newGood( $value=null)
Factory function for good results.