MediaWiki master
FSLockManager.php
Go to the documentation of this file.
1<?php
7
9
25 protected $lockTypeMap = [
26 self::LOCK_SH => self::LOCK_SH,
27 self::LOCK_UW => self::LOCK_SH,
28 self::LOCK_EX => self::LOCK_EX
29 ];
30
32 protected $lockDir;
33
35 protected $handles = [];
36
38 protected $isWindows;
39
46 public function __construct( array $config ) {
47 parent::__construct( $config );
48
49 $this->lockDir = $config['lockDirectory'];
50 $this->isWindows = ( PHP_OS_FAMILY === 'Windows' );
51 }
52
59 protected function doLock( array $paths, $type ) {
60 $status = StatusValue::newGood();
61
62 $lockedPaths = []; // files locked in this attempt
63 foreach ( $paths as $path ) {
64 $status->merge( $this->doSingleLock( $path, $type ) );
65 if ( $status->isOK() ) {
66 $lockedPaths[] = $path;
67 } else {
68 // Abort and unlock everything
69 $status->merge( $this->doUnlock( $lockedPaths, $type ) );
70
71 return $status;
72 }
73 }
74
75 return $status;
76 }
77
84 protected function doUnlock( array $paths, $type ) {
85 $status = StatusValue::newGood();
86
87 foreach ( $paths as $path ) {
88 $status->merge( $this->doSingleUnlock( $path, $type ) );
89 }
90
91 return $status;
92 }
93
101 protected function doSingleLock( $path, $type ) {
102 $status = StatusValue::newGood();
103
104 if ( isset( $this->locksHeld[$path][$type] ) ) {
105 ++$this->locksHeld[$path][$type];
106 } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) {
107 $this->locksHeld[$path][$type] = 1;
108 } else {
109 if ( isset( $this->handles[$path] ) ) {
110 $handle = $this->handles[$path];
111 } else {
112 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
113 $handle = @fopen( $this->getLockPath( $path ), 'a+' );
114 if ( !$handle && !is_dir( $this->lockDir ) ) {
115 // Create the lock directory and try again
116 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
117 if ( @mkdir( $this->lockDir, 0o777, true ) ) {
118 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
119 $handle = @fopen( $this->getLockPath( $path ), 'a+' );
120 } else {
121 $this->logger->error( "Cannot create directory '{$this->lockDir}'." );
122 }
123 }
124 }
125 if ( $handle ) {
126 // Either a shared or exclusive lock
127 $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX;
128 if ( flock( $handle, $lock | LOCK_NB ) ) {
129 // Record this lock as active
130 $this->locksHeld[$path][$type] = 1;
131 $this->handles[$path] = $handle;
132 } else {
133 fclose( $handle );
134 $status->fatal( 'lockmanager-fail-conflict' );
135 }
136 } else {
137 $status->fatal( 'lockmanager-fail-openlock', $path );
138 }
139 }
140
141 return $status;
142 }
143
151 protected function doSingleUnlock( $path, $type ) {
152 $status = StatusValue::newGood();
153
154 if ( !isset( $this->locksHeld[$path] ) ) {
155 $status->warning( 'lockmanager-notlocked', $path );
156 } elseif ( !isset( $this->locksHeld[$path][$type] ) ) {
157 $status->warning( 'lockmanager-notlocked', $path );
158 } else {
159 $handlesToClose = [];
160 --$this->locksHeld[$path][$type];
161 if ( $this->locksHeld[$path][$type] <= 0 ) {
162 unset( $this->locksHeld[$path][$type] );
163 }
164 if ( $this->locksHeld[$path] === [] ) {
165 unset( $this->locksHeld[$path] ); // no locks on this path
166 if ( isset( $this->handles[$path] ) ) {
167 $handlesToClose[] = $this->handles[$path];
168 unset( $this->handles[$path] );
169 }
170 }
171 // Unlock handles to release locks and delete
172 // any lock files that end up with no locks on them...
173 if ( $this->isWindows ) {
174 // Windows: for any process, including this one,
175 // calling unlink() on a locked file will fail
176 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
177 $status->merge( $this->pruneKeyLockFiles( $path ) );
178 } else {
179 // Unix: unlink() can be used on files currently open by this
180 // process and we must do so in order to avoid race conditions
181 $status->merge( $this->pruneKeyLockFiles( $path ) );
182 $status->merge( $this->closeLockHandles( $path, $handlesToClose ) );
183 }
184 }
185
186 return $status;
187 }
188
194 private function closeLockHandles( $path, array $handlesToClose ) {
195 $status = StatusValue::newGood();
196 foreach ( $handlesToClose as $handle ) {
197 if ( !flock( $handle, LOCK_UN ) ) {
198 $status->fatal( 'lockmanager-fail-releaselock', $path );
199 }
200 if ( !fclose( $handle ) ) {
201 $status->warning( 'lockmanager-fail-closelock', $path );
202 }
203 }
204
205 return $status;
206 }
207
212 private function pruneKeyLockFiles( $path ) {
213 $status = StatusValue::newGood();
214 if ( !isset( $this->locksHeld[$path] ) ) {
215 # No locks are held for the lock file anymore
216 if ( !unlink( $this->getLockPath( $path ) ) ) {
217 $status->warning( 'lockmanager-fail-deletelock', $path );
218 }
219 unset( $this->handles[$path] );
220 }
221
222 return $status;
223 }
224
230 protected function getLockPath( $path ) {
231 return "{$this->lockDir}/{$this->sha1Base36Absolute( $path )}.lock";
232 }
233
237 public function __destruct() {
238 while ( count( $this->locksHeld ) ) {
239 foreach ( $this->locksHeld as $path => $locks ) {
240 $this->doSingleUnlock( $path, self::LOCK_EX );
241 $this->doSingleUnlock( $path, self::LOCK_SH );
242 }
243 }
244 }
245}
247class_alias( FSLockManager::class, 'FSLockManager' );
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newGood( $value=null)
Factory function for good results.
Simple lock management based on server-local temporary files.
array $lockTypeMap
Mapping of lock types to the type actually used.
doSingleLock( $path, $type)
Lock a single resource key.
__construct(array $config)
Construct a new instance from configuration.
array $handles
Map of (locked key => lock file handle)
getLockPath( $path)
Get the path to the lock file for a key.
__destruct()
Make sure remaining locks get cleared.
string $lockDir
Global dir for all servers.
doSingleUnlock( $path, $type)
Unlock a single resource key.
Resource locking handling.
const LOCK_SH
Lock types; stronger locks have higher values.