Completed
Branch master (4de667)
by
unknown
26:16
created

LoadBalancer::getReadOnlyReason()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 13
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 17
rs 8.8571
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
	/** @var WANObjectCache */
53
	private $wanCache;
54
55
	/** @var bool|DatabaseBase Database connection that caused a problem */
56
	private $mErrorConnection;
57
	/** @var integer The generic (not query grouped) slave index (of $mServers) */
58
	private $mReadIndex;
59
	/** @var bool|DBMasterPos False if not set */
60
	private $mWaitForPos;
61
	/** @var bool Whether the generic reader fell back to a lagged slave */
62
	private $laggedSlaveMode = false;
63
	/** @var bool Whether the generic reader fell back to a lagged slave */
64
	private $slavesDownMode = false;
65
	/** @var string The last DB selection or connection error */
66
	private $mLastError = 'Unknown error';
67
	/** @var string|bool Reason the LB is read-only or false if not */
68
	private $readOnlyReason = false;
69
	/** @var integer Total connections opened */
70
	private $connsOpened = 0;
71
72
	/** @var TransactionProfiler */
73
	protected $trxProfiler;
74
75
	/** @var integer Warn when this many connection are held */
76
	const CONN_HELD_WARN_THRESHOLD = 10;
77
	/** @var integer Default 'max lag' when unspecified */
78
	const MAX_LAG = 10;
79
	/** @var integer Max time to wait for a slave to catch up (e.g. ChronologyProtector) */
80
	const POS_WAIT_TIMEOUT = 10;
81
	/** @var integer Seconds to cache master server read-only status */
82
	const TTL_CACHE_READONLY = 5;
83
84
	/**
85
	 * @var boolean
86
	 */
87
	private $disabled = false;
88
89
	/**
90
	 * @param array $params Array with keys:
91
	 *  - servers : Required. Array of server info structures.
92
	 *  - loadMonitor : Name of a class used to fetch server lag and load.
93
	 *  - readOnlyReason : Reason the master DB is read-only if so [optional]
94
	 * @throws MWException
95
	 */
96
	public function __construct( array $params ) {
97
		if ( !isset( $params['servers'] ) ) {
98
			throw new MWException( __CLASS__ . ': missing servers parameter' );
99
		}
100
		$this->mServers = $params['servers'];
101
		$this->mWaitTimeout = self::POS_WAIT_TIMEOUT;
102
103
		$this->mReadIndex = -1;
104
		$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...
105
		$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...
106
			'local' => [],
107
			'foreignUsed' => [],
108
			'foreignFree' => [] ];
109
		$this->mLoads = [];
110
		$this->mWaitForPos = false;
111
		$this->mErrorConnection = false;
112
		$this->mAllowLagged = false;
113
114 View Code Duplication
		if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) {
115
			$this->readOnlyReason = $params['readOnlyReason'];
116
		}
117
118
		if ( isset( $params['loadMonitor'] ) ) {
119
			$this->mLoadMonitorClass = $params['loadMonitor'];
120
		} else {
121
			$master = reset( $params['servers'] );
122
			if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) {
123
				$this->mLoadMonitorClass = 'LoadMonitorMySQL';
124
			} else {
125
				$this->mLoadMonitorClass = 'LoadMonitorNull';
126
			}
127
		}
128
129
		foreach ( $params['servers'] as $i => $server ) {
130
			$this->mLoads[$i] = $server['load'];
131
			if ( isset( $server['groupLoads'] ) ) {
132
				foreach ( $server['groupLoads'] as $group => $ratio ) {
133
					if ( !isset( $this->mGroupLoads[$group] ) ) {
134
						$this->mGroupLoads[$group] = [];
135
					}
136
					$this->mGroupLoads[$group][$i] = $ratio;
137
				}
138
			}
139
		}
140
141
		$this->srvCache = ObjectCache::getLocalServerInstance();
142
		$this->wanCache = ObjectCache::getMainWANInstance();
143
144
		if ( isset( $params['trxProfiler'] ) ) {
145
			$this->trxProfiler = $params['trxProfiler'];
146
		} else {
147
			$this->trxProfiler = new TransactionProfiler();
148
		}
149
	}
150
151
	/**
152
	 * Get a LoadMonitor instance
153
	 *
154
	 * @return LoadMonitor
155
	 */
156
	private function getLoadMonitor() {
157
		if ( !isset( $this->mLoadMonitor ) ) {
158
			$class = $this->mLoadMonitorClass;
159
			$this->mLoadMonitor = new $class( $this );
160
		}
161
162
		return $this->mLoadMonitor;
163
	}
164
165
	/**
166
	 * Get or set arbitrary data used by the parent object, usually an LBFactory
167
	 * @param mixed $x
168
	 * @return mixed
169
	 */
170
	public function parentInfo( $x = null ) {
171
		return wfSetVar( $this->mParentInfo, $x );
172
	}
173
174
	/**
175
	 * @param array $loads
176
	 * @param bool|string $wiki Wiki to get non-lagged for
177
	 * @param int $maxLag Restrict the maximum allowed lag to this many seconds
178
	 * @return bool|int|string
179
	 */
180
	private function getRandomNonLagged( array $loads, $wiki = false, $maxLag = self::MAX_LAG ) {
181
		$lags = $this->getLagTimes( $wiki );
182
183
		# Unset excessively lagged servers
184
		foreach ( $lags as $i => $lag ) {
185
			if ( $i != 0 ) {
186
				$maxServerLag = $maxLag;
187 View Code Duplication
				if ( isset( $this->mServers[$i]['max lag'] ) ) {
188
					$maxServerLag = min( $maxServerLag, $this->mServers[$i]['max lag'] );
189
				}
190
191
				$host = $this->getServerName( $i );
192
				if ( $lag === false ) {
193
					wfDebugLog( 'replication', "Server $host (#$i) is not replicating?" );
194
					unset( $loads[$i] );
195
				} elseif ( $lag > $maxServerLag ) {
196
					wfDebugLog( 'replication', "Server $host (#$i) has >= $lag seconds of lag" );
197
					unset( $loads[$i] );
198
				}
199
			}
200
		}
201
202
		# Find out if all the slaves with non-zero load are lagged
203
		$sum = 0;
204
		foreach ( $loads as $load ) {
205
			$sum += $load;
206
		}
207
		if ( $sum == 0 ) {
208
			# No appropriate DB servers except maybe the master and some slaves with zero load
209
			# Do NOT use the master
210
			# Instead, this function will return false, triggering read-only mode,
211
			# and a lagged slave will be used instead.
212
			return false;
213
		}
214
215
		if ( count( $loads ) == 0 ) {
216
			return false;
217
		}
218
219
		# Return a random representative of the remainder
220
		return ArrayUtils::pickRandom( $loads );
221
	}
