LBFactory   D
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 541
Duplicated Lines 2.4 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
dl 13
loc 541
rs 4.5142
c 0
b 0
f 0
wmc 90
lcom 1
cbo 11

34 Methods

Rating   Name   Duplication   Size   Complexity  
newExternalLB() 0 1 ?
A setWaitForReplicationListener() 0 7 2
A getEmptyTransactionTicket() 0 10 2
B commitAndWaitForReplication() 0 25 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 getScopedPHPBehaviorForCommit() 10 10 2
A __destruct() 0 3 1
F __construct() 3 41 17
A destroy() 0 4 1
A shutdown() 0 12 3
newMainLB() 0 1 ?
getMainLB() 0 1 ?
getExternalLB() 0 1 ?
A forEachLBCallMethod() 0 8 1
A flushReplicaSnapshots() 0 3 1
A commitAll() 0 4 1
A beginMasterChanges() 0 11 2
B commitMasterChanges() 0 32 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
D waitForReplication() 0 69 15

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
use Wikimedia\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
26
27
/**
28
 * An interface for generating database load balancers
29
 * @ingroup Database
30
 */
31
abstract class LBFactory implements ILBFactory {
32
	/** @var ChronologyProtector */
33
	protected $chronProt;
34
	/** @var object|string Class name or object With profileIn/profileOut methods */
35
	protected $profiler;
36
	/** @var TransactionProfiler */
37
	protected $trxProfiler;
38
	/** @var LoggerInterface */
39
	protected $replLogger;
40
	/** @var LoggerInterface */
41
	protected $connLogger;
42
	/** @var LoggerInterface */
43
	protected $queryLogger;
44
	/** @var LoggerInterface */
45
	protected $perfLogger;
46
	/** @var callable Error logger */
47
	protected $errorLogger;
48
	/** @var BagOStuff */
49
	protected $srvCache;
50
	/** @var BagOStuff */
51
	protected $memCache;
52
	/** @var WANObjectCache */
53
	protected $wanCache;
54
55
	/** @var DatabaseDomain Local domain */
56
	protected $localDomain;
57
	/** @var string Local hostname of the app server */
58
	protected $hostname;
59
	/** @var array Web request information about the client */
60
	protected $requestInfo;
61
62
	/** @var mixed */
63
	protected $ticket;
64
	/** @var string|bool String if a requested DBO_TRX transaction round is active */
65
	protected $trxRoundId = false;
66
	/** @var string|bool Reason all LBs are read-only or false if not */
67
	protected $readOnlyReason = false;
68
	/** @var callable[] */
69
	protected $replicationWaitCallbacks = [];
70
71
	/** @var bool Whether this PHP instance is for a CLI script */
72
	protected $cliMode;
73
	/** @var string Agent name for query profiling */
74
	protected $agent;
75
76
	private static $loggerFields =
77
		[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
78
79
	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...
80
		$this->localDomain = isset( $conf['localDomain'] )
81
			? DatabaseDomain::newFromId( $conf['localDomain'] )
82
			: DatabaseDomain::newUnspecified();
83
84 View Code Duplication
		if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
85
			$this->readOnlyReason = $conf['readOnlyReason'];
86
		}
87
88
		$this->srvCache = isset( $conf['srvCache'] ) ? $conf['srvCache'] : new EmptyBagOStuff();
89
		$this->memCache = isset( $conf['memCache'] ) ? $conf['memCache'] : new EmptyBagOStuff();
90
		$this->wanCache = isset( $conf['wanCache'] )
91
			? $conf['wanCache']
92
			: WANObjectCache::newEmpty();
93
94
		foreach ( self::$loggerFields as $key ) {
95
			$this->$key = isset( $conf[$key] ) ? $conf[$key] : new \Psr\Log\NullLogger();
96
		}
97
		$this->errorLogger = isset( $conf['errorLogger'] )
98
			? $conf['errorLogger']
99
			: function ( Exception $e ) {
100
				trigger_error( E_USER_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
101
			};
102
103
		$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...
104
		$this->trxProfiler = isset( $conf['trxProfiler'] )
105
			? $conf['trxProfiler']
106
			: new TransactionProfiler();
107
108
		$this->requestInfo = [
109
			'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
110
			'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
111
			'ChronologyProtection' => 'true'
112
		];
113
114
		$this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
115
		$this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
116
		$this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
117
118
		$this->ticket = mt_rand();
119
	}
120
121
	public function destroy() {
122
		$this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
123
		$this->forEachLBCallMethod( 'disable' );
124
	}
125
126
	public function shutdown(
127
		$mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
128
	) {
129
		$chronProt = $this->getChronologyProtector();
130
		if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
131
			$this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
132
		} elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
133
			$this->shutdownChronologyProtector( $chronProt, null, 'async' );
134
		}
