Completed
Branch master (9259dd)
by
unknown
27:26
created

LoadBalancer   F

Complexity

Total Complexity 225

Size/Duplication

Total Lines 1402
Duplicated Lines 4.14 %

Coupling/Cohesion

Components 2
Dependencies 15

Importance

Changes 0
Metric Value
dl 58
loc 1402
rs 0.6314
c 0
b 0
f 0
wmc 225
lcom 2
cbo 15

50 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 3 53 12
A getLoadMonitor() 0 8 2
A parentInfo() 0 3 1
D getRandomNonLagged() 3 42 9
F getReaderIndex() 5 128 28
A waitFor() 6 11 3
A waitForOne() 5 20 3
A waitForAll() 3 13 4
A getAnyOpenConnection() 0 9 3
C doWait() 0 57 10
F getConnection() 0 66 16
B reuseConnection() 0 38 6
A getConnectionRef() 0 3 1
A getLazyConnectionRef() 0 3 1
B openConnection() 0 31 6
B openForeignConnection() 0 56 8
A isOpen() 0 7 2
B reallyOpenConnection() 0 42 6
B reportConnectionError() 0 29 2
A getWriterIndex() 0 3 1
A haveIndex() 0 3 1
A isNonZeroLoad() 0 3 2
A getServerCount() 0 3 1
A getServerName() 3 11 4
A getServerInfo() 0 7 2
A setServerInfo() 0 3 1
A getMasterPos() 0 18 4
A disable() 0 4 1
A closeAll() 0 16 4
B closeConnection() 0 19 6
B commitAll() 0 12 5
B commitMasterChanges() 0 14 6
C rollbackMasterChanges() 0 26 8
A hasMasterConnection() 0 3 1
B hasMasterChanges() 0 15 6
A lastMasterChangeTimestamp() 14 14 4
A hasOrMadeRecentMasterChanges() 0 6 3
A pendingMasterChangeCallers() 16 16 4
A waitTimeout() 0 3 1
A getLaggedSlaveMode() 0 16 4
A laggedSlaveUsed() 0 3 1
A getReadOnlyReason() 0 15 4
A allowLagged() 0 8 2
B pingAll() 0 15 5
A forEachOpenConnection() 0 10 4
B getMaxLag() 0 20 5
A getLagTimes() 0 8 2
A safeGetLag() 0 7 2
C safeWaitForMasterPos() 0 23 7
A clearLagTimeCache() 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 LoadBalancer 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 LoadBalancer, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Database load balancing.
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
/**
25
 * Database load balancing object
26
 *
27
 * @todo document
28
 * @ingroup Database
29
 */
30
class LoadBalancer {
31
	/** @var array[] Map of (server index => server config array) */
32
	private $mServers;
33
	/** @var array[] Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */
34
	private $mConns;
35
	/** @var array Map of (server index => weight) */
36
	private $mLoads;
37
	/** @var array[] Map of (group => server index => weight) */
38
	private $mGroupLoads;
39
	/** @var bool Whether to disregard slave lag as a factor in slave selection */
40
	private $mAllowLagged;
41
	/** @var integer Seconds to spend waiting on slave lag to resolve */
42
	private $mWaitTimeout;
43
	/** @var array LBFactory information */
44
	private $mParentInfo;
45
46
	/** @var string The LoadMonitor subclass name */
47
	private $mLoadMonitorClass;
48
	/** @var LoadMonitor */
49
	private $mLoadMonitor;
50
	/** @var BagOStuff */
51
	private $srvCache;
52
53
	/** @var bool|DatabaseBase Database connection that caused a problem */
54
	private $mErrorConnection;
55
	/** @var integer The generic (not query grouped) slave index (of $mServers) */
56
	private $mReadIndex;
57
	/** @var bool|DBMasterPos False if not set */
58
	private $mWaitForPos;
59
	/** @var bool Whether the generic reader fell back to a lagged slave */
60
	private $laggedSlaveMode = false;
61
	/** @var bool Whether the generic reader fell back to a lagged slave */
62
	private $slavesDownMode = false;
63
	/** @var string The last DB selection or connection error */
64
	private $mLastError = 'Unknown error';
65
	/** @var string|bool Reason the LB is read-only or false if not */
66
	private $readOnlyReason = false;
67
	/** @var integer Total connections opened */
68
	private $connsOpened = 0;
69
70
	/** @var TransactionProfiler */
71
	protected $trxProfiler;
72
73
	/** @var integer Warn when this many connection are held */
74
	const CONN_HELD_WARN_THRESHOLD = 10;
75
	/** @var integer Default 'max lag' when unspecified */
76
	const MAX_LAG = 10;
77
	/** @var integer Max time to wait for a slave to catch up (e.g. ChronologyProtector) */
78
	const POS_WAIT_TIMEOUT = 10;
79
80
	/**
81
	 * @var boolean
82
	 */
83
	private $disabled = false;
84
85
	/**
86
	 * @param array $params Array with keys:
87
	 *  - servers : Required. Array of server info structures.
88
	 *  - loadMonitor : Name of a class used to fetch server lag and load.
89
	 *  - readOnlyReason : Reason the master DB is read-only if so [optional]
90
	 * @throws MWException
91
	 */
92
	public function __construct( array $params ) {
93
		if ( !isset( $params['servers'] ) ) {
94
			throw new MWException( __CLASS__ . ': missing servers parameter' );
95
		}
96
		$this->mServers = $params['servers'];
97
		$this->mWaitTimeout = self::POS_WAIT_TIMEOUT;
98
99
		$this->mReadIndex = -1;
100
		$this->mWriteIndex = -1;
0 ignored issues
show
Bug introduced by
The property mWriteIndex does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
101
		$this->mConns = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('local' => array()...oreignFree' => array()) of type array<string,array,{"loc..."foreignFree":"array"}> is incompatible with the declared type array<integer,array> of property $mConns.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
102
			'local' => [],
103
			'foreignUsed' => [],
104
			'foreignFree' => [] ];
105
		$this->mLoads = [];
106
		$this->mWaitForPos = false;
107
		$this->mErrorConnection = false;
108
		$this->mAllowLagged = false;
109
110 View Code Duplication
		if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) {
111
			$this->readOnlyReason = $params['readOnlyReason'];
112
		}
113
114
		if ( isset( $params['loadMonitor'] ) ) {
115
			$this->mLoadMonitorClass = $params['loadMonitor'];
116
		} else {
117
			$master = reset( $params['servers'] );
118
			if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) {
119
				$this->mLoadMonitorClass = 'LoadMonitorMySQL';
120
			} else {
121
				$this->mLoadMonitorClass = 'LoadMonitorNull';
122
			}
123
		}
