Completed
Branch master (098997)
by
unknown
28:44
created

LockManager::__construct()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 24
nop 1
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * @defgroup LockManager Lock management
4
 * @ingroup FileBackend
5
 */
6
use Psr\Log\LoggerInterface;
7
8
/**
9
 * Resource locking handling.
10
 *
11
 * This program is free software; you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation; either version 2 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License along
22
 * with this program; if not, write to the Free Software Foundation, Inc.,
23
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24
 * http://www.gnu.org/copyleft/gpl.html
25
 *
26
 * @file
27
 * @ingroup LockManager
28
 * @author Aaron Schulz
29
 */
30
31
/**
32
 * @brief Class for handling resource locking.
33
 *
34
 * Locks on resource keys can either be shared or exclusive.
35
 *
36
 * Implementations must keep track of what is locked by this proccess
37
 * in-memory and support nested locking calls (using reference counting).
38
 * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op.
39
 * Locks should either be non-blocking or have low wait timeouts.
40
 *
41
 * Subclasses should avoid throwing exceptions at all costs.
42
 *
43
 * @ingroup LockManager
44
 * @since 1.19
45
 */
46
abstract class LockManager {
47
	/** @var LoggerInterface */
48
	protected $logger;
49
50
	/** @var array Mapping of lock types to the type actually used */
51
	protected $lockTypeMap = [
52
		self::LOCK_SH => self::LOCK_SH,
53
		self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH
54
		self::LOCK_EX => self::LOCK_EX
55
	];
56
57
	/** @var array Map of (resource path => lock type => count) */
58
	protected $locksHeld = [];
59
60
	protected $domain; // string; domain (usually wiki ID)
61
	protected $lockTTL; // integer; maximum time locks can be held
62
63
	/** @var string Random 32-char hex number */
64
	protected $session;
65
66
	/** Lock types; stronger locks have higher values */
67
	const LOCK_SH = 1; // shared lock (for reads)
68
	const LOCK_UW = 2; // shared lock (for reads used to write elsewhere)
69
	const LOCK_EX = 3; // exclusive lock (for writes)
70
71
	/**
72
	 * Construct a new instance from configuration
73
	 *
74
	 * @param array $config Parameters include:
75
	 *   - domain  : Domain (usually wiki ID) that all resources are relative to [optional]
76
	 *   - lockTTL : Age (in seconds) at which resource locks should expire.
77
	 *               This only applies if locks are not tied to a connection/process.
78
	 */
79
	public function __construct( array $config ) {
80
		$this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global';
81
		if ( isset( $config['lockTTL'] ) ) {
82
			$this->lockTTL = max( 5, $config['lockTTL'] );
83
		} elseif ( PHP_SAPI === 'cli' ) {
84
			$this->lockTTL = 3600;
85
		} else {
86
			$met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
87
			$this->lockTTL = max( 5 * 60, 2 * (int)$met );
88
		}
89
90
		$random = [];
91
		for ( $i = 1; $i <= 5; ++$i ) {
92
			$random[] = mt_rand( 0, 0xFFFFFFF );
93
		}
94
		$this->session = md5( implode( '-', $random ) );
95
96
		$this->logger = isset( $config['logger'] ) ? $config['logger'] : new \Psr\Log\NullLogger();
97
	}
98
99
	/**
100
	 * Lock the resources at the given abstract paths
101
	 *
102
	 * @param array $paths List of resource names
103
	 * @param int $type LockManager::LOCK_* constant
104
	 * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
105
	 * @return StatusValue
106
	 */
107
	final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) {
108
		return $this->lockByType( [ $type => $paths ], $timeout );
109
	}
110
111
	/**
112
	 * Lock the resources at the given abstract paths
113
	 *
114
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
115
	 * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21)
116
	 * @return StatusValue
117
	 * @since 1.22
118
	 */
119
	final public function lockByType( array $pathsByType, $timeout = 0 ) {
120
		$pathsByType = $this->normalizePathsByType( $pathsByType );
121
122
		$status = null;
123
		$loop = new WaitConditionLoop(
124
			function () use ( &$status, $pathsByType ) {
125
				$status = $this->doLockByType( $pathsByType );
126
127
				return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE;
128
			},
129
			$timeout
130
		);
131
		$loop->invoke();
132
133
		return $status;
134
	}