222
223
	/**
224
	 * Get the index of the reader connection, which may be a slave
225
	 * This takes into account load ratios and lag times. It should
226
	 * always return a consistent index during a given invocation
227
	 *
228
	 * Side effect: opens connections to databases
229
	 * @param string|bool $group Query group, or false for the generic reader
230
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
231
	 * @throws MWException
232
	 * @return bool|int|string
233
	 */
234
	public function getReaderIndex( $group = false, $wiki = false ) {
235
		global $wgDBtype;
236
237
		# @todo FIXME: For now, only go through all this for mysql databases
238
		if ( $wgDBtype != 'mysql' ) {
239
			return $this->getWriterIndex();
240
		}
241
242
		if ( count( $this->mServers ) == 1 ) {
243
			# Skip the load balancing if there's only one server
244
			return 0;
245
		} elseif ( $group === false && $this->mReadIndex >= 0 ) {
246
			# Shortcut if generic reader exists already
247
			return $this->mReadIndex;
248
		}
249
250
		# Find the relevant load array
251
		if ( $group !== false ) {
252
			if ( isset( $this->mGroupLoads[$group] ) ) {
253
				$nonErrorLoads = $this->mGroupLoads[$group];
254
			} else {
255
				# No loads for this group, return false and the caller can use some other group
256
				wfDebugLog( 'connect', __METHOD__ . ": no loads for group $group\n" );
257
258
				return false;
259
			}
260
		} else {
261
			$nonErrorLoads = $this->mLoads;
262
		}
263
264
		if ( !count( $nonErrorLoads ) ) {
265
			throw new MWException( "Empty server array given to LoadBalancer" );
266
		}
267
268
		# Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
269
		$this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
270
271
		$laggedSlaveMode = false;
272
273
		# No server found yet
274
		$i = false;
275
		$conn = false;
276
		# First try quickly looking through the available servers for a server that
277
		# meets our criteria
278
		$currentLoads = $nonErrorLoads;
279
		while ( count( $currentLoads ) ) {
280
			if ( $this->mAllowLagged || $laggedSlaveMode ) {
281
				$i = ArrayUtils::pickRandom( $currentLoads );
282
			} else {
283
				$i = false;
284
				if ( $this->mWaitForPos && $this->mWaitForPos->asOfTime() ) {
285
					# ChronologyProtecter causes mWaitForPos to be set via sessions.
286
					# This triggers doWait() after connect, so it's especially good to
287
					# avoid lagged servers so as to avoid just blocking in that method.
288
					$ago = microtime( true ) - $this->mWaitForPos->asOfTime();
289
					# Aim for <= 1 second of waiting (being too picky can backfire)
290
					$i = $this->getRandomNonLagged( $currentLoads, $wiki, $ago + 1 );
291
				}
292
				if ( $i === false ) {
293
					# Any server with less lag than it's 'max lag' param is preferable
294
					$i = $this->getRandomNonLagged( $currentLoads, $wiki );
295
				}
296
				if ( $i === false && count( $currentLoads ) != 0 ) {
297
					# All slaves lagged. Switch to read-only mode
298
					wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode" );
299
					$i = ArrayUtils::pickRandom( $currentLoads );
300
					$laggedSlaveMode = true;
301
				}
302
			}
303
304
			if ( $i === false ) {
305
				# pickRandom() returned false
306
				# This is permanent and means the configuration or the load monitor
307
				# wants us to return false.
308
				wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false" );
309
310
				return false;
311
			}
312
313
			$serverName = $this->getServerName( $i );
314
			wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: $serverName..." );
315
316
			$conn = $this->openConnection( $i, $wiki );
317
			if ( !$conn ) {
318
				wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki" );
319
				unset( $nonErrorLoads[$i] );
320
				unset( $currentLoads[$i] );
321
				$i = false;
322
				continue;
323
			}
324
325
			// Decrement reference counter, we are finished with this connection.
326
			// It will be incremented for the caller later.
327
			if ( $wiki !== false ) {
328
				$this->reuseConnection( $conn );
329
			}
330
331
			# Return this server
332
			break;
333
		}
334
335
		# If all servers were down, quit now
336
		if ( !count( $nonErrorLoads ) ) {
337
			wfDebugLog( 'connect', "All servers down" );
338
		}
339
340
		if ( $i !== false ) {
341
			# Slave connection successful
342
			# Wait for the session master pos for a short time
343 View Code Duplication
			if ( $this->mWaitForPos && $i > 0 ) {
344
				if ( !$this->doWait( $i ) ) {
345
					$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...
346
				}
347
			}
348
			if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) {
349
				$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...
350
				# Record if the generic reader index is in "lagged slave" mode
351
				if ( $laggedSlaveMode ) {
352
					$this->laggedSlaveMode = true;
353
				}
354
			}
355
			$serverName = $this->getServerName( $i );
356
			wfDebugLog( 'connect', __METHOD__ .
357
				": using server $serverName for group '$group'\n" );
358
		}
359
360
		return $i;
361
	}
362
363
	/**
364
	 * Set the master wait position
365
	 * If a DB_SLAVE connection has been opened already, waits
366
	 * Otherwise sets a variable telling it to wait if such a connection is opened
367
	 * @param DBMasterPos $pos
368
	 */
369
	public function waitFor( $pos ) {
370
		$this->mWaitForPos = $pos;
371
		$i = $this->mReadIndex;
372
373 View Code Duplication
		if ( $i > 0 ) {
374
			if ( !$this->doWait( $i ) ) {
375
				$this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
376
				$this->laggedSlaveMode = true;
377
			}
378
		}
379
	}
380
381
	/**
382
	 * Set the master wait position and wait for a "generic" slave to catch up to it
383
	 *
384
	 * This can be used a faster proxy for waitForAll()
385
	 *
386
	 * @param DBMasterPos $pos
387
	 * @param int $timeout Max seconds to wait; default is mWaitTimeout
388
	 * @return bool Success (able to connect and no timeouts reached)
389
	 * @since 1.26
390
	 */
391
	public function waitForOne( $pos, $timeout = null ) {
392
		$this->mWaitForPos = $pos;
393
394
		$i = $this->mReadIndex;
395
		if ( $i <= 0 ) {
396
			// Pick a generic slave if there isn't one yet
397
			$readLoads = $this->mLoads;
398
			unset( $readLoads[$this->getWriterIndex()] ); // slaves only
399
			$readLoads = array_filter( $readLoads ); // with non-zero load
400
			$i = ArrayUtils::pickRandom( $readLoads );
401
		}
402
403 View Code Duplication
		if ( $i > 0 ) {
404
			$ok = $this->doWait( $i, true, $timeout );
405
		} else {
406
			$ok = true; // no applicable loads
407
		}
408
409
		return $ok;
410
	}