124
125
		foreach ( $params['servers'] as $i => $server ) {
126
			$this->mLoads[$i] = $server['load'];
127
			if ( isset( $server['groupLoads'] ) ) {
128
				foreach ( $server['groupLoads'] as $group => $ratio ) {
129
					if ( !isset( $this->mGroupLoads[$group] ) ) {
130
						$this->mGroupLoads[$group] = [];
131
					}
132
					$this->mGroupLoads[$group][$i] = $ratio;
133
				}
134
			}
135
		}
136
137
		$this->srvCache = ObjectCache::getLocalServerInstance();
138
139
		if ( isset( $params['trxProfiler'] ) ) {
140
			$this->trxProfiler = $params['trxProfiler'];
141
		} else {
142
			$this->trxProfiler = new TransactionProfiler();
143
		}
144
	}
145
146
	/**
147
	 * Get a LoadMonitor instance
148
	 *
149
	 * @return LoadMonitor
150
	 */
151
	private function getLoadMonitor() {
152
		if ( !isset( $this->mLoadMonitor ) ) {
153
			$class = $this->mLoadMonitorClass;
154
			$this->mLoadMonitor = new $class( $this );
155
		}
156
157
		return $this->mLoadMonitor;
158
	}
159
160
	/**
161
	 * Get or set arbitrary data used by the parent object, usually an LBFactory
162
	 * @param mixed $x
163
	 * @return mixed
164
	 */
165
	public function parentInfo( $x = null ) {
166
		return wfSetVar( $this->mParentInfo, $x );
167
	}
168
169
	/**
170
	 * @param array $loads
171
	 * @param bool|string $wiki Wiki to get non-lagged for
172
	 * @param int $maxLag Restrict the maximum allowed lag to this many seconds
173
	 * @return bool|int|string
174
	 */
175
	private function getRandomNonLagged( array $loads, $wiki = false, $maxLag = self::MAX_LAG ) {
176
		$lags = $this->getLagTimes( $wiki );
177
178
		# Unset excessively lagged servers
179
		foreach ( $lags as $i => $lag ) {
180
			if ( $i != 0 ) {
181
				$maxServerLag = $maxLag;
182 View Code Duplication
				if ( isset( $this->mServers[$i]['max lag'] ) ) {
183
					$maxServerLag = min( $maxServerLag, $this->mServers[$i]['max lag'] );
184
				}
185
186
				$host = $this->getServerName( $i );
187
				if ( $lag === false ) {
188
					wfDebugLog( 'replication', "Server $host (#$i) is not replicating?" );
189
					unset( $loads[$i] );
190
				} elseif ( $lag > $maxServerLag ) {
191
					wfDebugLog( 'replication', "Server $host (#$i) has >= $lag seconds of lag" );
192
					unset( $loads[$i] );
193
				}
194
			}
195
		}
196
197
		# Find out if all the slaves with non-zero load are lagged
198
		$sum = 0;
199
		foreach ( $loads as $load ) {
200
			$sum += $load;
201
		}
202
		if ( $sum == 0 ) {
203
			# No appropriate DB servers except maybe the master and some slaves with zero load
204
			# Do NOT use the master
205
			# Instead, this function will return false, triggering read-only mode,
206
			# and a lagged slave will be used instead.
207
			return false;
208
		}
209
210
		if ( count( $loads ) == 0 ) {
211
			return false;
212
		}
213
214
		# Return a random representative of the remainder
215
		return ArrayUtils::pickRandom( $loads );
216
	}
217
218
	/**
219
	 * Get the index of the reader connection, which may be a slave
220
	 * This takes into account load ratios and lag times. It should
221
	 * always return a consistent index during a given invocation
222
	 *
223
	 * Side effect: opens connections to databases
224
	 * @param string|bool $group Query group, or false for the generic reader
225
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
226
	 * @throws MWException
227
	 * @return bool|int|string
228
	 */
229
	public function getReaderIndex( $group = false, $wiki = false ) {
230
		global $wgDBtype;
231
232
		# @todo FIXME: For now, only go through all this for mysql databases
233
		if ( $wgDBtype != 'mysql' ) {
234
			return $this->getWriterIndex();
235
		}
236
237
		if ( count( $this->mServers ) == 1 ) {
238
			# Skip the load balancing if there's only one server
239
			return 0;
240
		} elseif ( $group === false && $this->mReadIndex >= 0 ) {
241
			# Shortcut if generic reader exists already
242
			return $this->mReadIndex;
243
		}
244
245
		# Find the relevant load array
246
		if ( $group !== false ) {
247
			if ( isset( $this->mGroupLoads[$group] ) ) {
248
				$nonErrorLoads = $this->mGroupLoads[$group];
249
			} else {
250
				# No loads for this group, return false and the caller can use some other group
251
				wfDebugLog( 'connect', __METHOD__ . ": no loads for group $group\n" );
252
253
				return false;
254
			}
255
		} else {
256
			$nonErrorLoads = $this->mLoads;
257
		}
258
259
		if ( !count( $nonErrorLoads ) ) {
260
			throw new MWException( "Empty server array given to LoadBalancer" );
261
		}
262
263
		# Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
264
		$this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
265
266
		$laggedSlaveMode = false;
267
268
		# No server found yet
269
		$i = false;
270
		$conn = false;
271
		# First try quickly looking through the available servers for a server that
272
		# meets our criteria
273
		$currentLoads = $nonErrorLoads;
274
		while ( count( $currentLoads ) ) {
275
			if ( $this->mAllowLagged || $laggedSlaveMode ) {
276
				$i = ArrayUtils::pickRandom( $currentLoads );
277
			} else {
278
				$i = false;
279
				if ( $this->mWaitForPos && $this->mWaitForPos->asOfTime() ) {
280
					# ChronologyProtecter causes mWaitForPos to be set via sessions.
281
					# This triggers doWait() after connect, so it's especially good to
282
					# avoid lagged servers so as to avoid just blocking in that method.
283
					$ago = microtime( true ) - $this->mWaitForPos->asOfTime();
284
					# Aim for <= 1 second of waiting (being too picky can backfire)
285
					$i = $this->getRandomNonLagged( $currentLoads, $wiki, $ago + 1 );
286
				}
287
				if ( $i === false ) {
288
					# Any server with less lag than it's 'max lag' param is preferable
289
					$i = $this->getRandomNonLagged( $currentLoads, $wiki );
290
				}
291
				if ( $i === false && count( $currentLoads ) != 0 ) {
292
					# All slaves lagged. Switch to read-only mode
293
					wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode" );
294
					$i = ArrayUtils::pickRandom( $currentLoads );
295
					$laggedSlaveMode = true;
296
				}
297
			}
298
299
			if ( $i === false ) {
300
				# pickRandom() returned false
301
				# This is permanent and means the configuration or the load monitor
302
				# wants us to return false.
303
				wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false" );
304
305
				return false;
306
			}
307
308
			$serverName = $this->getServerName( $i );
309
			wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: $serverName..." );
310
311
			$conn = $this->openConnection( $i, $wiki );
312
			if ( !$conn ) {
313
				wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki" );
314
				unset( $nonErrorLoads[$i] );
315
				unset( $currentLoads[$i] );
316
				$i = false;
317
				continue;
318
			}
319
320
			// Decrement reference counter, we are finished with this connection.
321
			// It will be incremented for the caller later.
322
			if ( $wiki !== false ) {
323
				$this->reuseConnection( $conn );
324
			}
325
326
			# Return this server
327
			break;
328
		}
329
330
		# If all servers were down, quit now
331
		if ( !count( $nonErrorLoads ) ) {
332
			wfDebugLog( 'connect', "All servers down" );
333
		}
334
335
		if ( $i !== false ) {
336
			# Slave connection successful
337
			# Wait for the session master pos for a short time
338 View Code Duplication
			if ( $this->mWaitForPos && $i > 0 ) {
339
				if ( !$this->doWait( $i ) ) {
340
					$this->mServers[$i]['slave pos'] = $conn->getSlavePos();
0 ignored issues
show
Bug introduced by
The method getSlavePos cannot be called on $conn (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
341
				}
342
			}
343
			if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) {
344
				$this->mReadIndex = $i;
0 ignored issues
show
Documentation Bug introduced by
It seems like $i can also be of type string. However, the property $mReadIndex is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
345
				# Record if the generic reader index is in "lagged slave" mode
346
				if ( $laggedSlaveMode ) {
347
					$this->laggedSlaveMode = true;
348
				}
349
			}
350
			$serverName = $this->getServerName( $i );
351
			wfDebugLog( 'connect', __METHOD__ .
352
				": using server $serverName for group '$group'\n" );
353
		}
354
355
		return $i;
356
	}
