Completed
Branch master (174b3a)
by
unknown
26:51
created

LBFactory   D

Complexity

Total Complexity 86

Size/Duplication

Total Lines 718
Duplicated Lines 0.42 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
dl 3
loc 718
rs 4.4444
c 0
b 0
f 0
wmc 86
lcom 1
cbo 10

34 Methods

Rating   Name   Duplication   Size   Complexity  
F __construct() 3 41 17
A destroy() 0 4 1
newMainLB() 0 1 ?
getMainLB() 0 1 ?
newExternalLB() 0 1 ?
getExternalLB() 0 1 ?
forEachLB() 0 1 ?
A shutdown() 0 12 3
A forEachLBCallMethod() 0 8 1
A flushReplicaSnapshots() 0 3 1
A commitAll() 0 4 1
A beginMasterChanges() 0 11 2
B commitMasterChanges() 0 30 5
A rollbackMasterChanges() 0 9 1
A logIfMultiDbTransaction() 0 19 4
A hasMasterChanges() 0 8 2
A laggedReplicaUsed() 0 8 2
A hasOrMadeRecentMasterChanges() 0 7 2
C waitForReplication() 0 64 13
A setWaitForReplicationListener() 0 7 2
A getEmptyTransactionTicket() 0 8 2
B commitAndWaitForReplication() 0 23 5
A getChronologyProtectorTouched() 0 3 1
A disableChronologyProtection() 0 3 1
B getChronologyProtector() 0 28 5
B shutdownChronologyProtector() 0 25 4
A baseLoadBalancerParams() 0 17 1
A initLoadBalancer() 0 5 2
A setDomainPrefix() 0 11 1
A closeAll() 0 3 1
A setAgentName() 0 3 1
A appendPreShutdownTimeAsQuery() 0 12 3
A setRequestInfo() 0 3 1
A __destruct() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LBFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LBFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Generator and manager of database load balancing objects
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 Database
22
 */
23
24
use Psr\Log\LoggerInterface;
25
26
/**
27
 * An interface for generating database load balancers
28
 * @ingroup Database
29
 */
