Completed
Branch master (420c52)
by
unknown
26:22
created

LoadBalancer::runPreCommitCallbacks()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 14
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 8
nc 3
nop 0
dl 14
loc 14
rs 8.8571
c 1
b 0
f 0
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
		$this->forEachOpenConnection( function ( DatabaseBase $conn ) {
1008
			$conn->close();
1009
		} );
1010
1011
		$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...
1012
			'local' => [],
1013
			'foreignFree' => [],
1014
			'foreignUsed' => [],
1015
		];
1016
		$this->connsOpened = 0;
1017
	}
1018
1019
	/**
1020
	 * Close a connection
1021
	 * Using this function makes sure the LoadBalancer knows the connection is closed.
1022
	 * If you use $conn->close() directly, the load balancer won't update its state.
1023
	 * @param DatabaseBase $conn
1024
	 */
1025
	public function closeConnection( $conn ) {
1026
		$done = false;
1027
		foreach ( $this->mConns as $i1 => $conns2 ) {
1028
			foreach ( $conns2 as $i2 => $conns3 ) {
1029
				foreach ( $conns3 as $i3 => $candidateConn ) {
1030
					if ( $conn === $candidateConn ) {
1031
						$conn->close();
1032
						unset( $this->mConns[$i1][$i2][$i3] );
1033
						--$this->connsOpened;
1034
						$done = true;
1035
						break;
1036
					}
1037
				}
1038
			}
1039
		}
1040
		if ( !$done ) {
1041
			$conn->close();
1042
		}
1043
	}
1044
1045
	/**
1046
	 * Commit transactions on all open connections
1047
	 * @param string $fname Caller name
1048
	 */
1049
	public function commitAll( $fname = __METHOD__ ) {
1050
		$this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( $fname ) {
1051
			$conn->commit( $fname, 'flush' );
1052
		} );
1053
	}
1054
1055
	/**
1056
	 * Perform all pre-commit callbacks that remain part of the atomic transactions
1057
	 * and disable any post-commit callbacks until runMasterPostCommitCallbacks()
1058
	 * @since 1.28
1059
	 */
1060
	public function runMasterPreCommitCallbacks() {
1061
		$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
1062
			// Any error will cause all DB transactions to be rolled back together.
1063
			$conn->runOnTransactionPreCommitCallbacks();
1064
			// Defer post-commit callbacks until COMMIT finishes for all DBs.
1065
			$conn->setPostCommitCallbackSupression( true );
1066
		} );
1067
	}
1068
1069
	/**
1070
	 * Perform all pre-commit checks for things like replication safety
1071
	 * @param array $options Includes:
1072
	 *   - maxWriteDuration : max write query duration time in seconds
1073
	 * @throws DBTransactionError
1074
	 * @since 1.28
1075
	 */
1076
	public function approveMasterChanges( array $options ) {
1077
		$limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
1078
		$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $limit ) {
1079
			// Assert that the time to replicate the transaction will be sane.
1080
			// If this fails, then all DB transactions will be rollback back together.
1081
			$time = $conn->pendingWriteQueryDuration();
1082
			if ( $limit > 0 && $time > $limit ) {
1083
				throw new DBTransactionError(
1084
					$conn,
1085
					wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
1086
				);
1087
			}
1088
		} );
1089
	}
1090
1091
	/**
1092
	 * Issue COMMIT on all master connections where writes where done
1093
	 * @param string $fname Caller name
1094
	 */
1095
	public function commitMasterChanges( $fname = __METHOD__ ) {
1096
		$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
1097
			if ( $conn->writesOrCallbacksPending() ) {
1098
				$conn->commit( $fname, 'flush' );
1099
			}
1100
		} );
1101
	}
1102
1103
	/**
1104
	 * Issue all pending post-commit callbacks
1105
	 * @since 1.28
1106
	 */
1107
	public function runMasterPostCommitCallbacks() {
1108
		$this->forEachOpenMasterConnection( function ( DatabaseBase $db ) {
1109
			$db->setPostCommitCallbackSupression( false );
1110
			$db->runOnTransactionIdleCallbacks( IDatabase::TRIGGER_COMMIT );
1111
		} );
1112
	}
1113
1114
	/**
1115
	 * Issue ROLLBACK only on master, only if queries were done on connection
1116
	 * @param string $fname Caller name
1117
	 * @throws DBExpectedError
1118
	 * @since 1.23
1119
	 */