357
358
	/**
359
	 * Set the master wait position
360
	 * If a DB_SLAVE connection has been opened already, waits
361
	 * Otherwise sets a variable telling it to wait if such a connection is opened
362
	 * @param DBMasterPos $pos
363
	 */
364
	public function waitFor( $pos ) {
365
		$this->mWaitForPos = $pos;
366
		$i = $this->mReadIndex;
367
368 View Code Duplication
		if ( $i > 0 ) {
369
			if ( !$this->doWait( $i ) ) {
370
				$this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
371
				$this->laggedSlaveMode = true;
372
			}
373
		}
374
	}
375
376
	/**
377
	 * Set the master wait position and wait for a "generic" slave to catch up to it
378
	 *
379
	 * This can be used a faster proxy for waitForAll()
380
	 *
381
	 * @param DBMasterPos $pos
382
	 * @param int $timeout Max seconds to wait; default is mWaitTimeout
383
	 * @return bool Success (able to connect and no timeouts reached)
384
	 * @since 1.26
385
	 */
386
	public function waitForOne( $pos, $timeout = null ) {
387
		$this->mWaitForPos = $pos;
388
389
		$i = $this->mReadIndex;
390
		if ( $i <= 0 ) {
391
			// Pick a generic slave if there isn't one yet
392
			$readLoads = $this->mLoads;
393
			unset( $readLoads[$this->getWriterIndex()] ); // slaves only
394
			$readLoads = array_filter( $readLoads ); // with non-zero load
395
			$i = ArrayUtils::pickRandom( $readLoads );
396
		}
397
398 View Code Duplication
		if ( $i > 0 ) {
399
			$ok = $this->doWait( $i, true, $timeout );
400
		} else {
401
			$ok = true; // no applicable loads
402
		}
403
404
		return $ok;
405
	}
406
407
	/**
408
	 * Set the master wait position and wait for ALL slaves to catch up to it
409
	 * @param DBMasterPos $pos
410
	 * @param int $timeout Max seconds to wait; default is mWaitTimeout
411
	 * @return bool Success (able to connect and no timeouts reached)
412
	 */
413
	public function waitForAll( $pos, $timeout = null ) {
414
		$this->mWaitForPos = $pos;
415
		$serverCount = count( $this->mServers );
416
417
		$ok = true;
418
		for ( $i = 1; $i < $serverCount; $i++ ) {
419 View Code Duplication
			if ( $this->mLoads[$i] > 0 ) {
420
				$ok = $this->doWait( $i, true, $timeout ) && $ok;
421
			}
422
		}
423
424
		return $ok;
425
	}
426
427
	/**
428
	 * Get any open connection to a given server index, local or foreign
429
	 * Returns false if there is no connection open
430
	 *
431
	 * @param int $i
432
	 * @return DatabaseBase|bool False on failure
433
	 */
434
	public function getAnyOpenConnection( $i ) {
435
		foreach ( $this->mConns as $conns ) {
436
			if ( !empty( $conns[$i] ) ) {
437
				return reset( $conns[$i] );
438
			}
439
		}
440
441
		return false;
442
	}
443
444
	/**
445
	 * Wait for a given slave to catch up to the master pos stored in $this
446
	 * @param int $index Server index
447
	 * @param bool $open Check the server even if a new connection has to be made
448
	 * @param int $timeout Max seconds to wait; default is mWaitTimeout
449
	 * @return bool
450
	 */
451
	protected function doWait( $index, $open = false, $timeout = null ) {
452
		$close = false; // close the connection afterwards
453
454
		// Check if we already know that the DB has reached this point
455
		$server = $this->getServerName( $index );
456
		$key = $this->srvCache->makeGlobalKey( __CLASS__, 'last-known-pos', $server );
457
		/** @var DBMasterPos $knownReachedPos */
458
		$knownReachedPos = $this->srvCache->get( $key );
459
		if ( $knownReachedPos && $knownReachedPos->hasReached( $this->mWaitForPos ) ) {
0 ignored issues
show
Bug introduced by
It seems like $this->mWaitForPos can also be of type boolean; however, DBMasterPos::hasReached() does only seem to accept object<DBMasterPos>, 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...
460
			wfDebugLog( 'replication', __METHOD__ .
461
				": slave $server known to be caught up (pos >= $knownReachedPos).\n" );
462
			return true;
463
		}
464
465
		// Find a connection to wait on, creating one if needed and allowed
466
		$conn = $this->getAnyOpenConnection( $index );
467
		if ( !$conn ) {
468
			if ( !$open ) {
469
				wfDebugLog( 'replication', __METHOD__ . ": no connection open for $server\n" );
470
471
				return false;
472
			} else {
473
				$conn = $this->openConnection( $index, '' );
474
				if ( !$conn ) {
475
					wfDebugLog( 'replication', __METHOD__ . ": failed to connect to $server\n" );
476
477
					return false;
478
				}
479
				// Avoid connection spam in waitForAll() when connections
480
				// are made just for the sake of doing this lag check.
481
				$close = true;
482
			}
483
		}
484
485
		wfDebugLog( 'replication', __METHOD__ . ": Waiting for slave $server to catch up...\n" );
486
		$timeout = $timeout ?: $this->mWaitTimeout;
487
		$result = $conn->masterPosWait( $this->mWaitForPos, $timeout );
0 ignored issues
show
Bug introduced by
It seems like $this->mWaitForPos can also be of type boolean; however, DatabaseBase::masterPosWait() does only seem to accept object<DBMasterPos>, 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...
488
489
		if ( $result == -1 || is_null( $result ) ) {
490
			// Timed out waiting for slave, use master instead
491
			$msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}";
492
			wfDebugLog( 'replication', "$msg\n" );
493
			wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
494
			$ok = false;
495
		} else {
496
			wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
497
			$ok = true;
498
			// Remember that the DB reached this point
499
			$this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY );
