MediaWiki fundraising/REL1_35
FSLockManager.php
Go to the documentation of this file.
1<?php
38 protected $lockTypeMap = [
39 self::LOCK_SH => self::LOCK_SH,
40 self::LOCK_UW => self::LOCK_SH,
41 self::LOCK_EX => self::LOCK_EX
42 ];
43
45 protected $lockDir;
46
48 protected $handles = [];
49
51 protected $isWindows;
52
59 public function __construct( array $config ) {
60 parent::__construct( $config );
61
62 $this->lockDir = $config['lockDirectory'];
63 $this->isWindows = ( PHP_OS_FAMILY === 'Windows' );
64 }
65
72 protected function doLock( array $paths, $type ) {
73 $status = StatusValue::newGood();
74
75 $lockedPaths = []; // files locked in this attempt
76 foreach ( $paths as $path ) {
77 $status->merge( $this->doSingleLock( $path, $type ) );
78 if ( $status->isOK() ) {
79 $lockedPaths[] = $path;
80 } else {
81 // Abort and unlock everything
82 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
83
84 return $status;
85 }
86 }
87
88 return $status;
89 }
90
97 protected function doUnlock( array $paths, $type ) {
98 $status = StatusValue::newGood();
99
100 foreach ( $paths as $path ) {
101 $status->merge( $this->doSingleUnlock( $path, $type ) );
102 }
103
104 return $status;
105 }
106
114 protected function doSingleLock( $path, $type ) {
115 $status = StatusValue::newGood();
116
117 if ( isset( $this->locksHeld[$path][$type] ) ) {
118 ++$this->locksHeld[$path][$type];
119 } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
120 $this->locksHeld[$path][$type] = 1;
121 } else {
122 if ( isset( $this->handles[$path] ) ) {
123 $handle = $this->handles[$path];
124 } else {
125 Wikimedia\suppressWarnings();
126 $handle = fopen( $this->getLockPath( $path ), 'a+' );
127 if ( !$handle && !is_dir( $this->lockDir ) ) {
128 // Create the lock directory in case it is missing
129 if ( mkdir( $this->lockDir, 0777, true ) ) {
130 $handle = fopen( $this->getLockPath( $path ), 'a+' ); // try again
131 } else {
132 $this->logger->error( "Cannot create directory '{$this->lockDir}'." );
133 }
134 }
135 Wikimedia\restoreWarnings();
136 }
137 if ( $handle ) {
138 // Either a shared or exclusive lock
139 $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX;
140 if ( flock( $handle, $lock | LOCK_NB ) ) {
141 // Record this lock as active
142 $this->locksHeld[$path][$type] = 1;
143 $this->handles[$path] = $handle;
144 } else {
145 fclose( $handle );
146 $status->fatal( 'lockmanager-fail-acquirelock', $path );
147 }
148 } else {
149 $status->fatal( 'lockmanager-fail-openlock', $path );
150 }
151 }
152
153 return $status;
154 }
155
163 protected function doSingleUnlock( $path, $type ) {
164 $status = StatusValue::newGood();
165
166 if ( !isset( $this->locksHeld[$path] ) ) {
167 $status->warning( 'lockmanager-notlocked', $path );
168 } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
169 $status->warning( 'lockmanager-notlocked', $path );
170 } else {
171 $handlesToClose = [];
172 --$this->locksHeld[$path][$type];
173 if ( $this->locksHeld[$path][$type] <= 0 ) {
174 unset( $this->locksHeld[$path][$type] );
175 }
176 if ( $this->locksHeld[$path] === [] ) {
177 unset( $this->locksHeld[$path] ); // no locks on this path
178 if ( isset( $this->handles[$path] ) ) {
179 $handlesToClose[] = $this->handles[$path];
180 unset( $this->handles[$path] );
181 }
182 }
183 // Unlock handles to release locks and delete
184 // any lock files that end up with no locks on them...
185 if ( $this->isWindows ) {
186 // Windows: for any process, including this one,
187 // calling unlink() on a locked file will fail
188 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
189 $status->merge( $this->pruneKeyLockFiles( $path ) );
190 } else {
191 // Unix: unlink() can be used on files currently open by this
192 // process and we must do so in order to avoid race conditions
193 $status->merge( $this->pruneKeyLockFiles( $path ) );
194 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
195 }
196 }
197
198 return $status;
199 }
200
206 private function closeLockHandles( $path, array $handlesToClose ) {
207 $status = StatusValue::newGood();
208 foreach ( $handlesToClose as $handle ) {
209 if ( !flock( $handle, LOCK_UN ) ) {
210 $status->fatal( 'lockmanager-fail-releaselock', $path );
211 }
212 if ( !fclose( $handle ) ) {
213 $status->warning( 'lockmanager-fail-closelock', $path );
214 }
215 }
216
217 return $status;
218 }
219
224 private function pruneKeyLockFiles( $path ) {
225 $status = StatusValue::newGood();
226 if ( !isset( $this->locksHeld[$path] ) ) {
227 # No locks are held for the lock file anymore
228 if ( !unlink( $this->getLockPath( $path ) ) ) {
229 $status->warning( 'lockmanager-fail-deletelock', $path );
230 }
231 unset( $this->handles[$path] );
232 }
233
234 return $status;
235 }
236
242 protected function getLockPath( $path ) {
243 return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
244 }
245
249 public function __destruct() {
250 while ( count( $this->locksHeld ) ) {
251 foreach ( $this->locksHeld as $path => $locks ) {
252 $this->doSingleUnlock( $path, self::LOCK_EX );
253 $this->doSingleUnlock( $path, self::LOCK_SH );
254 }
255 }
256 }
257}
Simple version of LockManager based on using FS lock files.
doSingleLock( $path, $type)
Lock a single resource key.
pruneKeyLockFiles( $path)
string $lockDir
Global dir for all servers.
__destruct()
Make sure remaining locks get cleared for sanity.
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.
closeLockHandles( $path, array $handlesToClose)
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.