Merged FileBackend branch. Manually avoiding merging the many prop-only changes SVN...
[lhc/web/wiklou.git] / includes / filerepo / backend / lockmanager / FSLockManager.php
1 <?php
2
3 /**
4 * Simple version of LockManager based on using FS lock files.
5 * All locks are non-blocking, which avoids deadlocks.
6 *
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.
11 *
12 * @ingroup LockManager
13 */
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
20 );
21
22 protected $lockDir; // global dir for all servers
23
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();
28
29 function __construct( array $config ) {
30 $this->lockDir = $config['lockDirectory'];
31 }
32
33 protected function doLock( array $keys, $type ) {
34 $status = Status::newGood();
35
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?
44 $lockedKeys[] = $key;
45 }
46 } else {
47 // Abort and unlock everything
48 $status->merge( $this->doUnlock( $lockedKeys, $type ) );
49 return $status;
50 }
51 }
52
53 return $status;
54 }
55
56 protected function doUnlock( array $keys, $type ) {
57 $status = Status::newGood();
58
59 foreach ( $keys as $key ) {
60 $status->merge( $this->doSingleUnlock( $key, $type ) );
61 }
62
63 return $status;
64 }
65
66 /**
67 * Lock a single resource key
68 *
69 * @param $key string
70 * @param $type integer
71 * @return Status
72 */
73 protected function doSingleLock( $key, $type ) {
74 $status = Status::newGood();
75
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;
80 } else {
81 wfSuppressWarnings();
82 $handle = fopen( $this->getLockPath( $key ), 'a+' );
83 wfRestoreWarnings();
84 if ( !$handle ) { // lock dir missing?
85 wfMkdirParents( $this->lockDir );
86 wfSuppressWarnings();
87 $handle = fopen( $this->getLockPath( $key ), 'a+' ); // try again
88 wfRestoreWarnings();
89 }
90 if ( $handle ) {
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;
97 } else {
98 fclose( $handle );
99 $status->fatal( 'lockmanager-fail-acquirelock', $key );
100 }
101 } else {
102 $status->fatal( 'lockmanager-fail-openlock', $key );
103 }
104 }
105
106 return $status;
107 }
108
109 /**
110 * Unlock a single resource key
111 *
112 * @param $key string
113 * @param $type integer
114 * @return Status
115 */
116 protected function doSingleUnlock( $key, $type ) {
117 $status = Status::newGood();
118
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 );
123 } else {
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] );
134 }
135 }
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 ) );
143 } else {
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 ) );
148 }
149 }
150
151 return $status;
152 }
153
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 );
160 }
161 if ( !fclose( $handle ) ) {
162 $status->warning( 'lockmanager-fail-closelock', $key );
163 }
164 wfRestoreWarnings();
165 }
166 return $status;
167 }
168
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 );
176 }
177 wfRestoreWarnings();
178 unset( $this->locksHeld[$key] );
179 unset( $this->handles[$key] );
180 }
181 return $status;
182 }
183
184 /**
185 * Get the path to the lock file for a key
186 * @param $key string
187 * @return string
188 */
189 protected function getLockPath( $key ) {
190 return "{$this->lockDir}/{$key}.lock";
191 }
192
193 function __destruct() {
194 // Make sure remaining locks get cleared for sanity
195 foreach ( $this->locksHeld as $key => $locks ) {
196 $this->doSingleUnlock( $key, 0 );
197 }
198 }
199 }