500
		}
501
502
		if ( $close ) {
503
			$this->closeConnection( $conn );
0 ignored issues
show
Bug introduced by
It seems like $conn can also be of type boolean; however, LoadBalancer::closeConnection() does only seem to accept object<DatabaseBase>, 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...
504
		}
505
506
		return $ok;
507
	}
508
509
	/**
510
	 * Get a connection by index
511
	 * This is the main entry point for this class.
512
	 *
513
	 * @param int $i Server index
514
	 * @param array|string|bool $groups Query group(s), or false for the generic reader
515
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
516
	 *
517
	 * @throws MWException
518
	 * @return DatabaseBase
519
	 */
520
	public function getConnection( $i, $groups = [], $wiki = false ) {
521
		if ( $i === null || $i === false ) {
522
			throw new MWException( 'Attempt to call ' . __METHOD__ .
523
				' with invalid server index' );
524
		}
525
526
		if ( $wiki === wfWikiID() ) {
527
			$wiki = false;
528
		}
529
530
		$groups = ( $groups === false || $groups === [] )
531
			? [ false ] // check one "group": the generic pool
532
			: (array)$groups;
533
534
		$masterOnly = ( $i == DB_MASTER || $i == $this->getWriterIndex() );
535
		$oldConnsOpened = $this->connsOpened; // connections open now
536
537
		if ( $i == DB_MASTER ) {
538
			$i = $this->getWriterIndex();
539
		} else {
540
			# Try to find an available server in any the query groups (in order)
541
			foreach ( $groups as $group ) {
542
				$groupIndex = $this->getReaderIndex( $group, $wiki );
543
				if ( $groupIndex !== false ) {
544
					$i = $groupIndex;
545
					break;
546
				}
547
			}
548
		}
549
550
		# Operation-based index
551
		if ( $i == DB_SLAVE ) {
552
			$this->mLastError = 'Unknown error'; // reset error string
553
			# Try the general server pool if $groups are unavailable.
554
			$i = in_array( false, $groups, true )
555
				? false // don't bother with this if that is what was tried above
556
				: $this->getReaderIndex( false, $wiki );
557
			# Couldn't find a working server in getReaderIndex()?
558
			if ( $i === false ) {
559
				$this->mLastError = 'No working slave server: ' . $this->mLastError;
560
561
				return $this->reportConnectionError();
562
			}
563
		}
564
565
		# Now we have an explicit index into the servers array
566
		$conn = $this->openConnection( $i, $wiki );
567
		if ( !$conn ) {
568
			return $this->reportConnectionError();
569
		}
570
571
		# Profile any new connections that happen
572
		if ( $this->connsOpened > $oldConnsOpened ) {
573
			$host = $conn->getServer();
574
			$dbname = $conn->getDBname();
575
			$trxProf = Profiler::instance()->getTransactionProfiler();
576
			$trxProf->recordConnection( $host, $dbname, $masterOnly );
577
		}
578
579
		if ( $masterOnly ) {
580
			# Make master-requested DB handles inherit any read-only mode setting
581
			$conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $wiki ) );
582
		}
583
584
		return $conn;
585
	}
586
587
	/**
588
	 * Mark a foreign connection as being available for reuse under a different
589
	 * DB name or prefix. This mechanism is reference-counted, and must be called
590
	 * the same number of times as getConnection() to work.
591
	 *
592
	 * @param DatabaseBase $conn
593
	 * @throws MWException
594
	 */
595
	public function reuseConnection( $conn ) {
596
		$serverIndex = $conn->getLBInfo( 'serverIndex' );
597
		$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
598
		if ( $serverIndex === null || $refCount === null ) {
599
			wfDebug( __METHOD__ . ": this connection was not opened as a foreign connection\n" );
600
			/**
601
			 * This can happen in code like:
602
			 *   foreach ( $dbs as $db ) {
603
			 *     $conn = $lb->getConnection( DB_SLAVE, array(), $db );
604
			 *     ...
605
			 *     $lb->reuseConnection( $conn );
606
			 *   }
607
			 * When a connection to the local DB is opened in this way, reuseConnection()
608
			 * should be ignored
609
			 */
610
			return;
611
		}
612
613
		$dbName = $conn->getDBname();
614
		$prefix = $conn->tablePrefix();
615
		if ( strval( $prefix ) !== '' ) {
616
			$wiki = "$dbName-$prefix";
617
		} else {
618
			$wiki = $dbName;
619
		}
620
		if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
621
			throw new MWException( __METHOD__ . ": connection not found, has " .
622
				"the connection been freed already?" );
623
		}
624
		$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
625
		if ( $refCount <= 0 ) {
626
			$this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
627
			unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
628
			wfDebug( __METHOD__ . ": freed connection $serverIndex/$wiki\n" );
629
		} else {
630
			wfDebug( __METHOD__ . ": reference count for $serverIndex/$wiki reduced to $refCount\n" );
631
		}
632
	}
633
634
	/**
635
	 * Get a database connection handle reference
636
	 *
637
	 * The handle's methods wrap simply wrap those of a DatabaseBase handle
638
	 *
639
	 * @see LoadBalancer::getConnection() for parameter information
640
	 *
641
	 * @param int $db
642
	 * @param array|string|bool $groups Query group(s), or false for the generic reader
643
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
644
	 * @return DBConnRef
645
	 */
646
	public function getConnectionRef( $db, $groups = [], $wiki = false ) {
647
		return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
648
	}
649
650
	/**
651
	 * Get a database connection handle reference without connecting yet
652
	 *
653
	 * The handle's methods wrap simply wrap those of a DatabaseBase handle
654
	 *
655
	 * @see LoadBalancer::getConnection() for parameter information
656
	 *
657
	 * @param int $db
658
	 * @param array|string|bool $groups Query group(s), or false for the generic reader
659
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
660
	 * @return DBConnRef
661
	 */
662
	public function getLazyConnectionRef( $db, $groups = [], $wiki = false ) {
663
		return new DBConnRef( $this, [ $db, $groups, $wiki ] );
664
	}
