Completed
Branch master (ee71c2)
by
unknown
26:37
created

LBFactory::singleton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Generator of database load balancing objects.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Database
22
 */
23
24
use MediaWiki\MediaWikiServices;
25
use MediaWiki\Services\DestructibleService;
26
use Psr\Log\LoggerInterface;
27
use MediaWiki\Logger\LoggerFactory;
28
29
/**
30
 * An interface for generating database load balancers
31
 * @ingroup Database
32
 */
33
abstract class LBFactory implements DestructibleService {
34
35
	/** @var ChronologyProtector */
36
	protected $chronProt;
37
38
	/** @var TransactionProfiler */
39
	protected $trxProfiler;
40
41
	/** @var LoggerInterface */
42
	protected $logger;
43
44
	/** @var string|bool Reason all LBs are read-only or false if not */
45
	protected $readOnlyReason = false;
46
47
	const SHUTDOWN_NO_CHRONPROT = 1; // don't save ChronologyProtector positions (for async code)
48
49
	/**
50
	 * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
51
	 * @param array $conf
52
	 */
53
	public function __construct( array $conf ) {
54 View Code Duplication
		if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
55
			$this->readOnlyReason = $conf['readOnlyReason'];
56
		}
57
58
		$this->chronProt = $this->newChronologyProtector();
59
		$this->trxProfiler = Profiler::instance()->getTransactionProfiler();
60
		$this->logger = LoggerFactory::getInstance( 'DBTransaction' );
61
	}
62
63
	/**
64
	 * Disables all load balancers. All connections are closed, and any attempt to
65
	 * open a new connection will result in a DBAccessError.
66
	 * @see LoadBalancer::disable()
67
	 */
68
	public function destroy() {
69
		$this->shutdown();
70
		$this->forEachLBCallMethod( 'disable' );
71
	}
72
73
	/**
74
	 * Disables all access to the load balancer, will cause all database access
75
	 * to throw a DBAccessError
76
	 */
77
	public static function disableBackend() {
78
		MediaWikiServices::disableStorageBackend();
79
	}
80
81
	/**
82
	 * Get an LBFactory instance
83
	 *
84
	 * @deprecated since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.
85
	 *
86
	 * @return LBFactory
87
	 */
88
	public static function singleton() {
89
		return MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
90
	}
91
92
	/**
93
	 * Returns the LBFactory class to use and the load balancer configuration.
94
	 *
95
	 * @todo instead of this, use a ServiceContainer for managing the different implementations.
96
	 *
97
	 * @param array $config (e.g. $wgLBFactoryConf)
98
	 * @return string Class name
99
	 */
100
	public static function getLBFactoryClass( array $config ) {
101
		// For configuration backward compatibility after removing
102
		// underscores from class names in MediaWiki 1.23.
103
		$bcClasses = [
104
			'LBFactory_Simple' => 'LBFactorySimple',
105
			'LBFactory_Single' => 'LBFactorySingle',
106
			'LBFactory_Multi' => 'LBFactoryMulti',
107
			'LBFactory_Fake' => 'LBFactoryFake',
108
		];
109
110
		$class = $config['class'];
111
112
		if ( isset( $bcClasses[$class] ) ) {
113
			$class = $bcClasses[$class];
114
			wfDeprecated(
115
				'$wgLBFactoryConf must be updated. See RELEASE-NOTES for details',
116
				'1.23'
117
			);
118
		}
119
120
		return $class;
121
	}
122
123
	/**
124
	 * Shut down, close connections and destroy the cached instance.
125
	 *
126
	 * @deprecated since 1.27, use LBFactory::destroy()
127
	 */
128
	public static function destroyInstance() {
129
		self::singleton()->destroy();
0 ignored issues
show
Deprecated Code introduced by
The method LBFactory::singleton() has been deprecated with message: since 1.27, use MediaWikiServices::getDBLoadBalancerFactory() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
130
	}
131
132
	/**
133
	 * Create a new load balancer object. The resulting object will be untracked,
134
	 * not chronology-protected, and the caller is responsible for cleaning it up.
135
	 *
136
	 * @param bool|string $wiki Wiki ID, or false for the current wiki
137
	 * @return LoadBalancer
138
	 */
139
	abstract public function newMainLB( $wiki = false );