411
412
	/**
413
	 * Set the master wait position and wait for ALL slaves to catch up to it
414
	 * @param DBMasterPos $pos
415
	 * @param int $timeout Max seconds to wait; default is mWaitTimeout
416
	 * @return bool Success (able to connect and no timeouts reached)
417
	 */
418
	public function waitForAll( $pos, $timeout = null ) {
419
		$this->mWaitForPos = $pos;
420
		$serverCount = count( $this->mServers );
421
422
		$ok = true;
423
		for ( $i = 1; $i < $serverCount; $i++ ) {
424 View Code Duplication
			if ( $this->mLoads[$i] > 0 ) {
425
				$ok = $this->doWait( $i, true, $timeout ) && $ok;
426
			}
427
		}
428
429
		return $ok;
430
	}
431
432
	/**
433
	 * Get any open connection to a given server index, local or foreign
434
	 * Returns false if there is no connection open
435
	 *
436
	 * @param int $i
437
	 * @return DatabaseBase|bool False on failure
438
	 */
439
	public function getAnyOpenConnection( $i ) {
440
		foreach ( $this->mConns as $conns ) {
441
			if ( !empty( $conns[$i] ) ) {
442
				return reset( $conns[$i] );
443
			}
444
		}
445
446
		return false;
447
	}
448
449
	/**
450
	 * Wait for a given slave to catch up to the master pos stored in $this
451
	 * @param int $index Server index
452
	 * @param bool $open Check the server even if a new connection has to be made
453
	 * @param int $timeout Max seconds to wait; default is mWaitTimeout
454
	 * @return bool
455
	 */
456
	protected function doWait( $index, $open = false, $timeout = null ) {
457
		$close = false; // close the connection afterwards
458
459
		// Check if we already know that the DB has reached this point
460
		$server = $this->getServerName( $index );
461
		$key = $this->srvCache->makeGlobalKey( __CLASS__, 'last-known-pos', $server );
462
		/** @var DBMasterPos $knownReachedPos */
463
		$knownReachedPos = $this->srvCache->get( $key );
464
		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...
465
			wfDebugLog( 'replication', __METHOD__ .
466
				": slave $server known to be caught up (pos >= $knownReachedPos).\n" );
467
			return true;
468
		}
469
470
		// Find a connection to wait on, creating one if needed and allowed
471
		$conn = $this->getAnyOpenConnection( $index );
472
		if ( !$conn ) {
473
			if ( !$open ) {
474
				wfDebugLog( 'replication', __METHOD__ . ": no connection open for $server\n" );
475
476
				return false;
477
			} else {
478
				$conn = $this->openConnection( $index, '' );
479
				if ( !$conn ) {
480
					wfDebugLog( 'replication', __METHOD__ . ": failed to connect to $server\n" );
481
482
					return false;
483
				}
484
				// Avoid connection spam in waitForAll() when connections
485
				// are made just for the sake of doing this lag check.
486
				$close = true;
487
			}
488
		}
489
490
		wfDebugLog( 'replication', __METHOD__ . ": Waiting for slave $server to catch up...\n" );
491
		$timeout = $timeout ?: $this->mWaitTimeout;
492
		$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...
493
494
		if ( $result == -1 || is_null( $result ) ) {
495
			// Timed out waiting for slave, use master instead
496
			$msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}";
497
			wfDebugLog( 'replication', "$msg\n" );
498
			wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
499
			$ok = false;
500
		} else {
501
			wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
502
			$ok = true;
503
			// Remember that the DB reached this point
504
			$this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY );
505
		}
506
507
		if ( $close ) {
508
			$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...
509
		}
510
511
		return $ok;
512
	}
513
514
	/**
515
	 * Get a connection by index
516
	 * This is the main entry point for this class.
517
	 *
518
	 * @param int $i Server index
519
	 * @param array|string|bool $groups Query group(s), or false for the generic reader
520
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
521
	 *
522
	 * @throws MWException
523
	 * @return DatabaseBase
524
	 */
525
	public function getConnection( $i, $groups = [], $wiki = false ) {
526
		if ( $i === null || $i === false ) {
527
			throw new MWException( 'Attempt to call ' . __METHOD__ .
528
				' with invalid server index' );
529
		}
530
531
		if ( $wiki === wfWikiID() ) {
532
			$wiki = false;
533
		}
534
535
		$groups = ( $groups === false || $groups === [] )
536
			? [ false ] // check one "group": the generic pool
537
			: (array)$groups;
538
539
		$masterOnly = ( $i == DB_MASTER || $i == $this->getWriterIndex() );
540
		$oldConnsOpened = $this->connsOpened; // connections open now
541
542
		if ( $i == DB_MASTER ) {
543
			$i = $this->getWriterIndex();
544
		} else {
545
			# Try to find an available server in any the query groups (in order)
546
			foreach ( $groups as $group ) {
547
				$groupIndex = $this->getReaderIndex( $group, $wiki );
548
				if ( $groupIndex !== false ) {
549
					$i = $groupIndex;
550
					break;
551
				}
552
			}
553
		}
554
555
		# Operation-based index
556
		if ( $i == DB_SLAVE ) {
557
			$this->mLastError = 'Unknown error'; // reset error string
558
			# Try the general server pool if $groups are unavailable.
559
			$i = in_array( false, $groups, true )
560
				? false // don't bother with this if that is what was tried above
561
				: $this->getReaderIndex( false, $wiki );
562
			# Couldn't find a working server in getReaderIndex()?
563
			if ( $i === false ) {
564
				$this->mLastError = 'No working slave server: ' . $this->mLastError;
565
566
				return $this->reportConnectionError();
567
			}
568
		}
569
570
		# Now we have an explicit index into the servers array
571
		$conn = $this->openConnection( $i, $wiki );
572
		if ( !$conn ) {
573
			return $this->reportConnectionError();
574
		}
575
576
		# Profile any new connections that happen
577
		if ( $this->connsOpened > $oldConnsOpened ) {
578
			$host = $conn->getServer();
579
			$dbname = $conn->getDBname();
580
			$trxProf = Profiler::instance()->getTransactionProfiler();
581
			$trxProf->recordConnection( $host, $dbname, $masterOnly );
582
		}
583
584
		if ( $masterOnly ) {
585
			# Make master-requested DB handles inherit any read-only mode setting
586
			$conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $wiki, $conn ) );
0 ignored issues
show
Bug introduced by
It seems like $conn defined by $this->openConnection($i, $wiki) on line 571 can also be of type boolean; however, LoadBalancer::getReadOnlyReason() does only seem to accept null|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...
587
		}