30
abstract class LBFactory {
31
	/** @var ChronologyProtector */
32
	protected $chronProt;
33
	/** @var object|string Class name or object With profileIn/profileOut methods */
34
	protected $profiler;
35
	/** @var TransactionProfiler */
36
	protected $trxProfiler;
37
	/** @var LoggerInterface */
38
	protected $replLogger;
39
	/** @var LoggerInterface */
40
	protected $connLogger;
41
	/** @var LoggerInterface */
42
	protected $queryLogger;
43
	/** @var LoggerInterface */
44
	protected $perfLogger;
45
	/** @var callable Error logger */
46
	protected $errorLogger;
47
	/** @var BagOStuff */
48
	protected $srvCache;
49
	/** @var BagOStuff */
50
	protected $memCache;
51
	/** @var WANObjectCache */
52
	protected $wanCache;
53
54
	/** @var DatabaseDomain Local domain */
55
	protected $localDomain;
56
	/** @var string Local hostname of the app server */
57
	protected $hostname;
58
	/** @var array Web request information about the client */
59
	protected $requestInfo;
60
61
	/** @var mixed */
62
	protected $ticket;
63
	/** @var string|bool String if a requested DBO_TRX transaction round is active */
64
	protected $trxRoundId = false;
65
	/** @var string|bool Reason all LBs are read-only or false if not */
66
	protected $readOnlyReason = false;
67
	/** @var callable[] */
68
	protected $replicationWaitCallbacks = [];
69
70
	/** @var bool Whether this PHP instance is for a CLI script */
71
	protected $cliMode;
72
	/** @var string Agent name for query profiling */
73
	protected $agent;
74
75
	const SHUTDOWN_NO_CHRONPROT = 0; // don't save DB positions at all
76
	const SHUTDOWN_CHRONPROT_ASYNC = 1; // save DB positions, but don't wait on remote DCs
77
	const SHUTDOWN_CHRONPROT_SYNC = 2; // save DB positions, waiting on all DCs
78
79
	private static $loggerFields =
80
		[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
81
82
	/**
83
	 * Construct a manager of ILoadBalancer objects
84
	 *
85
	 * Sub-classes will extend the required keys in $conf with additional parameters
86
	 *
87
	 * @param $conf $params Array with keys:
0 ignored issues
show
Documentation introduced by
The doc-type $conf could not be parsed: Unknown type name "$conf" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
Bug introduced by
There is no parameter named $params. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
88
	 *  - localDomain: A DatabaseDomain or domain ID string.
89
	 *  - readOnlyReason : Reason the master DB is read-only if so [optional]
90
	 *  - srvCache : BagOStuff object for server cache [optional]
91
	 *  - memCache : BagOStuff object for cluster memory cache [optional]
92
	 *  - wanCache : WANObjectCache object [optional]
93
	 *  - hostname : The name of the current server [optional]
94
	 *  - cliMode: Whether the execution context is a CLI script. [optional]
95
	 *  - profiler : Class name or instance with profileIn()/profileOut() methods. [optional]
96
	 *  - trxProfiler: TransactionProfiler instance. [optional]
97
	 *  - replLogger: PSR-3 logger instance. [optional]
98
	 *  - connLogger: PSR-3 logger instance. [optional]
99
	 *  - queryLogger: PSR-3 logger instance. [optional]
100
	 *  - perfLogger: PSR-3 logger instance. [optional]
101
	 *  - errorLogger : Callback that takes an Exception and logs it. [optional]
102
	 * @throws InvalidArgumentException
103
	 */
104
	public function __construct( array $conf ) {
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
105
		$this->localDomain = isset( $conf['localDomain'] )
106
			? DatabaseDomain::newFromId( $conf['localDomain'] )
107
			: DatabaseDomain::newUnspecified();
108
109 View Code Duplication
		if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
110
			$this->readOnlyReason = $conf['readOnlyReason'];
111
		}
112
113
		$this->srvCache = isset( $conf['srvCache'] ) ? $conf['srvCache'] : new EmptyBagOStuff();
114
		$this->memCache = isset( $conf['memCache'] ) ? $conf['memCache'] : new EmptyBagOStuff();
115
		$this->wanCache = isset( $conf['wanCache'] )
116
			? $conf['wanCache']
117
			: WANObjectCache::newEmpty();
118
119
		foreach ( self::$loggerFields as $key ) {
120
			$this->$key = isset( $conf[$key] ) ? $conf[$key] : new \Psr\Log\NullLogger();
121
		}
122
		$this->errorLogger = isset( $conf['errorLogger'] )
123
			? $conf['errorLogger']
124
			: function ( Exception $e ) {
125
				trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
126
			};
127
128
		$this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null;
0 ignored issues
show
Bug introduced by
The variable $params seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
129
		$this->trxProfiler = isset( $conf['trxProfiler'] )
130
			? $conf['trxProfiler']
131
			: new TransactionProfiler();
132
133
		$this->requestInfo = [
134
			'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
135
			'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
136
			'ChronologyProtection' => 'true'
137
		];
138
139
		$this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
140
		$this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
141
		$this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
142
143
		$this->ticket = mt_rand();
144
	}
145
146
	/**
147
	 * Disables all load balancers. All connections are closed, and any attempt to
148
	 * open a new connection will result in a DBAccessError.
149
	 * @see ILoadBalancer::disable()
150
	 */
151
	public function destroy() {
152
		$this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
153
		$this->forEachLBCallMethod( 'disable' );
154
	}
155
156
	/**
157
	 * Create a new load balancer object. The resulting object will be untracked,
158
	 * not chronology-protected, and the caller is responsible for cleaning it up.
159
	 *
160
	 * @param bool|string $domain Domain ID, or false for the current domain
161
	 * @return ILoadBalancer
162
	 */
163
	abstract public function newMainLB( $domain = false );
164
165
	/**
166
	 * Get a cached (tracked) load balancer object.
167
	 *
168
	 * @param bool|string $domain Domain ID, or false for the current domain
169
	 * @return ILoadBalancer
170
	 */
171
	abstract public function getMainLB( $domain = false );
172
173
	/**
174
	 * Create a new load balancer for external storage. The resulting object will be
175
	 * untracked, not chronology-protected, and the caller is responsible for
176
	 * cleaning it up.
177
	 *
178
	 * @param string $cluster External storage cluster, or false for core
179
	 * @param bool|string $domain Domain ID, or false for the current domain
180
	 * @return ILoadBalancer
181
	 */
182
	abstract protected function newExternalLB( $cluster, $domain = false );
183
184
	/**
185
	 * Get a cached (tracked) load balancer for external storage
186
	 *
187
	 * @param string $cluster External storage cluster, or false for core
188
	 * @param bool|string $domain Domain ID, or false for the current domain
189
	 * @return ILoadBalancer
190
	 */
191
	abstract public function getExternalLB( $cluster, $domain = false );
192
193
	/**
194
	 * Execute a function for each tracked load balancer
195
	 * The callback is called with the load balancer as the first parameter,
196
	 * and $params passed as the subsequent parameters.
197
	 *
198
	 * @param callable $callback
199
	 * @param array $params
200
	 */
201
	abstract public function forEachLB( $callback, array $params = [] );
202
203
	/**
204
	 * Prepare all tracked load balancers for shutdown
205
	 * @param integer $mode One of the class SHUTDOWN_* constants
206
	 * @param callable|null $workCallback Work to mask ChronologyProtector writes
207
	 */
208
	public function shutdown(
209
		$mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
210
	) {
211
		$chronProt = $this->getChronologyProtector();
212
		if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
213
			$this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
214
		} elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
215
			$this->shutdownChronologyProtector( $chronProt, null, 'async' );
216
		}