1120
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
1121
		$failedServers = [];
1122
1123
		$masterIndex = $this->getWriterIndex();
1124
		foreach ( $this->mConns as $conns2 ) {
1125
			if ( empty( $conns2[$masterIndex] ) ) {
1126
				continue;
1127
			}
1128
			/** @var DatabaseBase $conn */
1129
			foreach ( $conns2[$masterIndex] as $conn ) {
1130
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1131
					try {
1132
						$conn->rollback( $fname, 'flush' );
1133
					} catch ( DBError $e ) {
1134
						MWExceptionHandler::logException( $e );
1135
						$failedServers[] = $conn->getServer();
1136
					}
1137
				}
1138
			}
1139
		}
1140
1141
		if ( $failedServers ) {
1142
			throw new DBExpectedError( null, "Rollback failed on server(s) " .
1143
				implode( ', ', array_unique( $failedServers ) ) );
1144
		}
1145
	}
1146
1147
	/**
1148
	 * @return bool Whether a master connection is already open
1149
	 * @since 1.24
1150
	 */
1151
	public function hasMasterConnection() {
1152
		return $this->isOpen( $this->getWriterIndex() );
1153
	}
1154
1155
	/**
1156
	 * Determine if there are pending changes in a transaction by this thread
1157
	 * @since 1.23
1158
	 * @return bool
1159
	 */
1160
	public function hasMasterChanges() {
1161
		$masterIndex = $this->getWriterIndex();
1162
		foreach ( $this->mConns as $conns2 ) {
1163
			if ( empty( $conns2[$masterIndex] ) ) {
1164
				continue;
1165
			}
1166
			/** @var DatabaseBase $conn */
1167
			foreach ( $conns2[$masterIndex] as $conn ) {
1168
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1169
					return true;
1170
				}
1171
			}
1172
		}
1173
		return false;
1174
	}
1175
1176
	/**
1177
	 * Get the timestamp of the latest write query done by this thread
1178
	 * @since 1.25
1179
	 * @return float|bool UNIX timestamp or false
1180
	 */
1181 View Code Duplication
	public function lastMasterChangeTimestamp() {
1182
		$lastTime = false;
1183
		$masterIndex = $this->getWriterIndex();
1184
		foreach ( $this->mConns as $conns2 ) {
1185
			if ( empty( $conns2[$masterIndex] ) ) {
1186
				continue;
1187
			}
1188
			/** @var DatabaseBase $conn */
1189
			foreach ( $conns2[$masterIndex] as $conn ) {
1190
				$lastTime = max( $lastTime, $conn->lastDoneWrites() );
1191
			}
1192
		}
1193
		return $lastTime;
1194
	}
1195
1196
	/**
1197
	 * Check if this load balancer object had any recent or still
1198
	 * pending writes issued against it by this PHP thread
1199
	 *
1200
	 * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
1201
	 * @return bool
1202
	 * @since 1.25
1203
	 */
