summaryrefslogtreecommitdiff
path: root/includes/filebackend/lockmanager/LockManager.php
blob: df8d2d4f0c568a457438498a6ee0543b11d97e5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<?php
/**
 * @defgroup LockManager Lock management
 * @ingroup FileBackend
 */

/**
 * Resource locking handling.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup LockManager
 * @author Aaron Schulz
 */

/**
 * @brief Class for handling resource locking.
 *
 * Locks on resource keys can either be shared or exclusive.
 *
 * Implementations must keep track of what is locked by this proccess
 * in-memory and support nested locking calls (using reference counting).
 * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
 * Locks should either be non-blocking or have low wait timeouts.
 *
 * Subclasses should avoid throwing exceptions at all costs.
 *
 * @ingroup LockManager
 * @since 1.19
 */
abstract class LockManager {
	/** @var array Mapping of lock types to the type actually used */
	protected $lockTypeMap = array(
		self::LOCK_SH => self::LOCK_SH,
		self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH
		self::LOCK_EX => self::LOCK_EX
	);

	/** @var array Map of (resource path => lock type => count) */
	protected $locksHeld = array();

	protected $domain; // string; domain (usually wiki ID)
	protected $lockTTL; // integer; maximum time locks can be held

	/** Lock types; stronger locks have higher values */
	const LOCK_SH = 1; // shared lock (for reads)
	const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
	const LOCK_EX = 3; // exclusive lock (for writes)

	/**
	 * Construct a new instance from configuration
	 *
	 * @param array $config Paramaters include:
	 *   - domain  : Domain (usually wiki ID) that all resources are relative to [optional]
	 *   - lockTTL : Age (in seconds) at which resource locks should expire.
	 *               This only applies if locks are not tied to a connection/process.
	 */
	public function __construct( array $config ) {
		$this->domain = isset( $config['domain'] ) ? $config['domain'] : wfWikiID();
		if ( isset( $config['lockTTL'] ) ) {
			$this->lockTTL = max( 1, $config['lockTTL'] );
		} elseif ( PHP_SAPI === 'cli' ) {
			$this->lockTTL = 2 * 3600;
		} else {
			$met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
			$this->lockTTL = max( 5 * 60, 2 * (int)$met );
		}
	}

	/**
	 * Lock the resources at the given abstract paths
	 *
	 * @param array $paths List of resource names
	 * @param int $type LockManager::LOCK_* constant
	 * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
	 * @return Status
	 */
	final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
		return $this->lockByType( array( $type => $paths ), $timeout );
	}

	/**
	 * Lock the resources at the given abstract paths
	 *
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
	 * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
	 * @return Status
	 * @since 1.22
	 */
	final public function lockByType( array $pathsByType, $timeout = 0 ) {
		wfProfileIn( __METHOD__ );
		$status = Status::newGood();
		$pathsByType = $this->normalizePathsByType( $pathsByType );
		$msleep = array( 0, 50, 100, 300, 500 ); // retry backoff times
		$start = microtime( true );
		do {
			$status = $this->doLockByType( $pathsByType );
			$elapsed = microtime( true ) - $start;
			if ( $status->isOK() || $elapsed >= $timeout || $elapsed < 0 ) {
				break; // success, timeout, or clock set back
			}
			usleep( 1e3 * ( next( $msleep ) ?: 1000 ) ); // use 1 sec after enough times
			$elapsed = microtime( true ) - $start;
		} while ( $elapsed < $timeout && $elapsed >= 0 );
		wfProfileOut( __METHOD__ );

		return $status;
	}

	/**
	 * Unlock the resources at the given abstract paths
	 *
	 * @param array $paths List of paths
	 * @param int $type LockManager::LOCK_* constant
	 * @return Status
	 */
	final public function unlock( array $paths, $type = self::LOCK_EX ) {
		return $this->unlockByType( array( $type => $paths ) );
	}

	/**
	 * Unlock the resources at the given abstract paths
	 *
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
	 * @return Status
	 * @since 1.22
	 */
	final public function unlockByType( array $pathsByType ) {
		wfProfileIn( __METHOD__ );
		$pathsByType = $this->normalizePathsByType( $pathsByType );
		$status = $this->doUnlockByType( $pathsByType );
		wfProfileOut( __METHOD__ );

		return $status;
	}

	/**
	 * Get the base 36 SHA-1 of a string, padded to 31 digits.
	 * Before hashing, the path will be prefixed with the domain ID.
	 * This should be used interally for lock key or file names.
	 *
	 * @param string $path
	 * @return string
	 */
	final protected function sha1Base36Absolute( $path ) {
		return wfBaseConvert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
	}

	/**
	 * Get the base 16 SHA-1 of a string, padded to 31 digits.
	 * Before hashing, the path will be prefixed with the domain ID.
	 * This should be used interally for lock key or file names.
	 *
	 * @param string $path
	 * @return string
	 */
	final protected function sha1Base16Absolute( $path ) {
		return sha1( "{$this->domain}:{$path}" );
	}

	/**
	 * Normalize the $paths array by converting LOCK_UW locks into the
	 * appropriate type and removing any duplicated paths for each lock type.
	 *
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
	 * @return array
	 * @since 1.22
	 */
	final protected function normalizePathsByType( array $pathsByType ) {
		$res = array();
		foreach ( $pathsByType as $type => $paths ) {
			$res[$this->lockTypeMap[$type]] = array_unique( $paths );
		}

		return $res;
	}

	/**
	 * @see LockManager::lockByType()
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
	 * @return Status
	 * @since 1.22
	 */
	protected function doLockByType( array $pathsByType ) {
		$status = Status::newGood();
		$lockedByType = array(); // map of (type => paths)
		foreach ( $pathsByType as $type => $paths ) {
			$status->merge( $this->doLock( $paths, $type ) );
			if ( $status->isOK() ) {
				$lockedByType[$type] = $paths;
			} else {
				// Release the subset of locks that were acquired
				foreach ( $lockedByType as $lType => $lPaths ) {
					$status->merge( $this->doUnlock( $lPaths, $lType ) );
				}
				break;
			}
		}

		return $status;
	}

	/**
	 * Lock resources with the given keys and lock type
	 *
	 * @param array $paths List of paths
	 * @param int $type LockManager::LOCK_* constant
	 * @return Status
	 */
	abstract protected function doLock( array $paths, $type );

	/**
	 * @see LockManager::unlockByType()
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
	 * @return Status
	 * @since 1.22
	 */
	protected function doUnlockByType( array $pathsByType ) {
		$status = Status::newGood();
		foreach ( $pathsByType as $type => $paths ) {
			$status->merge( $this->doUnlock( $paths, $type ) );
		}

		return $status;
	}

	/**
	 * Unlock resources with the given keys and lock type
	 *
	 * @param array $paths List of paths
	 * @param int $type LockManager::LOCK_* constant
	 * @return Status
	 */
	abstract protected function doUnlock( array $paths, $type );
}

/**
 * Simple version of LockManager that does nothing
 * @since 1.19
 */
class NullLockManager extends LockManager {
	protected function doLock( array $paths, $type ) {
		return Status::newGood();
	}

	protected function doUnlock( array $paths, $type ) {
		return Status::newGood();
	}
}