217
218
		$this->commitMasterChanges( __METHOD__ ); // sanity
219
	}
220
221
	/**
222
	 * Call a method of each tracked load balancer
223
	 *
224
	 * @param string $methodName
225
	 * @param array $args
226
	 */
227
	protected function forEachLBCallMethod( $methodName, array $args = [] ) {
228
		$this->forEachLB(
229
			function ( ILoadBalancer $loadBalancer, $methodName, array $args ) {
230
				call_user_func_array( [ $loadBalancer, $methodName ], $args );
231
			},
232
			[ $methodName, $args ]
233
		);
234
	}
235
236
	/**
237
	 * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
238
	 *
239
	 * @param string $fname Caller name
240
	 * @since 1.28
241
	 */
242
	public function flushReplicaSnapshots( $fname = __METHOD__ ) {
243
		$this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
244
	}
245
246
	/**
247
	 * Commit on all connections. Done for two reasons:
248
	 * 1. To commit changes to the masters.
249
	 * 2. To release the snapshot on all connections, master and replica DB.
250
	 * @param string $fname Caller name
251
	 * @param array $options Options map:
252
	 *   - maxWriteDuration: abort if more than this much time was spent in write queries
253
	 */
254
	public function commitAll( $fname = __METHOD__, array $options = [] ) {
255
		$this->commitMasterChanges( $fname, $options );
256
		$this->forEachLBCallMethod( 'commitAll', [ $fname ] );
257
	}
258
259
	/**
260
	 * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
261
	 *
262
	 * The DBO_TRX setting will be reverted to the default in each of these methods:
263
	 *   - commitMasterChanges()
264
	 *   - rollbackMasterChanges()
265
	 *   - commitAll()
266
	 *
267
	 * This allows for custom transaction rounds from any outer transaction scope.
268
	 *
269
	 * @param string $fname
270
	 * @throws DBTransactionError
271
	 * @since 1.28
272
	 */
273
	public function beginMasterChanges( $fname = __METHOD__ ) {
274
		if ( $this->trxRoundId !== false ) {
275
			throw new DBTransactionError(
276
				null,
277
				"$fname: transaction round '{$this->trxRoundId}' already started."
278
			);
279
		}
280
		$this->trxRoundId = $fname;
281
		// Set DBO_TRX flags on all appropriate DBs
282
		$this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
283
	}
284
285
	/**
286
	 * Commit changes on all master connections
287
	 * @param string $fname Caller name
288
	 * @param array $options Options map:
289
	 *   - maxWriteDuration: abort if more than this much time was spent in write queries
290
	 * @throws Exception
291
	 */
292
	public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