140
141
	/**
142
	 * Get a cached (tracked) load balancer object.
143
	 *
144
	 * @param bool|string $wiki Wiki ID, or false for the current wiki
145
	 * @return LoadBalancer
146
	 */
147
	abstract public function getMainLB( $wiki = false );
148
149
	/**
150
	 * Create a new load balancer for external storage. The resulting object will be
151
	 * untracked, not chronology-protected, and the caller is responsible for
152
	 * cleaning it up.
153
	 *
154
	 * @param string $cluster External storage cluster, or false for core
155
	 * @param bool|string $wiki Wiki ID, or false for the current wiki
156
	 * @return LoadBalancer
157
	 */
158
	abstract protected function newExternalLB( $cluster, $wiki = false );
159
160
	/**
161
	 * Get a cached (tracked) load balancer for external storage
162
	 *
163
	 * @param string $cluster External storage cluster, or false for core
164
	 * @param bool|string $wiki Wiki ID, or false for the current wiki
165
	 * @return LoadBalancer
166
	 */
167
	abstract public function &getExternalLB( $cluster, $wiki = false );
168
169
	/**
170
	 * Execute a function for each tracked load balancer
171
	 * The callback is called with the load balancer as the first parameter,
172
	 * and $params passed as the subsequent parameters.
173
	 *
174
	 * @param callable $callback
175
	 * @param array $params
176
	 */
177
	abstract public function forEachLB( $callback, array $params = [] );
178
179
	/**
180
	 * Prepare all tracked load balancers for shutdown
181
	 * @param integer $flags Supports SHUTDOWN_* flags
182
	 * STUB
183
	 */
184
	public function shutdown( $flags = 0 ) {
185
	}
186
187
	/**
188
	 * Call a method of each tracked load balancer
189
	 *
190
	 * @param string $methodName
191
	 * @param array $args
192
	 */
193
	private function forEachLBCallMethod( $methodName, array $args = [] ) {
194
		$this->forEachLB(
195
			function ( LoadBalancer $loadBalancer, $methodName, array $args ) {
196
				call_user_func_array( [ $loadBalancer, $methodName ], $args );
197
			},
198
			[ $methodName, $args ]
199
		);
200
	}
201
202
	/**
203
	 * Commit on all connections. Done for two reasons:
204
	 * 1. To commit changes to the masters.
205
	 * 2. To release the snapshot on all connections, master and slave.
206
	 * @param string $fname Caller name
207
	 */
208
	public function commitAll( $fname = __METHOD__ ) {
209
		$this->logMultiDbTransaction();
210
211
		$start = microtime( true );
212
		$this->forEachLBCallMethod( 'commitAll', [ $fname ] );
213
		$timeMs = 1000 * ( microtime( true ) - $start );
214
215
		RequestContext::getMain()->getStats()->timing( "db.commit-all", $timeMs );
0 ignored issues
show
Deprecated Code introduced by
The method RequestContext::getStats() has been deprecated with message: since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
216
	}
217
218
	/**
219
	 * Commit changes on all master connections
220
	 * @param string $fname Caller name
221
	 * @param array $options Options map:
222
	 *   - maxWriteDuration: abort if more than this much time was spent in write queries
223
	 */
224
	public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
225
		$limit = isset( $options['maxWriteDuration'] ) ? $options['maxWriteDuration'] : 0;
226
227
		// Run pre-commit callbacks to keep them out of the COMMIT step. If one errors out here
228
		// then all DB transactions can be rolled back before anything was committed yet.
229
		$this->forEachLBCallMethod( 'runPreCommitCallbacks' );
230
231
		$this->logMultiDbTransaction();
232
		$this->forEachLB( function ( LoadBalancer $lb ) use ( $limit ) {
233
			$lb->forEachOpenConnection( function ( IDatabase $db ) use ( $limit ) {
234
				$time = $db->pendingWriteQueryDuration();
235
				if ( $limit > 0 && $time > $limit ) {
236
					throw new DBTransactionError(
237
						$db,
238
						wfMessage( 'transaction-duration-limit-exceeded', $time, $limit )->text()
239
					);
240
				}
241
			} );
242
		} );
243
244
		$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
245
	}
246
247
	/**
248
	 * Rollback changes on all master connections
249
	 * @param string $fname Caller name
250
	 * @since 1.23
251
	 */
252
	public function rollbackMasterChanges( $fname = __METHOD__ ) {
253
		$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
254
	}
