Completed
Branch master (7da094)
by
unknown
29:41
created

LBFactory::commitMasterChanges()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 3
nop 2
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
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 implements ILBFactory {
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
	private static $loggerFields =
76
		[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
77
78
	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...
79
		$this->localDomain = isset( $conf['localDomain'] )
80
			? DatabaseDomain::newFromId( $conf['localDomain'] )
81
			: DatabaseDomain::newUnspecified();
82
83 View Code Duplication
		if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
84
			$this->readOnlyReason = $conf['readOnlyReason'];
85
		}
86
87
		$this->srvCache = isset( $conf['srvCache'] ) ? $conf['srvCache'] : new EmptyBagOStuff();
88
		$this->memCache = isset( $conf['memCache'] ) ? $conf['memCache'] : new EmptyBagOStuff();
89
		$this->wanCache = isset( $conf['wanCache'] )
90
			? $conf['wanCache']
91
			: WANObjectCache::newEmpty();
92
93
		foreach ( self::$loggerFields as $key ) {
94
			$this->$key = isset( $conf[$key] ) ? $conf[$key] : new \Psr\Log\NullLogger();
95
		}
96
		$this->errorLogger = isset( $conf['errorLogger'] )
97
			? $conf['errorLogger']
98
			: function ( Exception $e ) {
99
				trigger_error( E_WARNING, get_class( $e ) . ': ' . $e->getMessage() );
100
			};
101
102
		$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...
103
		$this->trxProfiler = isset( $conf['trxProfiler'] )
104
			? $conf['trxProfiler']
105
			: new TransactionProfiler();
106
107
		$this->requestInfo = [
108
			'IPAddress' => isset( $_SERVER[ 'REMOTE_ADDR' ] ) ? $_SERVER[ 'REMOTE_ADDR' ] : '',
109
			'UserAgent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
110
			'ChronologyProtection' => 'true'
111
		];
112
113
		$this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli';
114
		$this->hostname = isset( $conf['hostname'] ) ? $conf['hostname'] : gethostname();
115
		$this->agent = isset( $params['agent'] ) ? $params['agent'] : '';
116
117
		$this->ticket = mt_rand();
118
	}
119
120
	public function destroy() {
121
		$this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
122
		$this->forEachLBCallMethod( 'disable' );
123
	}
124
125
	public function shutdown(
126
		$mode = self::SHUTDOWN_CHRONPROT_SYNC, callable $workCallback = null
127
	) {
128
		$chronProt = $this->getChronologyProtector();
129
		if ( $mode === self::SHUTDOWN_CHRONPROT_SYNC ) {
130
			$this->shutdownChronologyProtector( $chronProt, $workCallback, 'sync' );
131
		} elseif ( $mode === self::SHUTDOWN_CHRONPROT_ASYNC ) {
132
			$this->shutdownChronologyProtector( $chronProt, null, 'async' );
133
		}
134
135
		$this->commitMasterChanges( __METHOD__ ); // sanity
136
	}
137
138
	/**
139
	 * Call a method of each tracked load balancer
140
	 *
141
	 * @param string $methodName
142
	 * @param array $args
143
	 */
144
	protected function forEachLBCallMethod( $methodName, array $args = [] ) {
145
		$this->forEachLB(
146
			function ( ILoadBalancer $loadBalancer, $methodName, array $args ) {
147
				call_user_func_array( [ $loadBalancer, $methodName ], $args );
148
			},
149
			[ $methodName, $args ]
150
		);
151
	}
152
153
	public function flushReplicaSnapshots( $fname = __METHOD__ ) {
154
		$this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
155
	}
156
157
	public function commitAll( $fname = __METHOD__, array $options = [] ) {
158
		$this->commitMasterChanges( $fname, $options );
159
		$this->forEachLBCallMethod( 'commitAll', [ $fname ] );
160
	}
161
162
	public function beginMasterChanges( $fname = __METHOD__ ) {
163
		if ( $this->trxRoundId !== false ) {
164
			throw new DBTransactionError(
165
				null,
166
				"$fname: transaction round '{$this->trxRoundId}' already started."
167
			);
168
		}
169
		$this->trxRoundId = $fname;
170
		// Set DBO_TRX flags on all appropriate DBs
171
		$this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
172
	}
173
174
	public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
175
		if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) {
176
			throw new DBTransactionError(
177
				null,
178
				"$fname: transaction round '{$this->trxRoundId}' still running."
179
			);
180
		}