293
		if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) {
294
			throw new DBTransactionError(
295
				null,
296
				"$fname: transaction round '{$this->trxRoundId}' still running."
297
			);
298
		}
299
		// Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
300
		$this->forEachLBCallMethod( 'finalizeMasterChanges' );
301
		$this->trxRoundId = false;
302
		// Perform pre-commit checks, aborting on failure
303
		$this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
304
		// Log the DBs and methods involved in multi-DB transactions
305
		$this->logIfMultiDbTransaction();
306
		// Actually perform the commit on all master DB connections and revert DBO_TRX
307
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
308
		// Run all post-commit callbacks
309
		/** @var Exception $e */
310
		$e = null; // first callback exception
311
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
312
			$ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT );
313
			$e = $e ?: $ex;
314
		} );
315
		// Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB
316
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
317
		// Throw any last post-commit callback error
318
		if ( $e instanceof Exception ) {
319
			throw $e;
320
		}
321
	}
322
323
	/**
324
	 * Rollback changes on all master connections
325
	 * @param string $fname Caller name
326
	 * @since 1.23
327
	 */
328
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
329
		$this->trxRoundId = false;
330
		$this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
331
		$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
332
		// Run all post-rollback callbacks
333
		$this->forEachLB( function ( ILoadBalancer $lb ) {
334
			$lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK );
335
		} );
336
	}
337
338
	/**
339
	 * Log query info if multi DB transactions are going to be committed now
340
	 */
341
	private function logIfMultiDbTransaction() {
342
		$callersByDB = [];
343
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$callersByDB ) {
344
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
345
			$callers = $lb->pendingMasterChangeCallers();
346
			if ( $callers ) {
347
				$callersByDB[$masterName] = $callers;
348
			}
349
		} );
350
351
		if ( count( $callersByDB ) >= 2 ) {
352
			$dbs = implode( ', ', array_keys( $callersByDB ) );
353
			$msg = "Multi-DB transaction [{$dbs}]:\n";
354
			foreach ( $callersByDB as $db => $callers ) {
355
				$msg .= "$db: " . implode( '; ', $callers ) . "\n";
356
			}
357
			$this->queryLogger->info( $msg );
358
		}
359
	}
360
361
	/**
362
	 * Determine if any master connection has pending changes
363
	 * @return bool
364
	 * @since 1.23
365
	 */
366
	public function hasMasterChanges() {
367
		$ret = false;
368
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
369
			$ret = $ret || $lb->hasMasterChanges();
370
		} );
371
372
		return $ret;
373
	}
374
375
	/**
376
	 * Detemine if any lagged replica DB connection was used
377
	 * @return bool
378
	 * @since 1.28
379
	 */
380
	public function laggedReplicaUsed() {
381
		$ret = false;
382
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
383
			$ret = $ret || $lb->laggedReplicaUsed();
384
		} );
385
386
		return $ret;
387
	}
388
389
	/**
390
	 * Determine if any master connection has pending/written changes from this request
391
	 * @param float $age How many seconds ago is "recent" [defaults to LB lag wait timeout]
392
	 * @return bool
393
	 * @since 1.27
394
	 */
395
	public function hasOrMadeRecentMasterChanges( $age = null ) {
396
		$ret = false;
397
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $age, &$ret ) {
398
			$ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age );
399
		} );
400
		return $ret;
401
	}
402
403
	/**
404
	 * Waits for the replica DBs to catch up to the current master position
405
	 *
406
	 * Use this when updating very large numbers of rows, as in maintenance scripts,
407
	 * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs.
408
	 *
409
	 * By default this waits on all DB clusters actually used in this request.
410
	 * This makes sense when lag being waiting on is caused by the code that does this check.
411
	 * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters
412
	 * that were not changed since the last wait check. To forcefully wait on a specific cluster
413
	 * for a given wiki, use the 'wiki' parameter. To forcefully wait on an "external" cluster,
414
	 * use the "cluster" parameter.
415
	 *
416
	 * Never call this function after a large DB write that is *still* in a transaction.
417
	 * It only makes sense to call this after the possible lag inducing changes were committed.
418
	 *
419
	 * @param array $opts Optional fields that include:
420
	 *   - wiki : wait on the load balancer DBs that handles the given wiki
421
	 *   - cluster : wait on the given external load balancer DBs
422
	 *   - timeout : Max wait time. Default: ~60 seconds
423
	 *   - ifWritesSince: Only wait if writes were done since this UNIX timestamp
424
	 * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster
425
	 * @since 1.27
426
	 */