588
589
		return $conn;
590
	}
591
592
	/**
593
	 * Mark a foreign connection as being available for reuse under a different
594
	 * DB name or prefix. This mechanism is reference-counted, and must be called
595
	 * the same number of times as getConnection() to work.
596
	 *
597
	 * @param DatabaseBase $conn
598
	 * @throws MWException
599
	 */
600
	public function reuseConnection( $conn ) {
601
		$serverIndex = $conn->getLBInfo( 'serverIndex' );
602
		$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
603
		if ( $serverIndex === null || $refCount === null ) {
604
			wfDebug( __METHOD__ . ": this connection was not opened as a foreign connection\n" );
605
			/**
606
			 * This can happen in code like:
607
			 *   foreach ( $dbs as $db ) {
608
			 *     $conn = $lb->getConnection( DB_SLAVE, array(), $db );
609
			 *     ...
610
			 *     $lb->reuseConnection( $conn );
611
			 *   }
612
			 * When a connection to the local DB is opened in this way, reuseConnection()
613
			 * should be ignored
614
			 */
615
			return;
616
		}
617
618
		$dbName = $conn->getDBname();
619
		$prefix = $conn->tablePrefix();
620
		if ( strval( $prefix ) !== '' ) {
621
			$wiki = "$dbName-$prefix";
622
		} else {
623
			$wiki = $dbName;
624
		}
625
		if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
626
			throw new MWException( __METHOD__ . ": connection not found, has " .
627
				"the connection been freed already?" );
628
		}
629
		$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
630
		if ( $refCount <= 0 ) {
631
			$this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
632
			unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
633
			wfDebug( __METHOD__ . ": freed connection $serverIndex/$wiki\n" );
634
		} else {
635
			wfDebug( __METHOD__ . ": reference count for $serverIndex/$wiki reduced to $refCount\n" );
636
		}
637
	}
638
639
	/**
640
	 * Get a database connection handle reference
641
	 *
642
	 * The handle's methods wrap simply wrap those of a DatabaseBase handle
643
	 *
644
	 * @see LoadBalancer::getConnection() for parameter information
645
	 *
646
	 * @param int $db
647
	 * @param array|string|bool $groups Query group(s), or false for the generic reader
648
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
649
	 * @return DBConnRef
650
	 */
651
	public function getConnectionRef( $db, $groups = [], $wiki = false ) {
652
		return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
653
	}
654
655
	/**
656
	 * Get a database connection handle reference without connecting yet
657
	 *
658
	 * The handle's methods wrap simply wrap those of a DatabaseBase handle
659
	 *
660
	 * @see LoadBalancer::getConnection() for parameter information
661
	 *
662
	 * @param int $db
663
	 * @param array|string|bool $groups Query group(s), or false for the generic reader
664
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
665
	 * @return DBConnRef
666
	 */
667
	public function getLazyConnectionRef( $db, $groups = [], $wiki = false ) {
668
		return new DBConnRef( $this, [ $db, $groups, $wiki ] );
669
	}
670
671
	/**
672
	 * Open a connection to the server given by the specified index
673
	 * Index must be an actual index into the array.
674
	 * If the server is already open, returns it.
675
	 *
676
	 * On error, returns false, and the connection which caused the
677
	 * error will be available via $this->mErrorConnection.
678
	 *
679
	 * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
680
	 *
681
	 * @param int $i Server index
682
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
683
	 * @return DatabaseBase|bool Returns false on errors
684
	 */
685
	public function openConnection( $i, $wiki = false ) {
686
		if ( $wiki !== false ) {
687
			$conn = $this->openForeignConnection( $i, $wiki );
0 ignored issues
show
Bug introduced by
It seems like $wiki defined by parameter $wiki on line 685 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...
688
		} elseif ( isset( $this->mConns['local'][$i][0] ) ) {
689
			$conn = $this->mConns['local'][$i][0];
690
		} else {
691
			$server = $this->mServers[$i];
692
			$server['serverIndex'] = $i;
693
			$conn = $this->reallyOpenConnection( $server, false );
694
			$serverName = $this->getServerName( $i );
695
			if ( $conn->isOpen() ) {
696
				wfDebugLog( 'connect', "Connected to database $i at $serverName\n" );
697
				$this->mConns['local'][$i][0] = $conn;
698
			} else {
699
				wfDebugLog( 'connect', "Failed to connect to database $i at $serverName\n" );
700
				$this->mErrorConnection = $conn;
701
				$conn = false;
702
			}
703
		}
704
705
		if ( $conn && !$conn->isOpen() ) {
706
			// Connection was made but later unrecoverably lost for some reason.
707
			// Do not return a handle that will just throw exceptions on use,
708
			// but let the calling code (e.g. getReaderIndex) try another server.
709
			// See DatabaseMyslBase::ping() for how this can happen.
710
			$this->mErrorConnection = $conn;
711
			$conn = false;
712
		}
713
714
		return $conn;
715
	}
716
717
	/**
718
	 * Open a connection to a foreign DB, or return one if it is already open.
719
	 *
720
	 * Increments a reference count on the returned connection which locks the
721
	 * connection to the requested wiki. This reference count can be
722
	 * decremented by calling reuseConnection().
723
	 *
724
	 * If a connection is open to the appropriate server already, but with the wrong
725
	 * database, it will be switched to the right database and returned, as long as
726
	 * it has been freed first with reuseConnection().
727
	 *
728
	 * On error, returns false, and the connection which caused the
729
	 * error will be available via $this->mErrorConnection.
730
	 *
731
	 * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
732
	 *
733
	 * @param int $i Server index
734
	 * @param string $wiki Wiki ID to open
735
	 * @return DatabaseBase
736
	 */
737
	private function openForeignConnection( $i, $wiki ) {
738
		list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
739
		if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
740
			// Reuse an already-used connection
741
			$conn = $this->mConns['foreignUsed'][$i][$wiki];
742
			wfDebug( __METHOD__ . ": reusing connection $i/$wiki\n" );
743
		} elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
744
			// Reuse a free connection for the same wiki
745
			$conn = $this->mConns['foreignFree'][$i][$wiki];
746
			unset( $this->mConns['foreignFree'][$i][$wiki] );
747
			$this->mConns['foreignUsed'][$i][$wiki] = $conn;
748
			wfDebug( __METHOD__ . ": reusing free connection $i/$wiki\n" );
749
		} elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
750
			// Reuse a connection from another wiki
751
			$conn = reset( $this->mConns['foreignFree'][$i] );
752
			$oldWiki = key( $this->mConns['foreignFree'][$i] );