135
136
	/**
137
	 * Unlock the resources at the given abstract paths
138
	 *
139
	 * @param array $paths List of paths
140
	 * @param int $type LockManager::LOCK_* constant
141
	 * @return StatusValue
142
	 */
143
	final public function unlock( array $paths, $type = self::LOCK_EX ) {
144
		return $this->unlockByType( [ $type => $paths ] );
145
	}
146
147
	/**
148
	 * Unlock the resources at the given abstract paths
149
	 *
150
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
151
	 * @return StatusValue
152
	 * @since 1.22
153
	 */
154
	final public function unlockByType( array $pathsByType ) {
155
		$pathsByType = $this->normalizePathsByType( $pathsByType );
156
		$status = $this->doUnlockByType( $pathsByType );
157
158
		return $status;
159
	}
160
161
	/**
162
	 * Get the base 36 SHA-1 of a string, padded to 31 digits.
163
	 * Before hashing, the path will be prefixed with the domain ID.
164
	 * This should be used interally for lock key or file names.
165
	 *
166
	 * @param string $path
167
	 * @return string
168
	 */
169
	final protected function sha1Base36Absolute( $path ) {
170
		return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 );
171
	}
172
173
	/**
174
	 * Get the base 16 SHA-1 of a string, padded to 31 digits.
175
	 * Before hashing, the path will be prefixed with the domain ID.
176
	 * This should be used interally for lock key or file names.
177
	 *
178
	 * @param string $path
179
	 * @return string
180
	 */
181
	final protected function sha1Base16Absolute( $path ) {
182
		return sha1( "{$this->domain}:{$path}" );
183
	}
184
185
	/**
186
	 * Normalize the $paths array by converting LOCK_UW locks into the
187
	 * appropriate type and removing any duplicated paths for each lock type.
188
	 *
189
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
190
	 * @return array
191
	 * @since 1.22
192
	 */
193
	final protected function normalizePathsByType( array $pathsByType ) {
194
		$res = [];
195
		foreach ( $pathsByType as $type => $paths ) {
196
			$res[$this->lockTypeMap[$type]] = array_unique( $paths );
197
		}
198
199
		return $res;
200
	}
201
202
	/**
203
	 * @see LockManager::lockByType()
204
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
205
	 * @return StatusValue
206
	 * @since 1.22
207
	 */
208
	protected function doLockByType( array $pathsByType ) {
209
		$status = StatusValue::newGood();
210
		$lockedByType = []; // map of (type => paths)
211
		foreach ( $pathsByType as $type => $paths ) {
212
			$status->merge( $this->doLock( $paths, $type ) );
213
			if ( $status->isOK() ) {
214
				$lockedByType[$type] = $paths;
215
			} else {
216
				// Release the subset of locks that were acquired
217
				foreach ( $lockedByType as $lType => $lPaths ) {
218
					$status->merge( $this->doUnlock( $lPaths, $lType ) );
219
				}
220
				break;
221
			}
222
		}
223
224
		return $status;
225
	}
226
227
	/**
228
	 * Lock resources with the given keys and lock type
229
	 *
230
	 * @param array $paths List of paths
231
	 * @param int $type LockManager::LOCK_* constant
232
	 * @return StatusValue
233
	 */
234
	abstract protected function doLock( array $paths, $type );
235
236
	/**
237
	 * @see LockManager::unlockByType()
238
	 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths
239
	 * @return StatusValue
240
	 * @since 1.22
241
	 */
242 View Code Duplication
	protected function doUnlockByType( array $pathsByType ) {
243
		$status = StatusValue::newGood();
244
		foreach ( $pathsByType as $type => $paths ) {
245
			$status->merge( $this->doUnlock( $paths, $type ) );
246
		}
247
248
		return $status;
249
	}
250
251
	/**
252
	 * Unlock resources with the given keys and lock type
253
	 *
254
	 * @param array $paths List of paths
255
	 * @param int $type LockManager::LOCK_* constant
256
	 * @return StatusValue
257
	 */
258
	abstract protected function doUnlock( array $paths, $type );
259
}
260