135
136
		$this->commitMasterChanges( __METHOD__ ); // sanity
137
	}
138
139
	/**
140
	 * @see ILBFactory::newMainLB()
141
	 * @param bool $domain
142
	 * @return LoadBalancer
143
	 */
144
	abstract public function newMainLB( $domain = false );
145
146
	/**
147
	 * @see ILBFactory::getMainLB()
148
	 * @param bool $domain
149
	 * @return LoadBalancer
150
	 */
151
	abstract public function getMainLB( $domain = false );
152
153
	/**
154
	 * @see ILBFactory::newExternalLB()
155
	 * @param string $cluster
156
	 * @return LoadBalancer
157
	 */
158
	abstract public function newExternalLB( $cluster );
159
160
	/**
161
	 * @see ILBFactory::getExternalLB()
162
	 * @param string $cluster
163
	 * @return LoadBalancer
164
	 */
165
	abstract public function getExternalLB( $cluster );
166
167
	/**
168
	 * Call a method of each tracked load balancer
169
	 *
170
	 * @param string $methodName
171
	 * @param array $args
172
	 */
173
	protected function forEachLBCallMethod( $methodName, array $args = [] ) {
174
		$this->forEachLB(
175
			function ( ILoadBalancer $loadBalancer, $methodName, array $args ) {
176
				call_user_func_array( [ $loadBalancer, $methodName ], $args );
177
			},
178
			[ $methodName, $args ]
179
		);
180
	}
181
182
	public function flushReplicaSnapshots( $fname = __METHOD__ ) {
183
		$this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
184
	}
185
186
	public function commitAll( $fname = __METHOD__, array $options = [] ) {
187
		$this->commitMasterChanges( $fname, $options );
188
		$this->forEachLBCallMethod( 'commitAll', [ $fname ] );
189
	}
190
191
	public function beginMasterChanges( $fname = __METHOD__ ) {
192
		if ( $this->trxRoundId !== false ) {
193
			throw new DBTransactionError(
194
				null,
195
				"$fname: transaction round '{$this->trxRoundId}' already started."
196
			);
197
		}
198
		$this->trxRoundId = $fname;
199
		// Set DBO_TRX flags on all appropriate DBs
200
		$this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
201
	}
202
203
	public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
204
		if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) {
205
			throw new DBTransactionError(
206
				null,
207
				"$fname: transaction round '{$this->trxRoundId}' still running."
208
			);
209
		}
210
		/** @noinspection PhpUnusedLocalVariableInspection */
211
		$scope = $this->getScopedPHPBehaviorForCommit(); // try to ignore client aborts
0 ignored issues
show
Unused Code introduced by
$scope is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
212
		// Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
213
		$this->forEachLBCallMethod( 'finalizeMasterChanges' );
214
		$this->trxRoundId = false;
215
		// Perform pre-commit checks, aborting on failure
216
		$this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
217
		// Log the DBs and methods involved in multi-DB transactions
218
		$this->logIfMultiDbTransaction();
219
		// Actually perform the commit on all master DB connections and revert DBO_TRX
220
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
221
		// Run all post-commit callbacks
222
		/** @var Exception $e */
223
		$e = null; // first callback exception
224
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
225
			$ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT );
226
			$e = $e ?: $ex;
227
		} );
228
		// Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB
229
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
230
		// Throw any last post-commit callback error
231
		if ( $e instanceof Exception ) {
232
			throw $e;
233
		}
234
	}
235
236
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
237
		$this->trxRoundId = false;
238
		$this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
239
		$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
240
		// Run all post-rollback callbacks
241
		$this->forEachLB( function ( ILoadBalancer $lb ) {
242
			$lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK );
243
		} );
244
	}
245
246
	/**
247
	 * Log query info if multi DB transactions are going to be committed now
248
	 */
249
	private function logIfMultiDbTransaction() {
250
		$callersByDB = [];
251
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$callersByDB ) {
252
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
253
			$callers = $lb->pendingMasterChangeCallers();
254
			if ( $callers ) {
255
				$callersByDB[$masterName] = $callers;
256
			}
257
		} );
258
259
		if ( count( $callersByDB ) >= 2 ) {
260
			$dbs = implode( ', ', array_keys( $callersByDB ) );
261
			$msg = "Multi-DB transaction [{$dbs}]:\n";
262
			foreach ( $callersByDB as $db => $callers ) {
263
				$msg .= "$db: " . implode( '; ', $callers ) . "\n";
264
			}
265
			$this->queryLogger->info( $msg );
266
		}
267
	}
268
269
	public function hasMasterChanges() {
270
		$ret = false;
271
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
272
			$ret = $ret || $lb->hasMasterChanges();
273
		} );
274
275
		return $ret;
276
	}
277
278
	public function laggedReplicaUsed() {
279
		$ret = false;
280
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
281
			$ret = $ret || $lb->laggedReplicaUsed();
282
		} );
283
284
		return $ret;
285
	}
286
287
	public function hasOrMadeRecentMasterChanges( $age = null ) {
288
		$ret = false;
289
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $age, &$ret ) {
290
			$ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age );
291
		} );
292
		return $ret;
293
	}
294
295
	public function waitForReplication( array $opts = [] ) {
296
		$opts += [
297
			'domain' => false,
298
			'cluster' => false,
299
			'timeout' => 60,
300
			'ifWritesSince' => null
301
		];
302
303
		if ( $opts['domain'] === false && isset( $opts['wiki'] ) ) {
304
			$opts['domain'] = $opts['wiki']; // b/c
305
		}
306
307
		// Figure out which clusters need to be checked
308
		/** @var ILoadBalancer[] $lbs */
309
		$lbs = [];
310
		if ( $opts['cluster'] !== false ) {
311
			$lbs[] = $this->getExternalLB( $opts['cluster'] );
312
		} elseif ( $opts['domain'] !== false ) {
313
			$lbs[] = $this->getMainLB( $opts['domain'] );
314
		} else {
315
			$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) {
316
				$lbs[] = $lb;
317
			} );
318
			if ( !$lbs ) {
319
				return; // nothing actually used
320
			}
321
		}
322
323
		// Get all the master positions of applicable DBs right now.
324
		// This can be faster since waiting on one cluster reduces the
325
		// time needed to wait on the next clusters.
326
		$masterPositions = array_fill( 0, count( $lbs ), false );
327
		foreach ( $lbs as $i => $lb ) {
328
			if ( $lb->getServerCount() <= 1 ) {
329
				// Bug 27975 - Don't try to wait for replica DBs if there are none
330
				// Prevents permission error when getting master position
331
				continue;
332
			} elseif ( $opts['ifWritesSince']
333
				&& $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
334
			) {
335
				continue; // no writes since the last wait
336
			}
337
			$masterPositions[$i] = $lb->getMasterPos();
338
		}
339
340
		// Run any listener callbacks *after* getting the DB positions. The more
341
		// time spent in the callbacks, the less time is spent in waitForAll().
342
		foreach ( $this->replicationWaitCallbacks as $callback ) {
343
			$callback();
344
		}
345
346
		$failed = [];