753
754
			// The empty string as a DB name means "don't care".
755
			// DatabaseMysqlBase::open() already handle this on connection.
756
			if ( $dbName !== '' && !$conn->selectDB( $dbName ) ) {
757
				$this->mLastError = "Error selecting database $dbName on server " .
758
					$conn->getServer() . " from client host " . wfHostname() . "\n";
759
				$this->mErrorConnection = $conn;
760
				$conn = false;
761
			} else {
762
				$conn->tablePrefix( $prefix );
763
				unset( $this->mConns['foreignFree'][$i][$oldWiki] );
764
				$this->mConns['foreignUsed'][$i][$wiki] = $conn;
765
				wfDebug( __METHOD__ . ": reusing free connection from $oldWiki for $wiki\n" );
766
			}
767
		} else {
768
			// Open a new connection
769
			$server = $this->mServers[$i];
770
			$server['serverIndex'] = $i;
771
			$server['foreignPoolRefCount'] = 0;
772
			$server['foreign'] = true;
773
			$conn = $this->reallyOpenConnection( $server, $dbName );
774
			if ( !$conn->isOpen() ) {
775
				wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" );
776
				$this->mErrorConnection = $conn;
777
				$conn = false;
778
			} else {
779
				$conn->tablePrefix( $prefix );
780
				$this->mConns['foreignUsed'][$i][$wiki] = $conn;
781
				wfDebug( __METHOD__ . ": opened new connection for $i/$wiki\n" );
782
			}
783
		}
784
785
		// Increment reference count
786
		if ( $conn ) {
787
			$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
788
			$conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
789
		}
790
791
		return $conn;
792
	}
793
794
	/**
795
	 * Test if the specified index represents an open connection
796
	 *
797
	 * @param int $index Server index
798
	 * @access private
799
	 * @return bool
800
	 */
801
	private function isOpen( $index ) {
802
		if ( !is_integer( $index ) ) {
803
			return false;
804
		}
805
806
		return (bool)$this->getAnyOpenConnection( $index );
807
	}
808
809
	/**
810
	 * Really opens a connection. Uncached.
811
	 * Returns a Database object whether or not the connection was successful.
812
	 * @access private
813
	 *
814
	 * @param array $server
815
	 * @param bool $dbNameOverride
816
	 * @throws MWException
817
	 * @return DatabaseBase
818
	 */
819
	protected function reallyOpenConnection( $server, $dbNameOverride = false ) {
820
		if ( $this->disabled ) {
821
			throw new DBAccessError();
822
		}
823
824
		if ( !is_array( $server ) ) {
825
			throw new MWException( 'You must update your load-balancing configuration. ' .
826
				'See DefaultSettings.php entry for $wgDBservers.' );
827
		}
828
829
		if ( $dbNameOverride !== false ) {
830
			$server['dbname'] = $dbNameOverride;
831
		}
832
833
		// Let the handle know what the cluster master is (e.g. "db1052")
834
		$masterName = $this->getServerName( 0 );
835
		$server['clusterMasterHost'] = $masterName;
836
837
		// Log when many connection are made on requests
838
		if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) {
839
			wfDebugLog( 'DBPerformance', __METHOD__ . ": " .
840
				"{$this->connsOpened}+ connections made (master=$masterName)\n" .
841
				wfBacktrace( true ) );
842
		}
843
844
		# Create object
845
		try {
846
			$db = DatabaseBase::factory( $server['type'], $server );
847
		} catch ( DBConnectionError $e ) {
848
			// FIXME: This is probably the ugliest thing I have ever done to
849
			// PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
850
			$db = $e->db;
851
		}
852
853
		$db->setLBInfo( $server );
854
		$db->setLazyMasterHandle(
855
			$this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
856
		);
857
		$db->setTransactionProfiler( $this->trxProfiler );
858
859
		return $db;
860
	}
861
862
	/**
863
	 * @throws DBConnectionError
864
	 * @return bool
865
	 */
866
	private function reportConnectionError() {
867
		$conn = $this->mErrorConnection; // The connection which caused the error
868
		$context = [
869
			'method' => __METHOD__,
870
			'last_error' => $this->mLastError,
871
		];
872
873
		if ( !is_object( $conn ) ) {
874
			// No last connection, probably due to all servers being too busy
875
			wfLogDBError(
876
				"LB failure with no last connection. Connection error: {last_error}",
877
				$context
878
			);
879
880
			// If all servers were busy, mLastError will contain something sensible
881
			throw new DBConnectionError( null, $this->mLastError );
882
		} else {
883
			$context['db_server'] = $conn->getProperty( 'mServer' );
884
			wfLogDBError(
885
				"Connection error: {last_error} ({db_server})",
886
				$context
887
			);
888
889
			// throws DBConnectionError
890
			$conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
891
		}
892
893
		return false; /* not reached */
894
	}
895
896
	/**
897
	 * @return int
898
	 * @since 1.26
899
	 */
900
	public function getWriterIndex() {
901
		return 0;
902
	}
903
904
	/**
905
	 * Returns true if the specified index is a valid server index
906
	 *
907
	 * @param string $i
908
	 * @return bool
909
	 */
910
	public function haveIndex( $i ) {
911
		return array_key_exists( $i, $this->mServers );
912
	}
913
914
	/**
915
	 * Returns true if the specified index is valid and has non-zero load
916
	 *
917
	 * @param string $i
918
	 * @return bool
919
	 */
920
	public function isNonZeroLoad( $i ) {
921
		return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
922
	}
923
924
	/**
925
	 * Get the number of defined servers (not the number of open connections)
926
	 *
927
	 * @return int
928
	 */
929
	public function getServerCount() {
930
		return count( $this->mServers );
931
	}
932
933
	/**
934
	 * Get the host name or IP address of the server with the specified index
935
	 * Prefer a readable name if available.
936
	 * @param string $i
937
	 * @return string
938
	 */
939
	public function getServerName( $i ) {
940
		if ( isset( $this->mServers[$i]['hostName'] ) ) {
941
			$name = $this->mServers[$i]['hostName'];
942 View Code Duplication
		} elseif ( isset( $this->mServers[$i]['host'] ) ) {
943
			$name = $this->mServers[$i]['host'];
944
		} else {
945
			$name = '';
946
		}
947
948
		return ( $name != '' ) ? $name : 'localhost';
949
	}
950
951
	/**
952
	 * Return the server info structure for a given index, or false if the index is invalid.
953
	 * @param int $i
954
	 * @return array|bool
955
	 */
956
	public function getServerInfo( $i ) {
957
		if ( isset( $this->mServers[$i] ) ) {
958
			return $this->mServers[$i];
959
		} else {
960
			return false;
961
		}
962
	}