665
666
	/**
667
	 * Open a connection to the server given by the specified index
668
	 * Index must be an actual index into the array.
669
	 * If the server is already open, returns it.
670
	 *
671
	 * On error, returns false, and the connection which caused the
672
	 * error will be available via $this->mErrorConnection.
673
	 *
674
	 * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
675
	 *
676
	 * @param int $i Server index
677
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
678
	 * @return DatabaseBase|bool Returns false on errors
679
	 */
680
	public function openConnection( $i, $wiki = false ) {
681
		if ( $wiki !== false ) {
682
			$conn = $this->openForeignConnection( $i, $wiki );
0 ignored issues
show
Bug introduced by
It seems like $wiki defined by parameter $wiki on line 680 can also be of type boolean; however, LoadBalancer::openForeignConnection() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
683
		} elseif ( isset( $this->mConns['local'][$i][0] ) ) {
684
			$conn = $this->mConns['local'][$i][0];
685
		} else {
686
			$server = $this->mServers[$i];
687
			$server['serverIndex'] = $i;
688
			$conn = $this->reallyOpenConnection( $server, false );
689
			$serverName = $this->getServerName( $i );
690
			if ( $conn->isOpen() ) {
691
				wfDebugLog( 'connect', "Connected to database $i at $serverName\n" );
692
				$this->mConns['local'][$i][0] = $conn;
693
			} else {
694
				wfDebugLog( 'connect', "Failed to connect to database $i at $serverName\n" );
695
				$this->mErrorConnection = $conn;
696
				$conn = false;
697
			}
698
		}
699
700
		if ( $conn && !$conn->isOpen() ) {
701
			// Connection was made but later unrecoverably lost for some reason.
702
			// Do not return a handle that will just throw exceptions on use,
703
			// but let the calling code (e.g. getReaderIndex) try another server.
704
			// See DatabaseMyslBase::ping() for how this can happen.
705
			$this->mErrorConnection = $conn;
706
			$conn = false;
707
		}
708
709
		return $conn;
710
	}
711
712
	/**
713
	 * Open a connection to a foreign DB, or return one if it is already open.
714
	 *
715
	 * Increments a reference count on the returned connection which locks the
716
	 * connection to the requested wiki. This reference count can be
717
	 * decremented by calling reuseConnection().
718
	 *
719
	 * If a connection is open to the appropriate server already, but with the wrong
720
	 * database, it will be switched to the right database and returned, as long as
721
	 * it has been freed first with reuseConnection().
722
	 *
723
	 * On error, returns false, and the connection which caused the
724
	 * error will be available via $this->mErrorConnection.
725
	 *
726
	 * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
727
	 *
728
	 * @param int $i Server index
729
	 * @param string $wiki Wiki ID to open
730
	 * @return DatabaseBase
731
	 */
732
	private function openForeignConnection( $i, $wiki ) {
733
		list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
734
		if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
735
			// Reuse an already-used connection
736
			$conn = $this->mConns['foreignUsed'][$i][$wiki];
737
			wfDebug( __METHOD__ . ": reusing connection $i/$wiki\n" );
738
		} elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
739
			// Reuse a free connection for the same wiki
740
			$conn = $this->mConns['foreignFree'][$i][$wiki];
741
			unset( $this->mConns['foreignFree'][$i][$wiki] );
742
			$this->mConns['foreignUsed'][$i][$wiki] = $conn;
743
			wfDebug( __METHOD__ . ": reusing free connection $i/$wiki\n" );
744
		} elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
745
			// Reuse a connection from another wiki
746
			$conn = reset( $this->mConns['foreignFree'][$i] );
747
			$oldWiki = key( $this->mConns['foreignFree'][$i] );
748
749
			// The empty string as a DB name means "don't care".
750
			// DatabaseMysqlBase::open() already handle this on connection.
751
			if ( $dbName !== '' && !$conn->selectDB( $dbName ) ) {
752
				$this->mLastError = "Error selecting database $dbName on server " .
753
					$conn->getServer() . " from client host " . wfHostname() . "\n";
754
				$this->mErrorConnection = $conn;
755
				$conn = false;
756
			} else {
757
				$conn->tablePrefix( $prefix );
758
				unset( $this->mConns['foreignFree'][$i][$oldWiki] );
759
				$this->mConns['foreignUsed'][$i][$wiki] = $conn;
760
				wfDebug( __METHOD__ . ": reusing free connection from $oldWiki for $wiki\n" );
761
			}
762
		} else {
763
			// Open a new connection
764
			$server = $this->mServers[$i];
765
			$server['serverIndex'] = $i;
766
			$server['foreignPoolRefCount'] = 0;
767
			$server['foreign'] = true;
768
			$conn = $this->reallyOpenConnection( $server, $dbName );
769
			if ( !$conn->isOpen() ) {
770
				wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" );
771
				$this->mErrorConnection = $conn;
772
				$conn = false;
773
			} else {
774
				$conn->tablePrefix( $prefix );
775
				$this->mConns['foreignUsed'][$i][$wiki] = $conn;
776
				wfDebug( __METHOD__ . ": opened new connection for $i/$wiki\n" );
777
			}
778
		}
779
780
		// Increment reference count
781
		if ( $conn ) {
782
			$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
783
			$conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
784
		}
785
786
		return $conn;
787
	}
788
789
	/**
790
	 * Test if the specified index represents an open connection
791
	 *
792
	 * @param int $index Server index
793
	 * @access private
794
	 * @return bool
795
	 */
796
	private function isOpen( $index ) {
797
		if ( !is_integer( $index ) ) {
798
			return false;
799
		}
800
801
		return (bool)$this->getAnyOpenConnection( $index );
802
	}
803
804
	/**
805
	 * Really opens a connection. Uncached.
806
	 * Returns a Database object whether or not the connection was successful.
807
	 * @access private
808
	 *
809
	 * @param array $server
810
	 * @param bool $dbNameOverride
811
	 * @throws MWException
812
	 * @return DatabaseBase
813
	 */
814
	protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
815
		if ( $this->disabled ) {
816
			throw new DBAccessError();
817
		}
818
819
		if ( !is_array( $server ) ) {
820
			throw new MWException( 'You must update your load-balancing configuration. ' .
821
				'See DefaultSettings.php entry for $wgDBservers.' );
822
		}
823
824
		if ( $dbNameOverride !== false ) {
825
			$server['dbname'] = $dbNameOverride;
826
		}
827
828
		// Let the handle know what the cluster master is (e.g. "db1052")
829
		$masterName = $this->getServerName( 0 );
830
		$server['clusterMasterHost'] = $masterName;
831
832
		// Log when many connection are made on requests
833
		if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) {
834
			wfDebugLog( 'DBPerformance', __METHOD__ . ": " .
835
				"{$this->connsOpened}+ connections made (master=$masterName)\n" .
836
				wfBacktrace( true ) );
837
		}