181
		/** @noinspection PhpUnusedLocalVariableInspection */
182
		$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...
183
		// Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
184
		$this->forEachLBCallMethod( 'finalizeMasterChanges' );
185
		$this->trxRoundId = false;
186
		// Perform pre-commit checks, aborting on failure
187
		$this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
188
		// Log the DBs and methods involved in multi-DB transactions
189
		$this->logIfMultiDbTransaction();
190
		// Actually perform the commit on all master DB connections and revert DBO_TRX
191
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
192
		// Run all post-commit callbacks
193
		/** @var Exception $e */
194
		$e = null; // first callback exception
195
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
196
			$ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT );
197
			$e = $e ?: $ex;
198
		} );
199
		// Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB
200
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
201
		// Throw any last post-commit callback error
202
		if ( $e instanceof Exception ) {
203
			throw $e;
204
		}
205
	}
206
207
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
208
		$this->trxRoundId = false;
209
		$this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
210
		$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
211
		// Run all post-rollback callbacks
212
		$this->forEachLB( function ( ILoadBalancer $lb ) {
213
			$lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK );
214
		} );
215
	}
216
217
	/**
218
	 * Log query info if multi DB transactions are going to be committed now
219
	 */
220
	private function logIfMultiDbTransaction() {
221
		$callersByDB = [];
222
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$callersByDB ) {
223
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
224
			$callers = $lb->pendingMasterChangeCallers();
225
			if ( $callers ) {
226
				$callersByDB[$masterName] = $callers;
227
			}
228
		} );
229
230
		if ( count( $callersByDB ) >= 2 ) {
231
			$dbs = implode( ', ', array_keys( $callersByDB ) );
232
			$msg = "Multi-DB transaction [{$dbs}]:\n";
233
			foreach ( $callersByDB as $db => $callers ) {
234
				$msg .= "$db: " . implode( '; ', $callers ) . "\n";
235
			}
236
			$this->queryLogger->info( $msg );
237
		}
238
	}
239
240
	public function hasMasterChanges() {
241
		$ret = false;
242
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
243
			$ret = $ret || $lb->hasMasterChanges();
244
		} );
245
246
		return $ret;
247
	}
248
249
	public function laggedReplicaUsed() {
250
		$ret = false;
251
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$ret ) {
252
			$ret = $ret || $lb->laggedReplicaUsed();
253
		} );
254
255
		return $ret;
256
	}
257
258
	public function hasOrMadeRecentMasterChanges( $age = null ) {
259
		$ret = false;
260
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $age, &$ret ) {
261
			$ret = $ret || $lb->hasOrMadeRecentMasterChanges( $age );
262
		} );
263
		return $ret;
264
	}
265
266
	public function waitForReplication( array $opts = [] ) {
267
		$opts += [
268
			'domain' => false,
269
			'cluster' => false,
270
			'timeout' => 60,
271
			'ifWritesSince' => null
272
		];
273
274
		if ( $opts['domain'] === false && isset( $opts['wiki'] ) ) {
275
			$opts['domain'] = $opts['wiki']; // b/c
276
		}
277
278
		// Figure out which clusters need to be checked
279
		/** @var ILoadBalancer[] $lbs */
280
		$lbs = [];
281
		if ( $opts['cluster'] !== false ) {
282
			$lbs[] = $this->getExternalLB( $opts['cluster'] );
283
		} elseif ( $opts['domain'] !== false ) {
284
			$lbs[] = $this->getMainLB( $opts['domain'] );
285
		} else {
286
			$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$lbs ) {
287
				$lbs[] = $lb;
288
			} );
289
			if ( !$lbs ) {
290
				return; // nothing actually used
291
			}
292
		}
293
294
		// Get all the master positions of applicable DBs right now.
295
		// This can be faster since waiting on one cluster reduces the
296
		// time needed to wait on the next clusters.
297
		$masterPositions = array_fill( 0, count( $lbs ), false );
298
		foreach ( $lbs as $i => $lb ) {
299
			if ( $lb->getServerCount() <= 1 ) {
300
				// Bug 27975 - Don't try to wait for replica DBs if there are none
301
				// Prevents permission error when getting master position
302
				continue;
303
			} elseif ( $opts['ifWritesSince']
304
				&& $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
305
			) {
306
				continue; // no writes since the last wait
307
			}
308
			$masterPositions[$i] = $lb->getMasterPos();
309
		}