963
964
	/**
965
	 * Sets the server info structure for the given index. Entry at index $i
966
	 * is created if it doesn't exist
967
	 * @param int $i
968
	 * @param array $serverInfo
969
	 */
970
	public function setServerInfo( $i, array $serverInfo ) {
971
		$this->mServers[$i] = $serverInfo;
972
	}
973
974
	/**
975
	 * Get the current master position for chronology control purposes
976
	 * @return mixed
977
	 */
978
	public function getMasterPos() {
979
		# If this entire request was served from a slave without opening a connection to the
980
		# master (however unlikely that may be), then we can fetch the position from the slave.
981
		$masterConn = $this->getAnyOpenConnection( 0 );
982
		if ( !$masterConn ) {
983
			$serverCount = count( $this->mServers );
984
			for ( $i = 1; $i < $serverCount; $i++ ) {
985
				$conn = $this->getAnyOpenConnection( $i );
986
				if ( $conn ) {
987
					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...
988
				}
989
			}
990
		} else {
991
			return $masterConn->getMasterPos();
992
		}
993
994
		return false;
995
	}
996
997
	/**
998
	 * Disable this load balancer. All connections are closed, and any attempt to
999
	 * open a new connection will result in a DBAccessError.
1000
	 *
1001
	 * @since 1.27
1002
	 */
1003
	public function disable() {
1004
		$this->closeAll();
1005
		$this->disabled = true;
1006
	}
1007
1008
	/**
1009
	 * Close all open connections
1010
	 */
1011
	public function closeAll() {
1012
		$this->forEachOpenConnection( function ( DatabaseBase $conn ) {
1013
			$conn->close();
1014
		} );
1015
1016
		$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...
1017
			'local' => [],
1018
			'foreignFree' => [],
1019
			'foreignUsed' => [],
1020
		];
1021
		$this->connsOpened = 0;
1022
	}
1023
1024
	/**
1025
	 * Close a connection
1026
	 * Using this function makes sure the LoadBalancer knows the connection is closed.
1027
	 * If you use $conn->close() directly, the load balancer won't update its state.
1028
	 * @param DatabaseBase $conn
1029
	 */
1030
	public function closeConnection( $conn ) {
1031
		$done = false;
1032
		foreach ( $this->mConns as $i1 => $conns2 ) {
1033
			foreach ( $conns2 as $i2 => $conns3 ) {
1034
				foreach ( $conns3 as $i3 => $candidateConn ) {
1035
					if ( $conn === $candidateConn ) {
1036
						$conn->close();
1037
						unset( $this->mConns[$i1][$i2][$i3] );
1038
						--$this->connsOpened;
1039
						$done = true;
1040
						break;
1041
					}
1042
				}
1043
			}
1044
		}
1045
		if ( !$done ) {
1046
			$conn->close();
1047
		}
1048
	}
1049
1050
	/**
1051
	 * Commit transactions on all open connections
1052
	 * @param string $fname Caller name
1053
	 */
1054
	public function commitAll( $fname = __METHOD__ ) {
1055
		$this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( $fname ) {
1056
			$conn->commit( $fname, 'flush' );
1057
		} );
1058
	}
1059
1060
	/**
1061
	 * Perform all pre-commit callbacks that remain part of the atomic transactions
1062
	 * and disable any post-commit callbacks until runMasterPostCommitCallbacks()
1063
	 * @since 1.28
1064
	 */
1065
	public function runMasterPreCommitCallbacks() {
1066
		$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
1067
			// Any error will cause all DB transactions to be rolled back together.
1068
			$conn->runOnTransactionPreCommitCallbacks();
1069
			// Defer post-commit callbacks until COMMIT finishes for all DBs.
1070
			$conn->setPostCommitCallbackSupression( true );
1071
		} );
1072
	}
1073
1074
	/**
1075
	 * Perform all pre-commit checks for things like replication safety
1076
	 * @param array $options Includes:
1077
	 *   - maxWriteDuration : max write query duration time in seconds
1078
	 * @throws DBTransactionError
1079
	 * @since 1.28
1080
	 */
1081
	public function approveMasterChanges( array $options ) {
1082
		$limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
1083
		$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $limit ) {
1084
			// Assert that the time to replicate the transaction will be sane.
1085
			// If this fails, then all DB transactions will be rollback back together.
1086
			$time = $conn->pendingWriteQueryDuration();
1087
			if ( $limit > 0 && $time > $limit ) {
1088
				throw new DBTransactionError(
1089
					$conn,
1090
					wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
1091
				);
1092
			}
1093
		} );
1094
	}
1095
1096
	/**
1097
	 * Issue COMMIT on all master connections where writes where done
1098
	 * @param string $fname Caller name
1099
	 */
1100
	public function commitMasterChanges( $fname = __METHOD__ ) {
1101
		$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
1102
			if ( $conn->writesOrCallbacksPending() ) {
1103
				$conn->commit( $fname, 'flush' );
1104
			}
1105
		} );
1106
	}
1107
1108
	/**
1109
	 * Issue all pending post-commit callbacks
1110
	 * @since 1.28
1111
	 */
1112
	public function runMasterPostCommitCallbacks() {
1113
		$this->forEachOpenMasterConnection( function ( DatabaseBase $db ) {
1114
			$db->setPostCommitCallbackSupression( false );
1115
			$db->runOnTransactionIdleCallbacks( IDatabase::TRIGGER_COMMIT );
1116
		} );
1117
	}
1118
1119
	/**
1120
	 * Issue ROLLBACK only on master, only if queries were done on connection
1121
	 * @param string $fname Caller name
1122
	 * @throws DBExpectedError
1123
	 * @since 1.23
1124
	 */
1125
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
1126
		$failedServers = [];
1127
1128
		$masterIndex = $this->getWriterIndex();
1129
		foreach ( $this->mConns as $conns2 ) {
1130
			if ( empty( $conns2[$masterIndex] ) ) {
1131
				continue;
1132
			}
1133
			/** @var DatabaseBase $conn */
1134
			foreach ( $conns2[$masterIndex] as $conn ) {
1135
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1136
					try {
1137
						$conn->rollback( $fname, 'flush' );
1138
					} catch ( DBError $e ) {
1139
						MWExceptionHandler::logException( $e );
1140
						$failedServers[] = $conn->getServer();
1141
					}
1142
				}
1143
			}
1144
		}
1145
1146
		if ( $failedServers ) {
1147
			throw new DBExpectedError( null, "Rollback failed on server(s) " .
1148
				implode( ', ', array_unique( $failedServers ) ) );
1149
		}
1150
	}
1151
1152
	/**
1153
	 * @return bool Whether a master connection is already open
1154
	 * @since 1.24
1155
	 */
