Completed
Branch master (e770d9)
by
unknown
33:41
created

MemcLockManager::freeLocksOnServer()   C

Complexity

Conditions 11
Paths 32

Size

Total Lines 64
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 38
nc 32
nop 2
dl 0
loc 64
rs 6.0563
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Version of LockManager based on using memcached servers.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup LockManager
22
 */
23
use Wikimedia\WaitConditionLoop;
24
25
/**
26
 * Manage locks using memcached servers.
27
 *
28
 * Version of LockManager based on using memcached servers.
29
 * This is meant for multi-wiki systems that may share files.
30
 * All locks are non-blocking, which avoids deadlocks.
31
 *
32
 * All lock requests for a resource, identified by a hash string, will map to one
33
 * bucket. Each bucket maps to one or several peer servers, each running memcached.
34
 * A majority of peers must agree for a lock to be acquired.
35
 *
36
 * @ingroup LockManager
37
 * @since 1.20
38
 */
39
class MemcLockManager extends QuorumLockManager {
40
	/** @var array Mapping of lock types to the type actually used */
41
	protected $lockTypeMap = [
42
		self::LOCK_SH => self::LOCK_SH,
43
		self::LOCK_UW => self::LOCK_SH,
44
		self::LOCK_EX => self::LOCK_EX
45
	];
46
47
	/** @var MemcachedBagOStuff[] Map of (server name => MemcachedBagOStuff) */
48
	protected $cacheServers = [];
49
	/** @var HashBagOStuff Server status cache */
50
	protected $statusCache;
51
52
	/**
53
	 * Construct a new instance from configuration.
54
	 *
55
	 * @param array $config Parameters include:
56
	 *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
57
	 *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
58
	 *                    each having an odd-numbered list of server names (peers) as values.
59
	 *   - memcConfig   : Configuration array for MemcachedBagOStuff::construct() with an
60
	 *                    additional 'class' parameter specifying which MemcachedBagOStuff
61
	 *                    subclass to use. The server names will be injected. [optional]
62
	 * @throws Exception
63
	 */
64
	public function __construct( array $config ) {
65
		parent::__construct( $config );
66
67
		// Sanitize srvsByBucket config to prevent PHP errors
68
		$this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
69
		$this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
70
71
		$memcConfig = isset( $config['memcConfig'] ) ? $config['memcConfig'] : [];
72
		$memcConfig += [ 'class' => 'MemcachedPhpBagOStuff' ]; // default
73
74
		$class = $memcConfig['class'];
75
		if ( !is_subclass_of( $class, 'MemcachedBagOStuff' ) ) {
76
			throw new InvalidArgumentException( "$class is not of type MemcachedBagOStuff." );
77
		}
78
79
		foreach ( $config['lockServers'] as $name => $address ) {
80
			$params = [ 'servers' => [ $address ] ] + $memcConfig;
81
			$this->cacheServers[$name] = new $class( $params );
82
		}
83
84
		$this->statusCache = new HashBagOStuff();
85
	}
86
87
	protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
88
		$status = StatusValue::newGood();
89
90
		$memc = $this->getCache( $lockSrv );
91
		// List of affected paths
92
		$paths = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
93
		$paths = array_unique( $paths );
94
		// List of affected lock record keys
95
		$keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
96
97
		// Lock all of the active lock record keys...
98
		if ( !$this->acquireMutexes( $memc, $keys ) ) {
0 ignored issues
show
Bug introduced by
It seems like $memc defined by $this->getCache($lockSrv) on line 90 can be null; however, MemcLockManager::acquireMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
99
			foreach ( $paths as $path ) {
100
				$status->fatal( 'lockmanager-fail-acquirelock', $path );
101
			}
102
103
			return $status;
104
		}
105
106
		// Fetch all the existing lock records...
107
		$lockRecords = $memc->getMulti( $keys );
108
109
		$now = time();
110
		// Check if the requested locks conflict with existing ones...
111
		foreach ( $pathsByType as $type => $paths ) {
112
			foreach ( $paths as $path ) {
113
				$locksKey = $this->recordKeyForPath( $path );
114
				$locksHeld = isset( $lockRecords[$locksKey] )
115
					? self::sanitizeLockArray( $lockRecords[$locksKey] )
116
					: self::newLockArray(); // init
117 View Code Duplication
				foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
118
					if ( $expiry < $now ) { // stale?
119
						unset( $locksHeld[self::LOCK_EX][$session] );
120
					} elseif ( $session !== $this->session ) {
121
						$status->fatal( 'lockmanager-fail-acquirelock', $path );
122
					}
123
				}
124
				if ( $type === self::LOCK_EX ) {
125 View Code Duplication
					foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
126
						if ( $expiry < $now ) { // stale?
127
							unset( $locksHeld[self::LOCK_SH][$session] );
128
						} elseif ( $session !== $this->session ) {
129
							$status->fatal( 'lockmanager-fail-acquirelock', $path );
130
						}
131
					}
132
				}
133
				if ( $status->isOK() ) {
134
					// Register the session in the lock record array
135
					$locksHeld[$type][$this->session] = $now + $this->lockTTL;
136
					// We will update this record if none of the other locks conflict
137
					$lockRecords[$locksKey] = $locksHeld;
138
				}
139
			}