838
839
		# Create object
840
		try {
841
			$db = DatabaseBase::factory( $server['type'], $server );
842
		} catch ( DBConnectionError $e ) {
843
			// FIXME: This is probably the ugliest thing I have ever done to
844
			// PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
845
			$db = $e->db;
846
		}
847
848
		$db->setLBInfo( $server );
849
		$db->setLazyMasterHandle(
850
			$this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
851
		);
852
		$db->setTransactionProfiler( $this->trxProfiler );
853
854
		return $db;
855
	}
856
857
	/**
858
	 * @throws DBConnectionError
859
	 * @return bool
860
	 */
861
	private function reportConnectionError() {
862
		$conn = $this->mErrorConnection; // The connection which caused the error
863
		$context = [
864
			'method' => __METHOD__,
865
			'last_error' => $this->mLastError,
866
		];
867
868
		if ( !is_object( $conn ) ) {
869
			// No last connection, probably due to all servers being too busy
870
			wfLogDBError(
871
				"LB failure with no last connection. Connection error: {last_error}",
872
				$context
873
			);
874
875
			// If all servers were busy, mLastError will contain something sensible
876
			throw new DBConnectionError( null, $this->mLastError );
877
		} else {
878
			$context['db_server'] = $conn->getProperty( 'mServer' );
879
			wfLogDBError(
880
				"Connection error: {last_error} ({db_server})",
881
				$context
882
			);
883
884
			// throws DBConnectionError
885
			$conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
886
		}
887
888
		return false; /* not reached */
889
	}
890
891
	/**
892
	 * @return int
893
	 * @since 1.26
894
	 */
895
	public function getWriterIndex() {
896
		return 0;
897
	}
898
899
	/**
900
	 * Returns true if the specified index is a valid server index
901
	 *
902
	 * @param string $i
903
	 * @return bool
904
	 */
905
	public function haveIndex( $i ) {
906
		return array_key_exists( $i, $this->mServers );
907
	}
908
909
	/**
910
	 * Returns true if the specified index is valid and has non-zero load
911
	 *
912
	 * @param string $i
913
	 * @return bool
914
	 */
915
	public function isNonZeroLoad( $i ) {
916
		return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
917
	}
918
919
	/**
920
	 * Get the number of defined servers (not the number of open connections)
921
	 *
922
	 * @return int
923
	 */
924
	public function getServerCount() {
925
		return count( $this->mServers );
926
	}
927
928
	/**
929
	 * Get the host name or IP address of the server with the specified index
930
	 * Prefer a readable name if available.
931
	 * @param string $i
932
	 * @return string
933
	 */
934
	public function getServerName( $i ) {
935
		if ( isset( $this->mServers[$i]['hostName'] ) ) {
936
			$name = $this->mServers[$i]['hostName'];
937 View Code Duplication
		} elseif ( isset( $this->mServers[$i]['host'] ) ) {
938
			$name = $this->mServers[$i]['host'];
939
		} else {
940
			$name = '';
941
		}
942
943
		return ( $name != '' ) ? $name : 'localhost';
944
	}
945
946
	/**
947
	 * Return the server info structure for a given index, or false if the index is invalid.
948
	 * @param int $i
949
	 * @return array|bool
950
	 */
951
	public function getServerInfo( $i ) {
952
		if ( isset( $this->mServers[$i] ) ) {
953
			return $this->mServers[$i];
954
		} else {
955
			return false;
956
		}
957
	}
958
959
	/**
960
	 * Sets the server info structure for the given index. Entry at index $i
961
	 * is created if it doesn't exist
962
	 * @param int $i
963
	 * @param array $serverInfo
964
	 */
965
	public function setServerInfo( $i, array $serverInfo ) {
966
		$this->mServers[$i] = $serverInfo;
967
	}
968
969
	/**
970
	 * Get the current master position for chronology control purposes
971
	 * @return mixed
972
	 */
973
	public function getMasterPos() {
974
		# If this entire request was served from a slave without opening a connection to the
975
		# master (however unlikely that may be), then we can fetch the position from the slave.
976
		$masterConn = $this->getAnyOpenConnection( 0 );
977
		if ( !$masterConn ) {
978
			$serverCount = count( $this->mServers );
979
			for ( $i = 1; $i < $serverCount; $i++ ) {
980
				$conn = $this->getAnyOpenConnection( $i );
981
				if ( $conn ) {
982
					return $conn->getSlavePos();
0 ignored issues
show
Bug introduced by
The method getSlavePos cannot be called on $conn (of type boolean).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
983
				}
984
			}
985
		} else {
986
			return $masterConn->getMasterPos();
987
		}
988
989
		return false;
990
	}
991
992
	/**
993
	 * Disable this load balancer. All connections are closed, and any attempt to
994
	 * open a new connection will result in a DBAccessError.
995
	 *
996
	 * @since 1.27
997
	 */
998
	public function disable() {
999
		$this->closeAll();
1000
		$this->disabled = true;
1001
	}
1002
1003
	/**
1004
	 * Close all open connections
1005
	 */
1006
	public function closeAll() {
1007
		foreach ( $this->mConns as $conns2 ) {
1008
			foreach ( $conns2 as $conns3 ) {
1009
				/** @var DatabaseBase $conn */
1010
				foreach ( $conns3 as $conn ) {
1011
					$conn->close();
1012
				}
1013
			}
1014
		}
1015
		$this->mConns = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('local' => array()...oreignUsed' => array()) of type array<string,array,{"loc..."foreignUsed":"array"}> is incompatible with the declared type array<integer,array> of property $mConns.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1016
			'local' => [],
1017
			'foreignFree' => [],
1018
			'foreignUsed' => [],
1019
		];
1020
		$this->connsOpened = 0;
1021
	}
1022
1023
	/**
1024
	 * Close a connection
1025
	 * Using this function makes sure the LoadBalancer knows the connection is closed.
1026
	 * If you use $conn->close() directly, the load balancer won't update its state.
1027
	 * @param DatabaseBase $conn
1028
	 */
1029
	public function closeConnection( $conn ) {
1030
		$done = false;
1031
		foreach ( $this->mConns as $i1 => $conns2 ) {
1032
			foreach ( $conns2 as $i2 => $conns3 ) {
1033
				foreach ( $conns3 as $i3 => $candidateConn ) {
1034
					if ( $conn === $candidateConn ) {
1035
						$conn->close();
1036
						unset( $this->mConns[$i1][$i2][$i3] );
1037
						--$this->connsOpened;
1038
						$done = true;
1039
						break;
1040
					}
1041
				}
1042
			}
1043
		}
1044
		if ( !$done ) {
1045
			$conn->close();
1046
		}
1047
	}
1048
1049
	/**
1050
	 * Commit transactions on all open connections
1051
	 * @param string $fname Caller name
1052
	 */