427
	public function waitForReplication( array $opts = [] ) {
428
		$opts += [
429
			'wiki' => false,
430
			'cluster' => false,
431
			'timeout' => 60,
432
			'ifWritesSince' => null
433
		];
434
435
		// Figure out which clusters need to be checked
436
		/** @var ILoadBalancer[] $lbs */
437
		$lbs = [];
438
		if ( $opts['cluster'] !== false ) {
439
			$lbs[] = $this->getExternalLB( $opts['cluster'] );
440
		} elseif ( $opts['wiki'] !== false ) {
441
			$lbs[] = $this->getMainLB( $opts['wiki'] );
442
		} else {
443
			$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) {
444
				$lbs[] = $lb;
445
			} );
446
			if ( !$lbs ) {
447
				return; // nothing actually used
448
			}
449
		}
450
451
		// Get all the master positions of applicable DBs right now.
452
		// This can be faster since waiting on one cluster reduces the
453
		// time needed to wait on the next clusters.
454
		$masterPositions = array_fill( 0, count( $lbs ), false );
455
		foreach ( $lbs as $i => $lb ) {
456
			if ( $lb->getServerCount() <= 1 ) {
457
				// Bug 27975 - Don't try to wait for replica DBs if there are none
458
				// Prevents permission error when getting master position
459
				continue;
460
			} elseif ( $opts['ifWritesSince']
461
				&& $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
462
			) {
463
				continue; // no writes since the last wait
464
			}
465
			$masterPositions[$i] = $lb->getMasterPos();
466
		}
467
468
		// Run any listener callbacks *after* getting the DB positions. The more
469
		// time spent in the callbacks, the less time is spent in waitForAll().
470
		foreach ( $this->replicationWaitCallbacks as $callback ) {
471
			$callback();
472
		}
473
474
		$failed = [];
475
		foreach ( $lbs as $i => $lb ) {
476
			if ( $masterPositions[$i] ) {
477
				// The DBMS may not support getMasterPos()
478
				if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
479
					$failed[] = $lb->getServerName( $lb->getWriterIndex() );
480
				}
481
			}
482
		}
483
484
		if ( $failed ) {
485
			throw new DBReplicationWaitError(
0 ignored issues
show
Bug introduced by
The call to DBReplicationWaitError::__construct() misses a required argument $error.

This check looks for function calls that miss required arguments.

Loading history...
486
				"Could not wait for replica DBs to catch up to " .
487
				implode( ', ', $failed )
488
			);
489
		}
490
	}
491
492
	/**
493
	 * Add a callback to be run in every call to waitForReplication() before waiting
494
	 *
495
	 * Callbacks must clear any transactions that they start
496
	 *
497
	 * @param string $name Callback name
498
	 * @param callable|null $callback Use null to unset a callback
499
	 * @since 1.28
500
	 */
501
	public function setWaitForReplicationListener( $name, callable $callback = null ) {
502
		if ( $callback ) {
503
			$this->replicationWaitCallbacks[$name] = $callback;
504
		} else {
505
			unset( $this->replicationWaitCallbacks[$name] );
506
		}
507
	}
508
509
	/**
510
	 * Get a token asserting that no transaction writes are active
511
	 *
512
	 * @param string $fname Caller name (e.g. __METHOD__)
513
	 * @return mixed A value to pass to commitAndWaitForReplication()
514
	 * @since 1.28
515
	 */
516
	public function getEmptyTransactionTicket( $fname ) {
517
		if ( $this->hasMasterChanges() ) {
518
			$this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope." );
519
			return null;
520
		}
521
522
		return $this->ticket;
523
	}
524
525
	/**
526
	 * Convenience method for safely running commitMasterChanges()/waitForReplication()
527
	 *
528
	 * This will commit and wait unless $ticket indicates it is unsafe to do so
529
	 *
530
	 * @param string $fname Caller name (e.g. __METHOD__)
531
	 * @param mixed $ticket Result of getEmptyTransactionTicket()
532
	 * @param array $opts Options to waitForReplication()
533
	 * @throws DBReplicationWaitError
534
	 * @since 1.28
535
	 */
