MediaWiki REL1_39
FSLockManager.php
Go to the documentation of this file.
1<?php
24use Wikimedia\AtEase\AtEase;
25
40 protected $lockTypeMap = [
41 self::LOCK_SH => self::LOCK_SH,
42 self::LOCK_UW => self::LOCK_SH,
43 self::LOCK_EX => self::LOCK_EX
44 ];
45
47 protected $lockDir;
48
50 protected $handles = [];
51
53 protected $isWindows;
54
61 public function __construct( array $config ) {
62 parent::__construct( $config );
63
64 $this->lockDir = $config['lockDirectory'];
65 $this->isWindows = ( PHP_OS_FAMILY === 'Windows' );
66 }
67
74 protected function doLock( array $paths, $type ) {
75 $status = StatusValue::newGood();
76
77 $lockedPaths = []; // files locked in this attempt
78 foreach ( $paths as $path ) {
79 $status->merge( $this->doSingleLock( $path, $type ) );
80 if ( $status->isOK() ) {
81 $lockedPaths[] = $path;
82 } else {
83 // Abort and unlock everything
84 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
85
86 return $status;
87 }
88 }
89
90 return $status;
91 }
92
99 protected function doUnlock( array $paths, $type ) {
100 $status = StatusValue::newGood();
101
102 foreach ( $paths as $path ) {
103 $status->merge( $this->doSingleUnlock( $path, $type ) );
104 }
105
106 return $status;
107 }
108
116 protected function doSingleLock( $path, $type ) {
117 $status = StatusValue::newGood();
118
119 if ( isset( $this->locksHeld[$path][$type] ) ) {
120 ++$this->locksHeld[$path][$type];
121 } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
122 $this->locksHeld[$path][$type] = 1;
123 } else {
124 if ( isset( $this->handles[$path] ) ) {
125 $handle = $this->handles[$path];
126 } else {
127 AtEase::suppressWarnings();
128 $handle = fopen( $this->getLockPath( $path ), 'a+' );
129 if ( !$handle && !is_dir( $this->lockDir ) ) {
130 // Create the lock directory in case it is missing
131 if ( mkdir( $this->lockDir, 0777, true ) ) {
132 $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
133 } else {
134 $this->logger->error( "Cannot create directory '{$this->lockDir}'." );
135 }
136 }
137 AtEase::restoreWarnings();
138 }
139 if ( $handle ) {
140 // Either a shared or exclusive lock
141 $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX;
142 if ( flock( $handle, $lock | LOCK_NB ) ) {
143 // Record this lock as active
144 $this->locksHeld[$path][$type] = 1;
145 $this->handles[$path] = $handle;
146 } else {
147 fclose( $handle );
148 $status->fatal( 'lockmanager-fail-conflict' );
149 }
150 } else {
151 $status->fatal( 'lockmanager-fail-openlock', $path );
152 }
153 }
154
155 return $status;
156 }
157
165 protected function doSingleUnlock( $path, $type ) {
166 $status = StatusValue::newGood();
167
168 if ( !isset( $this->locksHeld[$path] ) ) {
169 $status->warning( 'lockmanager-notlocked', $path );
170 } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
171 $status->warning( 'lockmanager-notlocked', $path );
172 } else {
173 $handlesToClose = [];
174 --$this->locksHeld[$path][$type];
175 if ( $this->locksHeld[$path][$type] <= 0 ) {
176 unset( $this->locksHeld[$path][$type] );
177 }
178 if ( $this->locksHeld[$path] === [] ) {
179 unset( $this->locksHeld[$path] ); // no locks on this path
180 if ( isset( $this->handles[$path] ) ) {
181 $handlesToClose[] = $this->handles[$path];
182 unset( $this->handles[$path] );
183 }
184 }
185 // Unlock handles to release locks and delete
186 // any lock files that end up with no locks on them...
187 if ( $this->isWindows ) {
188 // Windows: for any process, including this one,
189 // calling unlink() on a locked file will fail
190 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
191 $status->merge( $this->pruneKeyLockFiles( $path ) );
192 } else {
193 // Unix: unlink() can be used on files currently open by this
194 // process and we must do so in order to avoid race conditions
195 $status->merge( $this->pruneKeyLockFiles( $path ) );
196 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
197 }
198 }
199
200 return $status;
201 }
202
208 private function closeLockHandles( $path, array $handlesToClose ) {
209 $status = StatusValue::newGood();
210 foreach ( $handlesToClose as $handle ) {
211 if ( !flock( $handle, LOCK_UN ) ) {
212 $status->fatal( 'lockmanager-fail-releaselock', $path );
213 }
214 if ( !fclose( $handle ) ) {
215 $status->warning( 'lockmanager-fail-closelock', $path );
216 }
217 }
218
219 return $status;
220 }
221
226 private function pruneKeyLockFiles( $path ) {
227 $status = StatusValue::newGood();
228 if ( !isset( $this->locksHeld[$path] ) ) {
229 # No locks are held for the lock file anymore
230 if ( !unlink( $this->getLockPath( $path ) ) ) {
231 $status->warning( 'lockmanager-fail-deletelock', $path );
232 }
233 unset( $this->handles[$path] );
234 }
235
236 return $status;
237 }
238
244 protected function getLockPath( $path ) {
245 return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
246 }
247
251 public function __destruct() {
252 while ( count( $this->locksHeld ) ) {
253 foreach ( $this->locksHeld as $path => $locks ) {
254 $this->doSingleUnlock( $path, self::LOCK_EX );
255 $this->doSingleUnlock( $path, self::LOCK_SH );
256 }
257 }
258 }
259}
Simple version of LockManager based on using FS lock 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)
Class for handling resource locking.
const LOCK_SH
Lock types; stronger locks have higher values.
static newGood( $value=null)
Factory function for good results.