1053
	public function commitAll( $fname = __METHOD__ ) {
1054
		foreach ( $this->mConns as $conns2 ) {
1055
			foreach ( $conns2 as $conns3 ) {
1056
				/** @var DatabaseBase[] $conns3 */
1057
				foreach ( $conns3 as $conn ) {
1058
					if ( $conn->trxLevel() ) {
1059
						$conn->commit( $fname, 'flush' );
1060
					}
1061
				}
1062
			}
1063
		}
1064
	}
1065
1066
	/**
1067
	 * Issue COMMIT only on master, only if queries were done on connection
1068
	 * @param string $fname Caller name
1069
	 */
1070
	public function commitMasterChanges( $fname = __METHOD__ ) {
1071
		$masterIndex = $this->getWriterIndex();
1072
		foreach ( $this->mConns as $conns2 ) {
1073
			if ( empty( $conns2[$masterIndex] ) ) {
1074
				continue;
1075
			}
1076
			/** @var DatabaseBase $conn */
1077
			foreach ( $conns2[$masterIndex] as $conn ) {
1078
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1079
					$conn->commit( $fname, 'flush' );
1080
				}
1081
			}
1082
		}
1083
	}
1084
1085
	/**
1086
	 * Issue ROLLBACK only on master, only if queries were done on connection
1087
	 * @param string $fname Caller name
1088
	 * @throws DBExpectedError
1089
	 * @since 1.23
1090
	 */
1091
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
1092
		$failedServers = [];
1093
1094
		$masterIndex = $this->getWriterIndex();
1095
		foreach ( $this->mConns as $conns2 ) {
1096
			if ( empty( $conns2[$masterIndex] ) ) {
1097
				continue;
1098
			}
1099
			/** @var DatabaseBase $conn */
1100
			foreach ( $conns2[$masterIndex] as $conn ) {
1101
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1102
					try {
1103
						$conn->rollback( $fname, 'flush' );
1104
					} catch ( DBError $e ) {
1105
						MWExceptionHandler::logException( $e );
1106
						$failedServers[] = $conn->getServer();
1107
					}
1108
				}
1109
			}
1110
		}
1111
1112
		if ( $failedServers ) {
1113
			throw new DBExpectedError( null, "Rollback failed on server(s) " .
1114
				implode( ', ', array_unique( $failedServers ) ) );
1115
		}
1116
	}
1117
1118
	/**
1119
	 * @return bool Whether a master connection is already open
1120
	 * @since 1.24
1121
	 */
1122
	public function hasMasterConnection() {
1123
		return $this->isOpen( $this->getWriterIndex() );
1124
	}
1125
1126
	/**
1127
	 * Determine if there are pending changes in a transaction by this thread
1128
	 * @since 1.23
1129
	 * @return bool
1130
	 */
1131
	public function hasMasterChanges() {
1132
		$masterIndex = $this->getWriterIndex();
1133
		foreach ( $this->mConns as $conns2 ) {
1134
			if ( empty( $conns2[$masterIndex] ) ) {
1135
				continue;
1136
			}
1137
			/** @var DatabaseBase $conn */
1138
			foreach ( $conns2[$masterIndex] as $conn ) {
1139
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1140
					return true;
1141
				}
1142
			}
1143
		}
1144
		return false;
1145
	}
1146
1147
	/**
1148
	 * Get the timestamp of the latest write query done by this thread
1149
	 * @since 1.25
1150
	 * @return float|bool UNIX timestamp or false
1151
	 */
1152 View Code Duplication
	public function lastMasterChangeTimestamp() {
1153
		$lastTime = false;
1154
		$masterIndex = $this->getWriterIndex();
1155
		foreach ( $this->mConns as $conns2 ) {
1156
			if ( empty( $conns2[$masterIndex] ) ) {
1157
				continue;
1158
			}
1159
			/** @var DatabaseBase $conn */
1160
			foreach ( $conns2[$masterIndex] as $conn ) {
1161
				$lastTime = max( $lastTime, $conn->lastDoneWrites() );
1162
			}
1163
		}
1164
		return $lastTime;
1165
	}
1166
1167
	/**
1168
	 * Check if this load balancer object had any recent or still
1169
	 * pending writes issued against it by this PHP thread
1170
	 *
1171
	 * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
1172
	 * @return bool
1173
	 * @since 1.25
1174
	 */