536
	public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
537
		if ( $ticket !== $this->ticket ) {
538
			$this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope." );
539
			return;
540
		}
541
542
		// The transaction owner and any caller with the empty transaction ticket can commit
543
		// so that getEmptyTransactionTicket() callers don't risk seeing DBTransactionError.
544
		if ( $this->trxRoundId !== false && $fname !== $this->trxRoundId ) {
545
			$this->queryLogger->info( "$fname: committing on behalf of {$this->trxRoundId}." );
546
			$fnameEffective = $this->trxRoundId;
547
		} else {
548
			$fnameEffective = $fname;
549
		}
550
551
		$this->commitMasterChanges( $fnameEffective );
0 ignored issues
show
Bug introduced by
It seems like $fnameEffective defined by $this->trxRoundId on line 546 can also be of type boolean; however, LBFactory::commitMasterChanges() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
552
		$this->waitForReplication( $opts );
553
		// If a nested caller committed on behalf of $fname, start another empty $fname
554
		// transaction, leaving the caller with the same empty transaction state as before.
555
		if ( $fnameEffective !== $fname ) {
556
			$this->beginMasterChanges( $fnameEffective );
0 ignored issues
show
Bug introduced by
It seems like $fnameEffective defined by $this->trxRoundId on line 546 can also be of type boolean; however, LBFactory::beginMasterChanges() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
557
		}
558
	}
559
560
	/**
561
	 * @param string $dbName DB master name (e.g. "db1052")
562
	 * @return float|bool UNIX timestamp when client last touched the DB or false if not recent
563
	 * @since 1.28
564
	 */
565
	public function getChronologyProtectorTouched( $dbName ) {
566
		return $this->getChronologyProtector()->getTouched( $dbName );
567
	}
568
569
	/**
570
	 * Disable the ChronologyProtector for all load balancers
571
	 *
572
	 * This can be called at the start of special API entry points
573
	 *
574
	 * @since 1.27
575
	 */
576
	public function disableChronologyProtection() {
577
		$this->getChronologyProtector()->setEnabled( false );
578
	}
579
580
	/**
581
	 * @return ChronologyProtector
582
	 */
583
	protected function getChronologyProtector() {
0 ignored issues
show
Coding Style introduced by
getChronologyProtector uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
584
		if ( $this->chronProt ) {
585
			return $this->chronProt;
586
		}
587
588
		$this->chronProt = new ChronologyProtector(
589
			$this->memCache,
590
			[
591
				'ip' => $this->requestInfo['IPAddress'],
592
				'agent' => $this->requestInfo['UserAgent'],
593
			],
594
			isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
595
		);
596
		$this->chronProt->setLogger( $this->replLogger );
597
598
		if ( $this->cliMode ) {
599
			$this->chronProt->setEnabled( false );
600
		} elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
601
			// Request opted out of using position wait logic. This is useful for requests
602
			// done by the job queue or background ETL that do not have a meaningful session.
603
			$this->chronProt->setWaitEnabled( false );
604
		}
605
606
		$this->replLogger->debug( __METHOD__ . ': using request info ' .
607
			json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
608
609
		return $this->chronProt;
610
	}
611
612
	/**
613
	 * Get and record all of the staged DB positions into persistent memory storage
614
	 *
615
	 * @param ChronologyProtector $cp
616
	 * @param callable|null $workCallback Work to do instead of waiting on syncing positions
617
	 * @param string $mode One of (sync, async); whether to wait on remote datacenters
618
	 */
619
	protected function shutdownChronologyProtector(
620
		ChronologyProtector $cp, $workCallback, $mode
621
	) {
622
		// Record all the master positions needed
623
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) {
624
			$cp->shutdownLB( $lb );
625
		} );
626
		// Write them to the persistent stash. Try to do something useful by running $work
627
		// while ChronologyProtector waits for the stash write to replicate to all DCs.
628
		$unsavedPositions = $cp->shutdown( $workCallback, $mode );