1204
	public function hasOrMadeRecentMasterChanges( $age = null ) {
1205
		$age = ( $age === null ) ? $this->mWaitTimeout : $age;
1206
1207
		return ( $this->hasMasterChanges()
1208
			|| $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
1209
	}
1210
1211
	/**
1212
	 * Get the list of callers that have pending master changes
1213
	 *
1214
	 * @return array
1215
	 * @since 1.27
1216
	 */
1217 View Code Duplication
	public function pendingMasterChangeCallers() {
1218
		$fnames = [];
1219
1220
		$masterIndex = $this->getWriterIndex();
1221
		foreach ( $this->mConns as $conns2 ) {
1222
			if ( empty( $conns2[$masterIndex] ) ) {
1223
				continue;
1224
			}
1225
			/** @var DatabaseBase $conn */
1226
			foreach ( $conns2[$masterIndex] as $conn ) {
1227
				$fnames = array_merge( $fnames, $conn->pendingWriteCallers() );
1228
			}
1229
		}
1230
1231
		return $fnames;
1232
	}
1233
1234
	/**
1235
	 * @param mixed $value
1236
	 * @return mixed
1237
	 */
1238
	public function waitTimeout( $value = null ) {
1239
		return wfSetVar( $this->mWaitTimeout, $value );
1240
	}
1241
1242
	/**
1243
	 * @note This method will trigger a DB connection if not yet done
1244
	 *
1245
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
1246
	 * @return bool Whether the generic connection for reads is highly "lagged"
1247
	 */
1248
	public function getLaggedSlaveMode( $wiki = false ) {
1249
		// No-op if there is only one DB (also avoids recursion)
1250
		if ( !$this->laggedSlaveMode && $this->getServerCount() > 1 ) {
1251
			try {
1252
				// See if laggedSlaveMode gets set
1253
				$conn = $this->getConnection( DB_SLAVE, false, $wiki );
1254
				$this->reuseConnection( $conn );
1255
			} catch ( DBConnectionError $e ) {
1256
				// Avoid expensive re-connect attempts and failures
1257
				$this->slavesDownMode = true;
1258
				$this->laggedSlaveMode = true;
1259
			}
1260
		}
1261
1262
		return $this->laggedSlaveMode;
1263
	}
1264
1265
	/**
1266
	 * @note This method will never cause a new DB connection
1267
	 * @return bool Whether any generic connection used for reads was highly "lagged"
1268
	 * @since 1.27
1269
	 */
1270
	public function laggedSlaveUsed() {
1271
		return $this->laggedSlaveMode;
1272
	}
1273
1274
	/**
1275
	 * @note This method may trigger a DB connection if not yet done
1276
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
1277
	 * @return string|bool Reason the master is read-only or false if it is not
1278
	 * @since 1.27
1279
	 */
1280
	public function getReadOnlyReason( $wiki = false ) {
1281
		if ( $this->readOnlyReason !== false ) {
1282
			return $this->readOnlyReason;
1283
		} elseif ( $this->getLaggedSlaveMode( $wiki ) ) {
1284
			if ( $this->slavesDownMode ) {
1285
				return 'The database has been automatically locked ' .
1286
					'until the slave database servers become available';
1287
			} else {
1288
				return 'The database has been automatically locked ' .
1289
					'while the slave database servers catch up to the master.';
1290
			}
1291
		}
1292
1293
		return false;
1294
	}
1295
1296
	/**
1297
	 * Disables/enables lag checks
1298
	 * @param null|bool $mode
1299
	 * @return bool
1300
	 */
1301
	public function allowLagged( $mode = null ) {
1302
		if ( $mode === null ) {
1303
			return $this->mAllowLagged;
1304
		}
1305
		$this->mAllowLagged = $mode;
1306
1307
		return $this->mAllowLagged;
1308
	}
1309
1310
	/**
1311
	 * @return bool
1312
	 */
1313
	public function pingAll() {
1314
		$success = true;
1315
		$this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( &$success ) {
1316
			if ( !$conn->ping() ) {
1317
				$success = false;
1318
			}
1319
		} );
1320
1321
		return $success;
1322
	}
1323
1324
	/**
1325
	 * Call a function with each open connection object
1326
	 * @param callable $callback
1327
	 * @param array $params
1328
	 */
1329
	public function forEachOpenConnection( $callback, array $params = [] ) {
1330 View Code Duplication
		foreach ( $this->mConns as $connsByServer ) {
1331
			foreach ( $connsByServer as $serverConns ) {
1332
				foreach ( $serverConns as $conn ) {
1333
					$mergedParams = array_merge( [ $conn ], $params );
1334
					call_user_func_array( $callback, $mergedParams );
1335
				}
1336
			}
1337
		}
1338
	}
1339
1340
	/**
1341
	 * Call a function with each open connection object to a master
1342
	 * @param callable $callback
1343
	 * @param array $params
1344
	 * @since 1.28
1345
	 */
1346
	public function forEachOpenMasterConnection( $callback, array $params = [] ) {
1347
		$masterIndex = $this->getWriterIndex();
1348 View Code Duplication
		foreach ( $this->mConns as $connsByServer ) {
1349
			if ( isset( $connsByServer[$masterIndex] ) ) {
1350
				/** @var DatabaseBase $conn */
1351
				foreach ( $connsByServer[$masterIndex] as $conn ) {
1352
					$mergedParams = array_merge( [ $conn ], $params );
1353
					call_user_func_array( $callback, $mergedParams );
1354
				}
1355
			}
1356
		}
1357
	}