140
		}
141
142
		// If there were no lock conflicts, update all the lock records...
143
		if ( $status->isOK() ) {
144
			foreach ( $paths as $path ) {
145
				$locksKey = $this->recordKeyForPath( $path );
146
				$locksHeld = $lockRecords[$locksKey];
147
				$ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
148
				if ( !$ok ) {
149
					$status->fatal( 'lockmanager-fail-acquirelock', $path );
150
				} else {
151
					$this->logger->debug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
152
				}
153
			}
154
		}
155
156
		// Unlock all of the active lock record keys...
157
		$this->releaseMutexes( $memc, $keys );
0 ignored issues
show
Bug introduced by
It seems like $memc defined by $this->getCache($lockSrv) on line 90 can be null; however, MemcLockManager::releaseMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
158
159
		return $status;
160
	}
161
162
	protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
163
		$status = StatusValue::newGood();
164
165
		$memc = $this->getCache( $lockSrv );
166
		// List of affected paths
167
		$paths = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
168
		$paths = array_unique( $paths );
169
		// List of affected lock record keys
170
		$keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
171
172
		// Lock all of the active lock record keys...
173
		if ( !$this->acquireMutexes( $memc, $keys ) ) {
0 ignored issues
show
Bug introduced by
It seems like $memc defined by $this->getCache($lockSrv) on line 165 can be null; however, MemcLockManager::acquireMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
174
			foreach ( $paths as $path ) {
175
				$status->fatal( 'lockmanager-fail-releaselock', $path );
176
			}
177
178
			return $status;
179
		}
180
181
		// Fetch all the existing lock records...
182
		$lockRecords = $memc->getMulti( $keys );
183
184
		// Remove the requested locks from all records...
185
		foreach ( $pathsByType as $type => $paths ) {
186
			foreach ( $paths as $path ) {
187
				$locksKey = $this->recordKeyForPath( $path ); // lock record
188
				if ( !isset( $lockRecords[$locksKey] ) ) {
189
					$status->warning( 'lockmanager-fail-releaselock', $path );
190
					continue; // nothing to do
191
				}
192
				$locksHeld = $this->sanitizeLockArray( $lockRecords[$locksKey] );
193
				if ( isset( $locksHeld[$type][$this->session] ) ) {
194
					unset( $locksHeld[$type][$this->session] ); // unregister this session
195
					$lockRecords[$locksKey] = $locksHeld;
196
				} else {
197
					$status->warning( 'lockmanager-fail-releaselock', $path );
198
				}
199
			}
200
		}
201
202
		// Persist the new lock record values...
203
		foreach ( $paths as $path ) {
204
			$locksKey = $this->recordKeyForPath( $path );
205
			if ( !isset( $lockRecords[$locksKey] ) ) {
206
				continue; // nothing to do
207
			}
208
			$locksHeld = $lockRecords[$locksKey];
209
			if ( $locksHeld === $this->newLockArray() ) {
210
				$ok = $memc->delete( $locksKey );
211
			} else {
212
				$ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
213
			}
214
			if ( $ok ) {
215
				$this->logger->debug( __METHOD__ . ": released lock on key $locksKey.\n" );
216
			} else {
217
				$status->fatal( 'lockmanager-fail-releaselock', $path );
218
			}
219
		}
220
221
		// Unlock all of the active lock record keys...
222
		$this->releaseMutexes( $memc, $keys );
