Completed
Branch master (227f0c)
by
unknown
30:54
created

LBFactory::closeAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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