1156
	public function hasMasterConnection() {
1157
		return $this->isOpen( $this->getWriterIndex() );
1158
	}
1159
1160
	/**
1161
	 * Determine if there are pending changes in a transaction by this thread
1162
	 * @since 1.23
1163
	 * @return bool
1164
	 */
1165
	public function hasMasterChanges() {
1166
		$masterIndex = $this->getWriterIndex();
1167
		foreach ( $this->mConns as $conns2 ) {
1168
			if ( empty( $conns2[$masterIndex] ) ) {
1169
				continue;
1170
			}
1171
			/** @var DatabaseBase $conn */
1172
			foreach ( $conns2[$masterIndex] as $conn ) {
1173
				if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
1174
					return true;
1175
				}
1176
			}
1177
		}
1178
		return false;
1179
	}
1180
1181
	/**
1182
	 * Get the timestamp of the latest write query done by this thread
1183
	 * @since 1.25
1184
	 * @return float|bool UNIX timestamp or false
1185
	 */
1186 View Code Duplication
	public function lastMasterChangeTimestamp() {
1187
		$lastTime = false;
1188
		$masterIndex = $this->getWriterIndex();
1189
		foreach ( $this->mConns as $conns2 ) {
1190
			if ( empty( $conns2[$masterIndex] ) ) {
1191
				continue;
1192
			}
1193
			/** @var DatabaseBase $conn */
1194
			foreach ( $conns2[$masterIndex] as $conn ) {
1195
				$lastTime = max( $lastTime, $conn->lastDoneWrites() );
1196
			}
1197
		}
1198
		return $lastTime;
1199
	}
1200
1201
	/**
1202
	 * Check if this load balancer object had any recent or still
1203
	 * pending writes issued against it by this PHP thread
1204
	 *
1205
	 * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
1206
	 * @return bool
1207
	 * @since 1.25
1208
	 */
1209
	public function hasOrMadeRecentMasterChanges( $age = null ) {
1210
		$age = ( $age === null ) ? $this->mWaitTimeout : $age;
1211
1212
		return ( $this->hasMasterChanges()
1213
			|| $this->lastMasterChangeTimestamp() > microtime( true ) - $age );
1214
	}
1215
1216
	/**
1217
	 * Get the list of callers that have pending master changes
1218
	 *
1219
	 * @return array
1220
	 * @since 1.27
1221
	 */
1222 View Code Duplication
	public function pendingMasterChangeCallers() {
1223
		$fnames = [];
1224
1225
		$masterIndex = $this->getWriterIndex();
1226
		foreach ( $this->mConns as $conns2 ) {
1227
			if ( empty( $conns2[$masterIndex] ) ) {
1228
				continue;
1229
			}
1230
			/** @var DatabaseBase $conn */
1231
			foreach ( $conns2[$masterIndex] as $conn ) {
1232
				$fnames = array_merge( $fnames, $conn->pendingWriteCallers() );
1233
			}
1234
		}
1235
1236
		return $fnames;
1237
	}
1238
1239
	/**
1240
	 * @param mixed $value
1241
	 * @return mixed
1242
	 */
1243
	public function waitTimeout( $value = null ) {
1244
		return wfSetVar( $this->mWaitTimeout, $value );
1245
	}
1246
1247
	/**
1248
	 * @note This method will trigger a DB connection if not yet done
1249
	 *
1250
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
1251
	 * @return bool Whether the generic connection for reads is highly "lagged"
1252
	 */
1253
	public function getLaggedSlaveMode( $wiki = false ) {
1254
		// No-op if there is only one DB (also avoids recursion)
1255
		if ( !$this->laggedSlaveMode && $this->getServerCount() > 1 ) {
1256
			try {
1257
				// See if laggedSlaveMode gets set
1258
				$conn = $this->getConnection( DB_SLAVE, false, $wiki );
1259
				$this->reuseConnection( $conn );
1260
			} catch ( DBConnectionError $e ) {
1261
				// Avoid expensive re-connect attempts and failures
1262
				$this->slavesDownMode = true;
1263
				$this->laggedSlaveMode = true;
1264
			}
1265
		}
1266
1267
		return $this->laggedSlaveMode;
1268
	}
1269
1270
	/**
1271
	 * @note This method will never cause a new DB connection
1272
	 * @return bool Whether any generic connection used for reads was highly "lagged"
1273
	 * @since 1.27
1274
	 */
1275
	public function laggedSlaveUsed() {
1276
		return $this->laggedSlaveMode;
1277
	}
1278
1279
	/**
1280
	 * @note This method may trigger a DB connection if not yet done
1281
	 * @param string|bool $wiki Wiki ID, or false for the current wiki
1282
	 * @param DatabaseBase|null DB master connection; used to avoid loops [optional]
1283
	 * @return string|bool Reason the master is read-only or false if it is not
1284
	 * @since 1.27
1285
	 */
1286
	public function getReadOnlyReason( $wiki = false, DatabaseBase $conn = null ) {
1287
		if ( $this->readOnlyReason !== false ) {
1288
			return $this->readOnlyReason;
1289
		} elseif ( $this->getLaggedSlaveMode( $wiki ) ) {
1290
			if ( $this->slavesDownMode ) {
1291
				return 'The database has been automatically locked ' .
1292
					'until the slave database servers become available';
1293
			} else {
1294
				return 'The database has been automatically locked ' .
1295
					'while the slave database servers catch up to the master.';
1296
			}
1297
		} elseif ( $this->masterRunningReadOnly( $wiki, $conn ) ) {
0 ignored issues
show
Bug introduced by
It seems like $wiki defined by parameter $wiki on line 1286 can also be of type boolean; however, LoadBalancer::masterRunningReadOnly() 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...
1298
			return 'The database master is running in read-only mode.';
1299
		}
1300
1301
		return false;
1302
	}
1303
1304
	/**
1305
	 * @param string $wiki Wiki ID, or false for the current wiki
1306
	 * @param DatabaseBase|null DB master connectionl used to avoid loops [optional]
1307
	 * @return bool
1308
	 */
1309
	private function masterRunningReadOnly( $wiki, DatabaseBase $conn = null ) {
1310
		$cache = $this->wanCache;
1311
		$masterServer = $this->getServerName( $this->getWriterIndex() );
1312
1313
		return (bool)$cache->getWithSetCallback(
1314
			$cache->makeGlobalKey( __CLASS__, 'server-read-only', $masterServer ),
1315
			self::TTL_CACHE_READONLY,
1316
			function () use ( $wiki, $conn ) {
1317
				try {
1318
					$dbw = $conn ?: $this->getConnection( DB_MASTER, [], $wiki );
1319
					return (int)$dbw->serverIsReadOnly();
1320
				} catch ( DBError $e ) {
1321
					return 0;
1322
				}
1323
			},
1324
			[ 'pcTTL' => $cache::TTL_PROC_LONG, 'busyValue' => 0 ]
1325
		);
1326
	}