0 ignored issues
show
Bug introduced by
It seems like $memc defined by $this->getCache($lockSrv) on line 165 can be null; however, MemcLockManager::releaseMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
223
224
		return $status;
225
	}
226
227
	/**
228
	 * @see QuorumLockManager::releaseAllLocks()
229
	 * @return StatusValue
230
	 */
231
	protected function releaseAllLocks() {
232
		return StatusValue::newGood(); // not supported
233
	}
234
235
	/**
236
	 * @see QuorumLockManager::isServerUp()
237
	 * @param string $lockSrv
238
	 * @return bool
239
	 */
240
	protected function isServerUp( $lockSrv ) {
241
		return (bool)$this->getCache( $lockSrv );
242
	}
243
244
	/**
245
	 * Get the MemcachedBagOStuff object for a $lockSrv
246
	 *
247
	 * @param string $lockSrv Server name
248
	 * @return MemcachedBagOStuff|null
249
	 */
250
	protected function getCache( $lockSrv ) {
251
		if ( !isset( $this->cacheServers[$lockSrv] ) ) {
252
			throw new InvalidArgumentException( "Invalid cache server '$lockSrv'." );
253
		}
254
255
		$online = $this->statusCache->get( "online:$lockSrv" );
256
		if ( $online === false ) {
257
			$online = $this->cacheServers[$lockSrv]->set( __CLASS__ . ':ping', 1, 1 );
258
			if ( !$online ) { // server down?
259
				$this->logger->warning( __METHOD__ . ": Could not contact $lockSrv." );
260
			}
261
			$this->statusCache->set( "online:$lockSrv", (int)$online, 30 );
262
		}
263
264
		return $online ? $this->cacheServers[$lockSrv] : null;
265
	}
266
267
	/**
268
	 * @param string $path
269
	 * @return string
270
	 */
271
	protected function recordKeyForPath( $path ) {
272
		return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
273
	}
274
275
	/**
276
	 * @return array An empty lock structure for a key
277
	 */
278
	protected function newLockArray() {
279
		return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
280
	}
281
282
	/**
283
	 * @param array $a
284
	 * @return array An empty lock structure for a key
285
	 */
286
	protected function sanitizeLockArray( $a ) {
287
		if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
288
			return $a;
289
		}
290
291
		$this->logger->error( __METHOD__ . ": reset invalid lock array." );
292
293
		return $this->newLockArray();
294
	}
295
296
	/**
297
	 * @param MemcachedBagOStuff $memc
298
	 * @param array $keys List of keys to acquire
299
	 * @return bool
300
	 */
301
	protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
302
		$lockedKeys = [];
303
304
		// Acquire the keys in lexicographical order, to avoid deadlock problems.
305
		// If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
306
		sort( $keys );
307
308
		// Try to quickly loop to acquire the keys, but back off after a few rounds.
309
		// This reduces memcached spam, especially in the rare case where a server acquires
310
		// some lock keys and dies without releasing them. Lock keys expire after a few minutes.
311
		$loop = new WaitConditionLoop(
312
			function () use ( $memc, $keys, &$lockedKeys ) {
313
				foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
314
					if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
315
						$lockedKeys[] = $key;
316
					}
317
				}
318
319
				return array_diff( $keys, $lockedKeys )
320
					? WaitConditionLoop::CONDITION_CONTINUE
321
					: true;
322
			},
323
			3.0 // timeout
324
		);
325
		$loop->invoke();
326
327
		if ( count( $lockedKeys ) != count( $keys ) ) {
328
			$this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
329
			return false;
330
		}
331
332
		return true;
333
	}
334
335
	/**
336
	 * @param MemcachedBagOStuff $memc
337
	 * @param array $keys List of acquired keys
338
	 */
339
	protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
340
		foreach ( $keys as $key ) {
341
			$memc->delete( "$key:mutex" );
342
		}
343
	}
344
345
	/**
346
	 * Make sure remaining locks get cleared for sanity
347
	 */
348
	function __destruct() {
349
		while ( count( $this->locksHeld ) ) {
350
			foreach ( $this->locksHeld as $path => $locks ) {
351
				$this->doUnlock( [ $path ], self::LOCK_EX );
352
				$this->doUnlock( [ $path ], self::LOCK_SH );
353
			}
354
		}
355
	}
356
}
357