310
311
		// Run any listener callbacks *after* getting the DB positions. The more
312
		// time spent in the callbacks, the less time is spent in waitForAll().
313
		foreach ( $this->replicationWaitCallbacks as $callback ) {
314
			$callback();
315
		}
316
317
		$failed = [];
318
		foreach ( $lbs as $i => $lb ) {
319
			if ( $masterPositions[$i] ) {
320
				// The DBMS may not support getMasterPos()
321
				if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
322
					$failed[] = $lb->getServerName( $lb->getWriterIndex() );
323
				}
324
			}
325
		}
326
327
		if ( $failed ) {
328
			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...
329
				"Could not wait for replica DBs to catch up to " .
330
				implode( ', ', $failed )
331
			);
332
		}
333
	}
334
335
	public function setWaitForReplicationListener( $name, callable $callback = null ) {
336
		if ( $callback ) {
337
			$this->replicationWaitCallbacks[$name] = $callback;
338
		} else {
339
			unset( $this->replicationWaitCallbacks[$name] );
340
		}
341
	}
342
343
	public function getEmptyTransactionTicket( $fname ) {
344
		if ( $this->hasMasterChanges() ) {
345
			$this->queryLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
346
				( new RuntimeException() )->getTraceAsString() );
347
348
			return null;
349
		}
350
351
		return $this->ticket;
352
	}
353
354
	public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
355
		if ( $ticket !== $this->ticket ) {
356
			$this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
357
				( new RuntimeException() )->getTraceAsString() );
358
359
			return;
360
		}
361
362
		// The transaction owner and any caller with the empty transaction ticket can commit
363
		// so that getEmptyTransactionTicket() callers don't risk seeing DBTransactionError.
364
		if ( $this->trxRoundId !== false && $fname !== $this->trxRoundId ) {
365
			$this->queryLogger->info( "$fname: committing on behalf of {$this->trxRoundId}." );
366
			$fnameEffective = $this->trxRoundId;
367
		} else {
368
			$fnameEffective = $fname;
369
		}
370
371
		$this->commitMasterChanges( $fnameEffective );
0 ignored issues
show
Bug introduced by
It seems like $fnameEffective defined by $this->trxRoundId on line 366 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...
372
		$this->waitForReplication( $opts );
373
		// If a nested caller committed on behalf of $fname, start another empty $fname
374
		// transaction, leaving the caller with the same empty transaction state as before.
375
		if ( $fnameEffective !== $fname ) {
376
			$this->beginMasterChanges( $fnameEffective );
0 ignored issues
show
Bug introduced by
It seems like $fnameEffective defined by $this->trxRoundId on line 366 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...
377
		}
378
	}
379
380
	public function getChronologyProtectorTouched( $dbName ) {
381
		return $this->getChronologyProtector()->getTouched( $dbName );
382
	}
383
384
	public function disableChronologyProtection() {
385
		$this->getChronologyProtector()->setEnabled( false );
386
	}
387
388
	/**
389
	 * @return ChronologyProtector
390
	 */
391
	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...
392
		if ( $this->chronProt ) {
393
			return $this->chronProt;
394
		}
395
396
		$this->chronProt = new ChronologyProtector(
397
			$this->memCache,
398
			[
399
				'ip' => $this->requestInfo['IPAddress'],
400
				'agent' => $this->requestInfo['UserAgent'],
401
			],
402
			isset( $_GET['cpPosTime'] ) ? $_GET['cpPosTime'] : null
403
		);
404
		$this->chronProt->setLogger( $this->replLogger );
405
406
		if ( $this->cliMode ) {
407
			$this->chronProt->setEnabled( false );
408
		} elseif ( $this->requestInfo['ChronologyProtection'] === 'false' ) {
409
			// Request opted out of using position wait logic. This is useful for requests
410
			// done by the job queue or background ETL that do not have a meaningful session.
411
			$this->chronProt->setWaitEnabled( false );
412
		}
413
414
		$this->replLogger->debug( __METHOD__ . ': using request info ' .
415
			json_encode( $this->requestInfo, JSON_PRETTY_PRINT ) );
416
417
		return $this->chronProt;
418
	}
419
420
	/**
421
	 * Get and record all of the staged DB positions into persistent memory storage
422
	 *
423
	 * @param ChronologyProtector $cp
424
	 * @param callable|null $workCallback Work to do instead of waiting on syncing positions
425
	 * @param string $mode One of (sync, async); whether to wait on remote datacenters
426
	 */