629
		if ( $unsavedPositions && $workCallback ) {
630
			// Invoke callback in case it did not cache the result yet
631
			$workCallback(); // work now to block for less time in waitForAll()
632
		}
633
		// If the positions failed to write to the stash, at least wait on local datacenter
634
		// replica DBs to catch up before responding. Even if there are several DCs, this increases
635
		// the chance that the user will see their own changes immediately afterwards. As long
636
		// as the sticky DC cookie applies (same domain), this is not even an issue.
637
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $unsavedPositions ) {
638
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
639
			if ( isset( $unsavedPositions[$masterName] ) ) {
640
				$lb->waitForAll( $unsavedPositions[$masterName] );
641
			}
642
		} );
643
	}
644
645
	/**
646
	 * Base parameters to LoadBalancer::__construct()
647
	 * @return array
648
	 */
649
	final protected function baseLoadBalancerParams() {
650
		return [
651
			'localDomain' => $this->localDomain,
652
			'readOnlyReason' => $this->readOnlyReason,
653
			'srvCache' => $this->srvCache,
654
			'wanCache' => $this->wanCache,
655
			'profiler' => $this->profiler,
656
			'trxProfiler' => $this->trxProfiler,
657
			'queryLogger' => $this->queryLogger,
658
			'connLogger' => $this->connLogger,
659
			'replLogger' => $this->replLogger,
660
			'errorLogger' => $this->errorLogger,
661
			'hostname' => $this->hostname,
662
			'cliMode' => $this->cliMode,
663
			'agent' => $this->agent
664
		];
665
	}
666
667
	/**
668
	 * @param ILoadBalancer $lb
669
	 */
670
	protected function initLoadBalancer( ILoadBalancer $lb ) {
671
		if ( $this->trxRoundId !== false ) {
672
			$lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
0 ignored issues
show
Bug introduced by
It seems like $this->trxRoundId can also be of type boolean; however, ILoadBalancer::beginMasterChanges() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
673
		}
674
	}
675
676
	/**
677
	 * Set a new table prefix for the existing local domain ID for testing
678
	 *
679
	 * @param string $prefix
680
	 * @since 1.28
681
	 */
682
	public function setDomainPrefix( $prefix ) {
683
		$this->localDomain = new DatabaseDomain(
684
			$this->localDomain->getDatabase(),
685
			null,
686
			$prefix
687
		);
688
689
		$this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
690
			$lb->setDomainPrefix( $prefix );
691
		} );
692
	}
693
694
	/**
695
	 * Close all open database connections on all open load balancers.
696
	 * @since 1.28
697
	 */
698
	public function closeAll() {
699
		$this->forEachLBCallMethod( 'closeAll', [] );
700
	}
701
702
	/**
703
	 * @param string $agent Agent name for query profiling
704
	 * @since 1.28
705
	 */
706
	public function setAgentName( $agent ) {
707
		$this->agent = $agent;
708
	}
709
710
	/**
711
	 * Append ?cpPosTime parameter to a URL for ChronologyProtector purposes if needed
712
	 *
713
	 * Note that unlike cookies, this works accross domains
714
	 *
715
	 * @param string $url
716
	 * @param float $time UNIX timestamp just before shutdown() was called
717
	 * @return string
718
	 * @since 1.28
719
	 */
720
	public function appendPreShutdownTimeAsQuery( $url, $time ) {
721
		$usedCluster = 0;
722
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
723
			$usedCluster |= ( $lb->getServerCount() > 1 );
724
		} );
725
726
		if ( !$usedCluster ) {
727
			return $url; // no master/replica clusters touched
728
		}
729
730
		return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
731
	}
732
733
	/**
734
	 * @param array $info Map of fields, including:
735
	 *   - IPAddress : IP address
736
	 *   - UserAgent : User-Agent HTTP header
737
	 *   - ChronologyProtection : cookie/header value specifying ChronologyProtector usage
738
	 * @since 1.28
739
	 */
740
	public function setRequestInfo( array $info ) {
741
		$this->requestInfo = $info + $this->requestInfo;
742
	}
743
744
	function __destruct() {
745
		$this->destroy();
746
	}
747
}
748