255
256
	/**
257
	 * Log query info if multi DB transactions are going to be committed now
258
	 */
259
	private function logMultiDbTransaction() {
260
		$callersByDB = [];
261
		$this->forEachLB( function ( LoadBalancer $lb ) use ( &$callersByDB ) {
262
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
263
			$callers = $lb->pendingMasterChangeCallers();
264
			if ( $callers ) {
265
				$callersByDB[$masterName] = $callers;
266
			}
267
		} );
268
269
		if ( count( $callersByDB ) >= 2 ) {
270
			$dbs = implode( ', ', array_keys( $callersByDB ) );
271
			$msg = "Multi-DB transaction [{$dbs}]:\n";
272
			foreach ( $callersByDB as $db => $callers ) {
273
				$msg .= "$db: " . implode( '; ', $callers ) . "\n";
274
			}
275
			$this->logger->info( $msg );
276
		}
277
	}
278
279
	/**
280
	 * Determine if any master connection has pending changes
281
	 * @return bool
282
	 * @since 1.23
283
	 */
284
	public function hasMasterChanges() {
285
		$ret = false;
286
		$this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
287
			$ret = $ret || $lb->hasMasterChanges();
288
		} );
289
290
		return $ret;
291
	}
292
293
	/**
294
	 * Detemine if any lagged slave connection was used
295
	 * @since 1.27
296
	 * @return bool
297
	 */
298
	public function laggedSlaveUsed() {
299
		$ret = false;
300
		$this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
301
			$ret = $ret || $lb->laggedSlaveUsed();
302
		} );
303
304
		return $ret;
305
	}
306
307
	/**
308
	 * Determine if any master connection has pending/written changes from this request
309
	 * @return bool
310
	 * @since 1.27
311
	 */
312
	public function hasOrMadeRecentMasterChanges() {
313
		$ret = false;
314
		$this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
315
			$ret = $ret || $lb->hasOrMadeRecentMasterChanges();
316
		} );
317
		return $ret;
318
	}
319
320
	/**
321
	 * Waits for the slave DBs to catch up to the current master position
322
	 *
323
	 * Use this when updating very large numbers of rows, as in maintenance scripts,
324
	 * to avoid causing too much lag. Of course, this is a no-op if there are no slaves.
325
	 *
326
	 * By default this waits on all DB clusters actually used in this request.
327
	 * This makes sense when lag being waiting on is caused by the code that does this check.
328
	 * In that case, setting "ifWritesSince" can avoid the overhead of waiting for clusters
329
	 * that were not changed since the last wait check. To forcefully wait on a specific cluster
330
	 * for a given wiki, use the 'wiki' parameter. To forcefully wait on an "external" cluster,
331
	 * use the "cluster" parameter.
332
	 *
333
	 * Never call this function after a large DB write that is *still* in a transaction.
334
	 * It only makes sense to call this after the possible lag inducing changes were committed.
335
	 *
336
	 * @param array $opts Optional fields that include:
337
	 *   - wiki : wait on the load balancer DBs that handles the given wiki
338
	 *   - cluster : wait on the given external load balancer DBs
339
	 *   - timeout : Max wait time. Default: ~60 seconds
340
	 *   - ifWritesSince: Only wait if writes were done since this UNIX timestamp
341
	 * @throws DBReplicationWaitError If a timeout or error occured waiting on a DB cluster
342
	 * @since 1.27
343
	 */
344
	public function waitForReplication( array $opts = [] ) {
345
		$opts += [
346
			'wiki' => false,
347
			'cluster' => false,
348
			'timeout' => 60,
349
			'ifWritesSince' => null
350
		];
351
352
		// Figure out which clusters need to be checked
353
		/** @var LoadBalancer[] $lbs */
354
		$lbs = [];
355
		if ( $opts['cluster'] !== false ) {
356
			$lbs[] = $this->getExternalLB( $opts['cluster'] );
357
		} elseif ( $opts['wiki'] !== false ) {
358
			$lbs[] = $this->getMainLB( $opts['wiki'] );
359
		} else {
360
			$this->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) {
361
				$lbs[] = $lb;
362
			} );
363
			if ( !$lbs ) {
364
				return; // nothing actually used
365
			}
366
		}
367
368
		// Get all the master positions of applicable DBs right now.
