MediaWiki  master
FSLockManager.php
Go to the documentation of this file.
1 <?php
34 class FSLockManager extends LockManager {
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.
Definition: LockManager.php:47
const LOCK_SH
Lock types; stronger locks have higher values.
Definition: LockManager.php:68
const LOCK_EX
Definition: LockManager.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85