347
		foreach ( $lbs as $i => $lb ) {
348
			if ( $masterPositions[$i] ) {
349
				// The DBMS may not support getMasterPos()
350
				if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
351
					$failed[] = $lb->getServerName( $lb->getWriterIndex() );
352
				}
353
			}
354
		}
355
356
		if ( $failed ) {
357
			throw new DBReplicationWaitError(
358
				null,
359
				"Could not wait for replica DBs to catch up to " .
360
				implode( ', ', $failed )
361
			);
362
		}
363
	}
364
365
	public function setWaitForReplicationListener( $name, callable $callback = null ) {
366
		if ( $callback ) {
367
			$this->replicationWaitCallbacks[$name] = $callback;
368
		} else {
369
			unset( $this->replicationWaitCallbacks[$name] );
370
		}
371
	}
372
373
	public function getEmptyTransactionTicket( $fname ) {
374
		if ( $this->hasMasterChanges() ) {
375
			$this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
376
				( new RuntimeException() )->getTraceAsString() );
377
378
			return null;
379
		}
380
381
		return $this->ticket;
382
	}
383
384
	public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
385
		if ( $ticket !== $this->ticket ) {
386
			$this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
387
				( new RuntimeException() )->getTraceAsString() );
388
389
			return;
390
		}
391
392
		// The transaction owner and any caller with the empty transaction ticket can commit
393
		// so that getEmptyTransactionTicket() callers don't risk seeing DBTransactionError.
394
		if ( $this->trxRoundId !== false && $fname !== $this->trxRoundId ) {
395
			$this->queryLogger->info( "$fname: committing on behalf of {$this->trxRoundId}." );
396
			$fnameEffective = $this->trxRoundId;
397
		} else {
398
			$fnameEffective = $fname;
399
		}
400
401
		$this->commitMasterChanges( $fnameEffective );
0 ignored issues
show
Bug introduced by
It seems like $fnameEffective defined by $this->trxRoundId on line 396 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...
402
		$this->waitForReplication( $opts );
403
		// If a nested caller committed on behalf of $fname, start another empty $fname
404
		// transaction, leaving the caller with the same empty transaction state as before.
405
		if ( $fnameEffective !== $fname ) {
406
			$this->beginMasterChanges( $fnameEffective );
0 ignored issues
show
Bug introduced by
It seems like $fnameEffective defined by $this->trxRoundId on line 396 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...
407
		}
408
	}
409
410
	public function getChronologyProtectorTouched( $dbName ) {
411
		return $this->getChronologyProtector()->getTouched( $dbName );
412
	}
413
414
	public function disableChronologyProtection() {
415
		$this->getChronologyProtector()->setEnabled( false );
416
	}
417
418
	/**
419
	 * @return ChronologyProtector
420
	 */
421
	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...
422
		if ( $this->chronProt ) {
423
			return $this->chronProt;
424
		}
425
426
		$this->chronProt = new ChronologyProtector(
427
			$this->memCache,
428
			[
429
				'ip' => $this->requestInfo['IPAddress'],
430
				'agent' => $this->requestInfo['UserAgent'],
431
			],
432
			isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
433
		);
434
		$this->chronProt->setLogger( $this->replLogger );
435
436
		if ( $this->cliMode ) {
437
			$this->chronProt->setEnabled( false );
438
		} elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
439
			// Request opted out of using position wait logic. This is useful for requests
440
			// done by the job queue or background ETL that do not have a meaningful session.
441
			$this->chronProt->setWaitEnabled( false );
442
		}
443
444
		$this->replLogger->debug( __METHOD__ . ': using request info ' .
445
			json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
446
447
		return $this->chronProt;
448
	}
449
450
	/**
451
	 * Get and record all of the staged DB positions into persistent memory storage
452
	 *
453
	 * @param ChronologyProtector $cp
454
	 * @param callable|null $workCallback Work to do instead of waiting on syncing positions
455
	 * @param string $mode One of (sync, async); whether to wait on remote datacenters
456
	 */