427
	protected function shutdownChronologyProtector(
428
		ChronologyProtector $cp, $workCallback, $mode
429
	) {
430
		// Record all the master positions needed
431
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $cp ) {
432
			$cp->shutdownLB( $lb );
433
		} );
434
		// Write them to the persistent stash. Try to do something useful by running $work
435
		// while ChronologyProtector waits for the stash write to replicate to all DCs.
436
		$unsavedPositions = $cp->shutdown( $workCallback, $mode );
437
		if ( $unsavedPositions && $workCallback ) {
438
			// Invoke callback in case it did not cache the result yet
439
			$workCallback(); // work now to block for less time in waitForAll()
440
		}
441
		// If the positions failed to write to the stash, at least wait on local datacenter
442
		// replica DBs to catch up before responding. Even if there are several DCs, this increases
443
		// the chance that the user will see their own changes immediately afterwards. As long
444
		// as the sticky DC cookie applies (same domain), this is not even an issue.
445
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( $unsavedPositions ) {
446
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
447
			if ( isset( $unsavedPositions[$masterName] ) ) {
448
				$lb->waitForAll( $unsavedPositions[$masterName] );
449
			}
450
		} );
451
	}
452
453
	/**
454
	 * Base parameters to LoadBalancer::__construct()
455
	 * @return array
456
	 */
457
	final protected function baseLoadBalancerParams() {
458
		return [
459
			'localDomain' => $this->localDomain,
460
			'readOnlyReason' => $this->readOnlyReason,
461
			'srvCache' => $this->srvCache,
462
			'wanCache' => $this->wanCache,
463
			'profiler' => $this->profiler,
464
			'trxProfiler' => $this->trxProfiler,
465
			'queryLogger' => $this->queryLogger,
466
			'connLogger' => $this->connLogger,
467
			'replLogger' => $this->replLogger,
468
			'errorLogger' => $this->errorLogger,
469
			'hostname' => $this->hostname,
470
			'cliMode' => $this->cliMode,
471
			'agent' => $this->agent
472
		];
473
	}
474
475
	/**
476
	 * @param ILoadBalancer $lb
477
	 */
478
	protected function initLoadBalancer( ILoadBalancer $lb ) {
479
		if ( $this->trxRoundId !== false ) {
480
			$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...
481
		}
482
	}
483
484
	public function setDomainPrefix( $prefix ) {
485
		$this->localDomain = new DatabaseDomain(
486
			$this->localDomain->getDatabase(),
487
			null,
488
			$prefix
489
		);
490
491
		$this->forEachLB( function( ILoadBalancer $lb ) use ( $prefix ) {
492
			$lb->setDomainPrefix( $prefix );
493
		} );
494
	}
495
496
	public function closeAll() {
497
		$this->forEachLBCallMethod( 'closeAll', [] );
498
	}
499
500
	public function setAgentName( $agent ) {
501
		$this->agent = $agent;
502
	}
503
504
	public function appendPreShutdownTimeAsQuery( $url, $time ) {
505
		$usedCluster = 0;
506
		$this->forEachLB( function ( ILoadBalancer $lb ) use ( &$usedCluster ) {
507
			$usedCluster |= ( $lb->getServerCount() > 1 );
508
		} );
509
510
		if ( !$usedCluster ) {
511
			return $url; // no master/replica clusters touched
512
		}
513
514
		return strpos( $url, '?' ) === false ? "$url?cpPosTime=$time" : "$url&cpPosTime=$time";
515
	}
516
517
	public function setRequestInfo( array $info ) {
518
		$this->requestInfo = $info + $this->requestInfo;
519
	}
520
521
	/**
522
	 * Make PHP ignore user aborts/disconnects until the returned
523
	 * value leaves scope. This returns null and does nothing in CLI mode.
524
	 *
525
	 * @return ScopedCallback|null
526
	 */
527 View Code Duplication
	final protected function getScopedPHPBehaviorForCommit() {
528
		if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
529
			$old = ignore_user_abort( true ); // avoid half-finished operations
530
			return new ScopedCallback( function () use ( $old ) {
531
				ignore_user_abort( $old );
532
			} );
533
		}
534
535
		return null;
536
	}
537
538
	function __destruct() {
539
		$this->destroy();
540
	}
541
}
542