1175
	public function hasOrMadeRecentMasterChanges( $age = null ) {
1176
		$age = ( $age === null ) ? $this->mWaitTimeout : $age;
1177
1178
		return ( $this->hasMasterChanges()
1179
			|| $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
1180
	}
1181
1182
	/**
1183
	 * Get the list of callers that have pending master changes
1184
	 *
1185
	 * @return array
1186
	 * @since 1.27
1187
	 */
1188 View Code Duplication
	public function pendingMasterChangeCallers() {
1189
		$fnames = [];
1190
1191
		$masterIndex = $this->getWriterIndex();
1192
		foreach ( $this->mConns as $conns2 ) {
1193
			if ( empty( $conns2[$masterIndex] ) ) {
1194
				continue;
1195
			}
1196
			/** @var DatabaseBase $conn */
1197
			foreach ( $conns2[$masterIndex] as $conn ) {
1198
				$fnames = array_merge( $fnames, $conn->pendingWriteCallers() );
1199
			}
1200
		}
1201
1202
		return $fnames;
1203
	}
1204
1205
	/**
1206
	 * @param mixed $value
1207
	 * @return mixed
1208
	 */
1209
	public function waitTimeout( $value = null ) {
1210
		return wfSetVar( $this->mWaitTimeout, $value );
1211
	}
1212
1213
	/**
1214
	 * @note This method will trigger a DB connection if not yet done
1215
	 *
1216
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
1217
	 * @return bool Whether the generic connection for reads is highly "lagged"
1218
	 */
1219
	public function getLaggedSlaveMode( $wiki = false ) {
1220
		// No-op if there is only one DB (also avoids recursion)
1221
		if ( !$this->laggedSlaveMode && $this->getServerCount() > 1 ) {
1222
			try {
1223
				// See if laggedSlaveMode gets set
1224
				$conn = $this->getConnection( DB_SLAVE, false, $wiki );
1225
				$this->reuseConnection( $conn );
1226
			} catch ( DBConnectionError $e ) {
1227
				// Avoid expensive re-connect attempts and failures
1228
				$this->slavesDownMode = true;
1229
				$this->laggedSlaveMode = true;
1230
			}
1231
		}
1232
1233
		return $this->laggedSlaveMode;
1234
	}
1235
1236
	/**
1237
	 * @note This method will never cause a new DB connection
1238
	 * @return bool Whether any generic connection used for reads was highly "lagged"
1239
	 * @since 1.27
1240
	 */
1241
	public function laggedSlaveUsed() {
1242
		return $this->laggedSlaveMode;
1243
	}
1244
1245
	/**
1246
	 * @note This method may trigger a DB connection if not yet done
1247
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
1248
	 * @return string|bool Reason the master is read-only or false if it is not
1249
	 * @since 1.27
1250
	 */
1251
	public function getReadOnlyReason( $wiki = false ) {
1252
		if ( $this->readOnlyReason !== false ) {
1253
			return $this->readOnlyReason;
1254
		} elseif ( $this->getLaggedSlaveMode( $wiki ) ) {
1255
			if ( $this->slavesDownMode ) {
1256
				return 'The database has been automatically locked ' .
1257
					'until the slave database servers become available';
1258
			} else {
1259
				return 'The database has been automatically locked ' .
1260
					'while the slave database servers catch up to the master.';
1261
			}
1262
		}
1263
1264
		return false;
1265
	}
1266
1267
	/**
1268
	 * Disables/enables lag checks
1269
	 * @param null|bool $mode
1270
	 * @return bool
1271
	 */
1272
	public function allowLagged( $mode = null ) {
1273
		if ( $mode === null ) {
1274
			return $this->mAllowLagged;
1275
		}
1276
		$this->mAllowLagged = $mode;
1277
1278
		return $this->mAllowLagged;
1279
	}
1280
1281
	/**
1282
	 * @return bool
1283
	 */
1284
	public function pingAll() {
1285
		$success = true;
1286
		foreach ( $this->mConns as $conns2 ) {
1287
			foreach ( $conns2 as $conns3 ) {
1288
				/** @var DatabaseBase[] $conns3 */
1289
				foreach ( $conns3 as $conn ) {
1290
					if ( !$conn->ping() ) {
1291
						$success = false;
1292
					}
1293
				}
1294
			}
1295
		}
1296
1297
		return $success;
1298
	}
1299
1300
	/**
1301
	 * Call a function with each open connection object
1302
	 * @param callable $callback
1303
	 * @param array $params
1304
	 */
1305
	public function forEachOpenConnection( $callback, array $params = [] ) {
1306
		foreach ( $this->mConns as $conns2 ) {
1307
			foreach ( $conns2 as $conns3 ) {
1308
				foreach ( $conns3 as $conn ) {
1309
					$mergedParams = array_merge( [ $conn ], $params );
1310
					call_user_func_array( $callback, $mergedParams );
1311
				}
1312
			}
1313
		}
1314
	}
1315
1316
	/**
1317
	 * Get the hostname and lag time of the most-lagged slave
1318
	 *
1319
	 * This is useful for maintenance scripts that need to throttle their updates.
1320
	 * May attempt to open connections to slaves on the default DB. If there is
1321
	 * no lag, the maximum lag will be reported as -1.
1322
	 *
1323
	 * @param bool|string $wiki Wiki ID, or false for the default database
1324
	 * @return array ( host, max lag, index of max lagged host )
1325
	 */
1326
	public function getMaxLag( $wiki = false ) {
1327
		$maxLag = -1;
1328
		$host = '';
1329
		$maxIndex = 0;
1330
1331
		if ( $this->getServerCount() <= 1 ) {
1332
			return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
1333
		}
1334
1335
		$lagTimes = $this->getLagTimes( $wiki );
1336
		foreach ( $lagTimes as $i => $lag ) {
1337
			if ( $this->mLoads[$i] > 0 && $lag > $maxLag ) {
1338
				$maxLag = $lag;
1339
				$host = $this->mServers[$i]['host'];
1340
				$maxIndex = $i;
1341
			}
1342
		}
1343
1344
		return [ $host, $maxLag, $maxIndex ];
1345
	}
1346
1347
	/**
1348
	 * Get an estimate of replication lag (in seconds) for each server
1349
	 *
1350
	 * Results are cached for a short time in memcached/process cache
1351
	 *
1352
	 * Values may be "false" if replication is too broken to estimate
1353
	 *
1354
	 * @param string|bool $wiki
1355
	 * @return int[] Map of (server index => float|int|bool)
1356
	 */
1357
	public function getLagTimes( $wiki = false ) {
1358
		if ( $this->getServerCount() <= 1 ) {
1359
			return [ 0 => 0 ]; // no replication = no lag
1360
		}
1361
1362
		# Send the request to the load monitor
1363
		return $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
0 ignored issues
show
Bug introduced by
It seems like $wiki defined by parameter $wiki on line 1357 can also be of type boolean; however, LoadMonitor::getLagTimes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1364
	}
1365
1366
	/**
1367
	 * Get the lag in seconds for a given connection, or zero if this load
1368
	 * balancer does not have replication enabled.
1369
	 *
1370
	 * This should be used in preference to Database::getLag() in cases where
1371
	 * replication may not be in use, since there is no way to determine if
1372
	 * replication is in use at the connection level without running
1373
	 * potentially restricted queries such as SHOW SLAVE STATUS. Using this
1374
	 * function instead of Database::getLag() avoids a fatal error in this
1375
	 * case on many installations.
1376
	 *
1377
	 * @param IDatabase $conn
1378
	 * @return int|bool Returns false on error
1379
	 */
1380
	public function safeGetLag( IDatabase $conn ) {
1381
		if ( $this->getServerCount() == 1 ) {
1382
			return 0;
1383
		} else {
1384
			return $conn->getLag();
1385
		}
1386
	}
1387
1388
	/**
1389
	 * Wait for a slave DB to reach a specified master position
1390
	 *
1391
	 * This will connect to the master to get an accurate position if $pos is not given
1392
	 *
1393
	 * @param IDatabase $conn Slave DB
1394
	 * @param DBMasterPos|bool $pos Master position; default: current position
1395
	 * @param integer $timeout Timeout in seconds
1396
	 * @return bool Success
1397
	 * @since 1.27
1398
	 */
1399
	public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
1400
		if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'slave' ) ) {
1401
			return true; // server is not a slave DB
1402
		}
1403
1404
		$pos = $pos ?: $this->getConnection( DB_MASTER )->getMasterPos();
1405
		if ( !( $pos instanceof DBMasterPos ) ) {
1406
			return false; // something is misconfigured
1407
		}
1408
1409
		$result = $conn->masterPosWait( $pos, $timeout );
1410
		if ( $result == -1 || is_null( $result ) ) {
1411
			$msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
1412
			wfDebugLog( 'replication', "$msg\n" );
1413
			wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
1414
			$ok = false;
1415
		} else {
1416
			wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
1417
			$ok = true;
1418
		}
1419
1420
		return $ok;
1421
	}
1422
1423
	/**
1424
	 * Clear the cache for slag lag delay times
1425
	 *
1426
	 * This is only used for testing
1427
	 */
1428
	public function clearLagTimeCache() {
1429
		$this->getLoadMonitor()->clearCaches();
1430
	}
1431
}
1432