1327
1328
	/**
1329
	 * Disables/enables lag checks
1330
	 * @param null|bool $mode
1331
	 * @return bool
1332
	 */
1333
	public function allowLagged( $mode = null ) {
1334
		if ( $mode === null ) {
1335
			return $this->mAllowLagged;
1336
		}
1337
		$this->mAllowLagged = $mode;
1338
1339
		return $this->mAllowLagged;
1340
	}
1341
1342
	/**
1343
	 * @return bool
1344
	 */
1345
	public function pingAll() {
1346
		$success = true;
1347
		$this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( &$success ) {
1348
			if ( !$conn->ping() ) {
1349
				$success = false;
1350
			}
1351
		} );
1352
1353
		return $success;
1354
	}
1355
1356
	/**
1357
	 * Call a function with each open connection object
1358
	 * @param callable $callback
1359
	 * @param array $params
1360
	 */
1361
	public function forEachOpenConnection( $callback, array $params = [] ) {
1362 View Code Duplication
		foreach ( $this->mConns as $connsByServer ) {
1363
			foreach ( $connsByServer as $serverConns ) {
1364
				foreach ( $serverConns as $conn ) {
1365
					$mergedParams = array_merge( [ $conn ], $params );
1366
					call_user_func_array( $callback, $mergedParams );
1367
				}
1368
			}
1369
		}
1370
	}
1371
1372
	/**
1373
	 * Call a function with each open connection object to a master
1374
	 * @param callable $callback
1375
	 * @param array $params
1376
	 * @since 1.28
1377
	 */
1378
	public function forEachOpenMasterConnection( $callback, array $params = [] ) {
1379
		$masterIndex = $this->getWriterIndex();
1380 View Code Duplication
		foreach ( $this->mConns as $connsByServer ) {
1381
			if ( isset( $connsByServer[$masterIndex] ) ) {
1382
				/** @var DatabaseBase $conn */
1383
				foreach ( $connsByServer[$masterIndex] as $conn ) {
1384
					$mergedParams = array_merge( [ $conn ], $params );
1385
					call_user_func_array( $callback, $mergedParams );
1386
				}
1387
			}
1388
		}
1389
	}
1390
1391
	/**
1392
	 * Get the hostname and lag time of the most-lagged slave
1393
	 *
1394
	 * This is useful for maintenance scripts that need to throttle their updates.
1395
	 * May attempt to open connections to slaves on the default DB. If there is
1396
	 * no lag, the maximum lag will be reported as -1.
1397
	 *
1398
	 * @param bool|string $wiki Wiki ID, or false for the default database
1399
	 * @return array ( host, max lag, index of max lagged host )
1400
	 */
1401
	public function getMaxLag( $wiki = false ) {
1402
		$maxLag = -1;
1403
		$host = '';
1404
		$maxIndex = 0;
1405
1406
		if ( $this->getServerCount() <= 1 ) {
1407
			return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
1408
		}
1409
1410
		$lagTimes = $this->getLagTimes( $wiki );
1411
		foreach ( $lagTimes as $i => $lag ) {
1412
			if ( $this->mLoads[$i] > 0 && $lag > $maxLag ) {
1413
				$maxLag = $lag;
1414
				$host = $this->mServers[$i]['host'];
1415
				$maxIndex = $i;
1416
			}
1417
		}
1418
1419
		return [ $host, $maxLag, $maxIndex ];
1420
	}
1421
1422
	/**
1423
	 * Get an estimate of replication lag (in seconds) for each server
1424
	 *
1425
	 * Results are cached for a short time in memcached/process cache
1426
	 *
1427
	 * Values may be "false" if replication is too broken to estimate
1428
	 *
1429
	 * @param string|bool $wiki
1430
	 * @return int[] Map of (server index => float|int|bool)
1431
	 */
1432
	public function getLagTimes( $wiki = false ) {
1433
		if ( $this->getServerCount() <= 1 ) {
1434
			return [ 0 => 0 ]; // no replication = no lag
1435
		}
1436
1437
		# Send the request to the load monitor
1438
		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 1432 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...
1439
	}
1440
1441
	/**
1442
	 * Get the lag in seconds for a given connection, or zero if this load
1443
	 * balancer does not have replication enabled.
1444
	 *
1445
	 * This should be used in preference to Database::getLag() in cases where
1446
	 * replication may not be in use, since there is no way to determine if
1447
	 * replication is in use at the connection level without running
1448
	 * potentially restricted queries such as SHOW SLAVE STATUS. Using this
1449
	 * function instead of Database::getLag() avoids a fatal error in this
1450
	 * case on many installations.
1451
	 *
1452
	 * @param IDatabase $conn
1453
	 * @return int|bool Returns false on error
1454
	 */
1455
	public function safeGetLag( IDatabase $conn ) {
1456
		if ( $this->getServerCount() == 1 ) {
1457
			return 0;
1458
		} else {
1459
			return $conn->getLag();
1460
		}
1461
	}
1462
1463
	/**
1464
	 * Wait for a slave DB to reach a specified master position
1465
	 *
1466
	 * This will connect to the master to get an accurate position if $pos is not given
1467
	 *
1468
	 * @param IDatabase $conn Slave DB
1469
	 * @param DBMasterPos|bool $pos Master position; default: current position
1470
	 * @param integer $timeout Timeout in seconds
1471
	 * @return bool Success
1472
	 * @since 1.27
1473
	 */
1474
	public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
1475
		if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'slave' ) ) {
1476
			return true; // server is not a slave DB
1477
		}
1478
1479
		$pos = $pos ?: $this->getConnection( DB_MASTER )->getMasterPos();
1480
		if ( !( $pos instanceof DBMasterPos ) ) {
1481
			return false; // something is misconfigured
1482
		}
1483
1484
		$result = $conn->masterPosWait( $pos, $timeout );
1485
		if ( $result == -1 || is_null( $result ) ) {
1486
			$msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
1487
			wfDebugLog( 'replication', "$msg\n" );
1488
			wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
1489
			$ok = false;
1490
		} else {
1491
			wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
1492
			$ok = true;
1493
		}
1494
1495
		return $ok;
1496
	}
1497
1498
	/**
1499
	 * Clear the cache for slag lag delay times
1500
	 *
1501
	 * This is only used for testing
1502
	 */
1503
	public function clearLagTimeCache() {
1504
		$this->getLoadMonitor()->clearCaches();
1505
	}
1506
}
1507