4 * Simple version of LockManager based on using FS lock files.
5 * All locks are non-blocking, which avoids deadlocks.
7 * This should work fine for small sites running off one server.
8 * Do not use this with 'lockDir' set to an NFS mount unless the
9 * NFS client is at least version 2.6.12. Otherwise, the BSD flock()
10 * locks will be ignored; see http://nfs.sourceforge.net/#section_d.
12 * @ingroup LockManager
14 class FSLockManager
extends LockManager
{
15 /** @var Array Mapping of lock types to the type actually used */
16 protected $lockTypeMap = array(
17 self
::LOCK_SH
=> self
::LOCK_SH
,
18 self
::LOCK_UW
=> self
::LOCK_SH
,
19 self
::LOCK_EX
=> self
::LOCK_EX
22 protected $lockDir; // global dir for all servers
24 /** @var Array Map of (locked key => lock type => count) */
25 protected $locksHeld = array();
26 /** @var Array Map of (locked key => lock type => lock file handle) */
27 protected $handles = array();
29 function __construct( array $config ) {
30 $this->lockDir
= $config['lockDirectory'];
33 protected function doLock( array $keys, $type ) {
34 $status = Status
::newGood();
36 $lockedKeys = array(); // files locked in this attempt
37 foreach ( $keys as $key ) {
38 $subStatus = $this->doSingleLock( $key, $type );
39 $status->merge( $subStatus );
40 if ( $status->isOK() ) {
41 // Don't append to $lockedKeys if $key is already locked.
42 // We do NOT want to unlock the key if we have to rollback.
43 if ( $subStatus->isGood() ) { // no warnings/fatals?
47 // Abort and unlock everything
48 $status->merge( $this->doUnlock( $lockedKeys, $type ) );
56 protected function doUnlock( array $keys, $type ) {
57 $status = Status
::newGood();
59 foreach ( $keys as $key ) {
60 $status->merge( $this->doSingleUnlock( $key, $type ) );
67 * Lock a single resource key
70 * @param $type integer
73 protected function doSingleLock( $key, $type ) {
74 $status = Status
::newGood();
76 if ( isset( $this->locksHeld
[$key][$type] ) ) {
77 ++
$this->locksHeld
[$key][$type];
78 } elseif ( isset( $this->locksHeld
[$key][self
::LOCK_EX
] ) ) {
79 $this->locksHeld
[$key][$type] = 1;
82 $handle = fopen( $this->getLockPath( $key ), 'a+' );
84 if ( !$handle ) { // lock dir missing?
85 wfMkdirParents( $this->lockDir
);
87 $handle = fopen( $this->getLockPath( $key ), 'a+' ); // try again
91 // Either a shared or exclusive lock
92 $lock = ( $type == self
::LOCK_SH
) ? LOCK_SH
: LOCK_EX
;
93 if ( flock( $handle, $lock | LOCK_NB
) ) {
94 // Record this lock as active
95 $this->locksHeld
[$key][$type] = 1;
96 $this->handles
[$key][$type] = $handle;
99 $status->fatal( 'lockmanager-fail-acquirelock', $key );
102 $status->fatal( 'lockmanager-fail-openlock', $key );
110 * Unlock a single resource key
113 * @param $type integer
116 protected function doSingleUnlock( $key, $type ) {
117 $status = Status
::newGood();
119 if ( !isset( $this->locksHeld
[$key] ) ) {
120 $status->warning( 'lockmanager-notlocked', $key );
121 } elseif ( !isset( $this->locksHeld
[$key][$type] ) ) {
122 $status->warning( 'lockmanager-notlocked', $key );
124 $handlesToClose = array();
125 --$this->locksHeld
[$key][$type];
126 if ( $this->locksHeld
[$key][$type] <= 0 ) {
127 unset( $this->locksHeld
[$key][$type] );
128 // If a LOCK_SH comes in while we have a LOCK_EX, we don't
129 // actually add a handler, so check for handler existence.
130 if ( isset( $this->handles
[$key][$type] ) ) {
131 // Mark this handle to be unlocked and closed
132 $handlesToClose[] = $this->handles
[$key][$type];
133 unset( $this->handles
[$key][$type] );
136 // Unlock handles to release locks and delete
137 // any lock files that end up with no locks on them...
138 if ( wfIsWindows() ) {
139 // Windows: for any process, including this one,
140 // calling unlink() on a locked file will fail
141 $status->merge( $this->closeLockHandles( $key, $handlesToClose ) );
142 $status->merge( $this->pruneKeyLockFiles( $key ) );
144 // Unix: unlink() can be used on files currently open by this
145 // process and we must do so in order to avoid race conditions
146 $status->merge( $this->pruneKeyLockFiles( $key ) );
147 $status->merge( $this->closeLockHandles( $key, $handlesToClose ) );
154 private function closeLockHandles( $key, array $handlesToClose ) {
155 $status = Status
::newGood();
156 foreach ( $handlesToClose as $handle ) {
157 wfSuppressWarnings();
158 if ( !flock( $handle, LOCK_UN
) ) {
159 $status->fatal( 'lockmanager-fail-releaselock', $key );
161 if ( !fclose( $handle ) ) {
162 $status->warning( 'lockmanager-fail-closelock', $key );
169 private function pruneKeyLockFiles( $key ) {
170 $status = Status
::newGood();
171 if ( !count( $this->locksHeld
[$key] ) ) {
172 wfSuppressWarnings();
173 # No locks are held for the lock file anymore
174 if ( !unlink( $this->getLockPath( $key ) ) ) {
175 $status->warning( 'lockmanager-fail-deletelock', $key );
178 unset( $this->locksHeld
[$key] );
179 unset( $this->handles
[$key] );
185 * Get the path to the lock file for a key
189 protected function getLockPath( $key ) {
190 return "{$this->lockDir}/{$key}.lock";
193 function __destruct() {
194 // Make sure remaining locks get cleared for sanity
195 foreach ( $this->locksHeld
as $key => $locks ) {
196 $this->doSingleUnlock( $key, 0 );