457
	protected function shutdownChronologyProtector(
458
		ChronologyProtector $cp, $workCallback, $mode
459
	) {
460
		// Record all the master positions needed
461
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) {
462
			$cp->shutdownLB( $lb );
463
		} );
464
		// Write them to the persistent stash. Try to do something useful by running $work
465
		// while ChronologyProtector waits for the stash write to replicate to all DCs.
466
		$unsavedPositions = $cp->shutdown( $workCallback, $mode );
467
		if ( $unsavedPositions && $workCallback ) {
468
			// Invoke callback in case it did not cache the result yet
469
			$workCallback(); // work now to block for less time in waitForAll()
470
		}
471
		// If the positions failed to write to the stash, at least wait on local datacenter
472
		// replica DBs to catch up before responding. Even if there are several DCs, this increases
473
		// the chance that the user will see their own changes immediately afterwards. As long
474
		// as the sticky DC cookie applies (same domain), this is not even an issue.
475
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $unsavedPositions ) {
476
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
477
			if ( isset( $unsavedPositions[$masterName] ) ) {
478
				$lb->waitForAll( $unsavedPositions[$masterName] );
479
			}
480
		} );
481
	}
482
483
	/**
484
	 * Base parameters to LoadBalancer::__construct()
485
	 * @return array
486
	 */
487
	final protected function baseLoadBalancerParams() {
488
		return [
489
			'localDomain' => $this->localDomain,
490
			'readOnlyReason' => $this->readOnlyReason,
491
			'srvCache' => $this->srvCache,
492
			'wanCache' => $this->wanCache,
493
			'profiler' => $this->profiler,
494
			'trxProfiler' => $this->trxProfiler,
495
			'queryLogger' => $this->queryLogger,
496
			'connLogger' => $this->connLogger,
497
			'replLogger' => $this->replLogger,
498
			'errorLogger' => $this->errorLogger,
499
			'hostname' => $this->hostname,
500
			'cliMode' => $this->cliMode,
501
			'agent' => $this->agent
502
		];
503
	}
504
505
	/**
506
	 * @param ILoadBalancer $lb
507
	 */
508
	protected function initLoadBalancer( ILoadBalancer $lb ) {
509
		if ( $this->trxRoundId !== false ) {
510
			$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...
511
		}
512
	}
513
514
	public function setDomainPrefix( $prefix ) {
515
		$this->localDomain = new DatabaseDomain(
516
			$this->localDomain->getDatabase(),
517
			null,
518
			$prefix
519
		);
520
521
		$this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
522
			$lb->setDomainPrefix( $prefix );
523
		} );
524
	}
525
526
	public function closeAll() {
527
		$this->forEachLBCallMethod( 'closeAll', [] );
528
	}
529
530
	public function setAgentName( $agent ) {
531
		$this->agent = $agent;
532
	}
533
534
	public function appendPreShutdownTimeAsQuery( $url, $time ) {
535
		$usedCluster = 0;
536
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
537
			$usedCluster |= ( $lb->getServerCount() > 1 );
538
		} );
539
540
		if ( !$usedCluster ) {
541
			return $url; // no master/replica clusters touched
542
		}
543
544
		return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
545
	}
546
547
	public function setRequestInfo( array $info ) {
548
		$this->requestInfo = $info + $this->requestInfo;
549
	}
550
551
	/**
552
	 * Make PHP ignore user aborts/disconnects until the returned
553
	 * value leaves scope. This returns null and does nothing in CLI mode.
554
	 *
555
	 * @return ScopedCallback|null
556
	 */
557 View Code Duplication
	final protected function getScopedPHPBehaviorForCommit() {
558
		if ( PHP_SAPI != 'cli' ) { // https://bugs.php.net/bug.php?id=47540
559
			$old = ignore_user_abort( true ); // avoid half-finished operations
560
			return new ScopedCallback( function () use ( $old ) {
561
				ignore_user_abort( $old );
562
			} );
563
		}
564
565
		return null;
566
	}
567
568
	function __destruct() {
569
		$this->destroy();
570
	}
571
}
572