Completed
Branch master (0c9f05)
by
unknown
29:21
created

LoadBalancer::approveMasterChanges()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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