369
		// This can be faster since waiting on one cluster reduces the
370
		// time needed to wait on the next clusters.
371
		$masterPositions = array_fill( 0, count( $lbs ), false );
372
		foreach ( $lbs as $i => $lb ) {
373
			if ( $lb->getServerCount() <= 1 ) {
374
				// Bug 27975 - Don't try to wait for slaves if there are none
375
				// Prevents permission error when getting master position
376
				continue;
377
			} elseif ( $opts['ifWritesSince']
378
				&& $lb->lastMasterChangeTimestamp() < $opts['ifWritesSince']
379
			) {
380
				continue; // no writes since the last wait
381
			}
382
			$masterPositions[$i] = $lb->getMasterPos();
383
		}
384
385
		$failed = [];
386
		foreach ( $lbs as $i => $lb ) {
387
			if ( $masterPositions[$i] ) {
388
				// The DBMS may not support getMasterPos() or the whole
389
				// load balancer might be fake (e.g. $wgAllDBsAreLocalhost).
390
				if ( !$lb->waitForAll( $masterPositions[$i], $opts['timeout'] ) ) {
391
					$failed[] = $lb->getServerName( $lb->getWriterIndex() );
392
				}
393
			}
394
		}
395
396
		if ( $failed ) {
397
			throw new DBReplicationWaitError(
398
				"Could not wait for slaves to catch up to " .
399
				implode( ', ', $failed )
400
			);
401
		}
402
	}
403
404
	/**
405
	 * Disable the ChronologyProtector for all load balancers
406
	 *
407
	 * This can be called at the start of special API entry points
408
	 *
409
	 * @since 1.27
410
	 */
411
	public function disableChronologyProtection() {
412
		$this->chronProt->setEnabled( false );
413
	}
414
415
	/**
416
	 * @return ChronologyProtector
417
	 */
418
	protected function newChronologyProtector() {
419
		$request = RequestContext::getMain()->getRequest();
420
		$chronProt = new ChronologyProtector(
421
			ObjectCache::getMainStashInstance(),
422
			[
423
				'ip' => $request->getIP(),
424
				'agent' => $request->getHeader( 'User-Agent' )
425
			]
426
		);
427
		if ( PHP_SAPI === 'cli' ) {
428
			$chronProt->setEnabled( false );
429
		} elseif ( $request->getHeader( 'ChronologyProtection' ) === 'false' ) {
430
			// Request opted out of using position wait logic. This is useful for requests
431
			// done by the job queue or background ETL that do not have a meaningful session.
432
			$chronProt->setWaitEnabled( false );
433
		}
434
435
		return $chronProt;
436
	}
437
438
	/**
439
	 * @param ChronologyProtector $cp
440
	 */
441
	protected function shutdownChronologyProtector( ChronologyProtector $cp ) {
442
		// Get all the master positions needed
443
		$this->forEachLB( function ( LoadBalancer $lb ) use ( $cp ) {
444
			$cp->shutdownLB( $lb );
445
		} );
446
		// Write them to the stash
447
		$unsavedPositions = $cp->shutdown();
448
		// If the positions failed to write to the stash, at least wait on local datacenter
449
		// slaves to catch up before responding. Even if there are several DCs, this increases
450
		// the chance that the user will see their own changes immediately afterwards. As long
451
		// as the sticky DC cookie applies (same domain), this is not even an issue.
452
		$this->forEachLB( function ( LoadBalancer $lb ) use ( $unsavedPositions ) {
453
			$masterName = $lb->getServerName( $lb->getWriterIndex() );
454
			if ( isset( $unsavedPositions[$masterName] ) ) {
455
				$lb->waitForAll( $unsavedPositions[$masterName] );
456
			}
457
		} );
458
	}
459
460
	/**
461
	 * Close all open database connections on all open load balancers.
462
	 * @since 1.28
463
	 */
464
	public function closeAll() {
465
		$this->forEachLBCallMethod( 'closeAll', [] );
466
	}
467
468
}
469
470
/**
471
 * Exception class for attempted DB access
472
 */
473
class DBAccessError extends MWException {
474
	public function __construct() {
475
		parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " .
476
			"This is not allowed, because database access has been disabled." );
477
	}
478
}
479
480
/**
481
 * Exception class for replica DB wait timeouts
482
 */
483
class DBReplicationWaitError extends Exception {
484
}
485