1358
1359
	/**
1360
	 * Get the hostname and lag time of the most-lagged slave
1361
	 *
1362
	 * This is useful for maintenance scripts that need to throttle their updates.
1363
	 * May attempt to open connections to slaves on the default DB. If there is
1364
	 * no lag, the maximum lag will be reported as -1.
1365
	 *
1366
	 * @param bool|string $wiki Wiki ID, or false for the default database
1367
	 * @return array ( host, max lag, index of max lagged host )
1368
	 */
1369
	public function getMaxLag( $wiki = false ) {
1370
		$maxLag = -1;
1371
		$host = '';
1372
		$maxIndex = 0;
1373
1374
		if ( $this->getServerCount() <= 1 ) {
1375
			return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
1376
		}
1377
1378
		$lagTimes = $this->getLagTimes( $wiki );
1379
		foreach ( $lagTimes as $i => $lag ) {
1380
			if ( $this->mLoads[$i] > 0 && $lag > $maxLag ) {
1381
				$maxLag = $lag;
1382
				$host = $this->mServers[$i]['host'];
1383
				$maxIndex = $i;
1384
			}
1385
		}
1386
1387
		return [ $host, $maxLag, $maxIndex ];
1388
	}
1389
1390
	/**
1391
	 * Get an estimate of replication lag (in seconds) for each server
1392
	 *
1393
	 * Results are cached for a short time in memcached/process cache
1394
	 *
1395
	 * Values may be "false" if replication is too broken to estimate
1396
	 *
1397
	 * @param string|bool $wiki
1398
	 * @return int[] Map of (server index => float|int|bool)
1399
	 */
1400
	public function getLagTimes( $wiki = false ) {
1401
		if ( $this->getServerCount() <= 1 ) {
1402
			return [ 0 => 0 ]; // no replication = no lag
1403
		}
1404
1405
		# Send the request to the load monitor
1406
		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 1400 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...
1407
	}
1408
1409
	/**
1410
	 * Get the lag in seconds for a given connection, or zero if this load
1411
	 * balancer does not have replication enabled.
1412
	 *
1413
	 * This should be used in preference to Database::getLag() in cases where
1414
	 * replication may not be in use, since there is no way to determine if
1415
	 * replication is in use at the connection level without running
1416
	 * potentially restricted queries such as SHOW SLAVE STATUS. Using this
1417
	 * function instead of Database::getLag() avoids a fatal error in this
1418
	 * case on many installations.
1419
	 *
1420
	 * @param IDatabase $conn
1421
	 * @return int|bool Returns false on error
1422
	 */
1423
	public function safeGetLag( IDatabase $conn ) {
1424
		if ( $this->getServerCount() == 1 ) {
1425
			return 0;
1426
		} else {
1427
			return $conn->getLag();
1428
		}
1429
	}
1430
1431
	/**
1432
	 * Wait for a slave DB to reach a specified master position
1433
	 *
1434
	 * This will connect to the master to get an accurate position if $pos is not given
1435
	 *
1436
	 * @param IDatabase $conn Slave DB
1437
	 * @param DBMasterPos|bool $pos Master position; default: current position
1438
	 * @param integer $timeout Timeout in seconds
1439
	 * @return bool Success
1440
	 * @since 1.27
1441
	 */
1442
	public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
1443
		if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'slave' ) ) {
1444
			return true; // server is not a slave DB
1445
		}
1446
1447
		$pos = $pos ?: $this->getConnection( DB_MASTER )->getMasterPos();
1448
		if ( !( $pos instanceof DBMasterPos ) ) {
1449
			return false; // something is misconfigured
1450
		}
1451
1452
		$result = $conn->masterPosWait( $pos, $timeout );
1453
		if ( $result == -1 || is_null( $result ) ) {
1454
			$msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
1455
			wfDebugLog( 'replication', "$msg\n" );
1456
			wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
1457
			$ok = false;
1458
		} else {
1459
			wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
1460
			$ok = true;
1461
		}
1462
1463
		return $ok;
1464
	}
1465
1466
	/**
1467
	 * Clear the cache for slag lag delay times
1468
	 *
1469
	 * This is only used for testing
1470
	 */
1471
	public function clearLagTimeCache() {
1472
		$this->getLoadMonitor()->clearCaches();
1473
	}
1474
}
1475