Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like LoadBalancer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use LoadBalancer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
30 | class LoadBalancer implements ILoadBalancer { |
||
31 | /** @var array[] Map of (server index => server config array) */ |
||
32 | private $mServers; |
||
33 | /** @var array[] Map of (local/foreignUsed/foreignFree => server index => IDatabase array) */ |
||
34 | private $mConns; |
||
35 | /** @var float[] 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 replica DB lag as a factor in replica DB selection */ |
||
40 | private $mAllowLagged; |
||
41 | /** @var integer Seconds to spend waiting on replica DB lag to resolve */ |
||
42 | private $mWaitTimeout; |
||
43 | /** @var array The LoadMonitor configuration */ |
||
44 | private $loadMonitorConfig; |
||
45 | /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */ |
||
46 | private $tableAliases = []; |
||
47 | |||
48 | /** @var ILoadMonitor */ |
||
49 | private $loadMonitor; |
||
50 | /** @var BagOStuff */ |
||
51 | private $srvCache; |
||
52 | /** @var BagOStuff */ |
||
53 | private $memCache; |
||
54 | /** @var WANObjectCache */ |
||
55 | private $wanCache; |
||
56 | /** @var object|string Class name or object With profileIn/profileOut methods */ |
||
57 | protected $profiler; |
||
58 | /** @var TransactionProfiler */ |
||
59 | protected $trxProfiler; |
||
60 | /** @var LoggerInterface */ |
||
61 | protected $replLogger; |
||
62 | /** @var LoggerInterface */ |
||
63 | protected $connLogger; |
||
64 | /** @var LoggerInterface */ |
||
65 | protected $queryLogger; |
||
66 | /** @var LoggerInterface */ |
||
67 | protected $perfLogger; |
||
68 | |||
69 | /** @var bool|IDatabase Database connection that caused a problem */ |
||
70 | private $mErrorConnection; |
||
71 | /** @var integer The generic (not query grouped) replica DB index (of $mServers) */ |
||
72 | private $mReadIndex; |
||
73 | /** @var bool|DBMasterPos False if not set */ |
||
74 | private $mWaitForPos; |
||
75 | /** @var bool Whether the generic reader fell back to a lagged replica DB */ |
||
76 | private $laggedReplicaMode = false; |
||
77 | /** @var bool Whether the generic reader fell back to a lagged replica DB */ |
||
78 | private $allReplicasDownMode = false; |
||
79 | /** @var string The last DB selection or connection error */ |
||
80 | private $mLastError = 'Unknown error'; |
||
81 | /** @var string|bool Reason the LB is read-only or false if not */ |
||
82 | private $readOnlyReason = false; |
||
83 | /** @var integer Total connections opened */ |
||
84 | private $connsOpened = 0; |
||
85 | /** @var string|bool String if a requested DBO_TRX transaction round is active */ |
||
86 | private $trxRoundId = false; |
||
87 | /** @var array[] Map of (name => callable) */ |
||
88 | private $trxRecurringCallbacks = []; |
||
89 | /** @var DatabaseDomain Local Domain ID and default for selectDB() calls */ |
||
90 | private $localDomain; |
||
91 | /** @var string Alternate ID string for the domain instead of DatabaseDomain::getId() */ |
||
92 | private $localDomainIdAlias; |
||
93 | /** @var string Current server name */ |
||
94 | private $host; |
||
95 | /** @var bool Whether this PHP instance is for a CLI script */ |
||
96 | protected $cliMode; |
||
97 | /** @var string Agent name for query profiling */ |
||
98 | protected $agent; |
||
99 | |||
100 | /** @var callable Exception logger */ |
||
101 | private $errorLogger; |
||
102 | |||
103 | /** @var boolean */ |
||
104 | private $disabled = false; |
||
105 | |||
106 | /** @var integer Warn when this many connection are held */ |
||
107 | const CONN_HELD_WARN_THRESHOLD = 10; |
||
108 | |||
109 | /** @var integer Default 'max lag' when unspecified */ |
||
110 | const MAX_LAG_DEFAULT = 10; |
||
111 | /** @var integer Seconds to cache master server read-only status */ |
||
112 | const TTL_CACHE_READONLY = 5; |
||
113 | |||
114 | public function __construct( array $params ) { |
||
115 | if ( !isset( $params['servers'] ) ) { |
||
116 | throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' ); |
||
117 | } |
||
118 | $this->mServers = $params['servers']; |
||
119 | |||
120 | $this->localDomain = isset( $params['localDomain'] ) |
||
121 | ? DatabaseDomain::newFromId( $params['localDomain'] ) |
||
122 | : DatabaseDomain::newUnspecified(); |
||
123 | // In case a caller assumes that the domain ID is simply <db>-<prefix>, which is almost |
||
124 | // always true, gracefully handle the case when they fail to account for escaping. |
||
125 | if ( $this->localDomain->getTablePrefix() != '' ) { |
||
126 | $this->localDomainIdAlias = |
||
127 | $this->localDomain->getDatabase() . '-' . $this->localDomain->getTablePrefix(); |
||
128 | } else { |
||
129 | $this->localDomainIdAlias = $this->localDomain->getDatabase(); |
||
130 | } |
||
131 | |||
132 | $this->mWaitTimeout = isset( $params['waitTimeout'] ) ? $params['waitTimeout'] : 10; |
||
133 | |||
134 | $this->mReadIndex = -1; |
||
135 | $this->mConns = [ |
||
|
|||
136 | 'local' => [], |
||
137 | 'foreignUsed' => [], |
||
138 | 'foreignFree' => [] |
||
139 | ]; |
||
140 | $this->mLoads = []; |
||
141 | $this->mWaitForPos = false; |
||
142 | $this->mErrorConnection = false; |
||
143 | $this->mAllowLagged = false; |
||
144 | |||
145 | View Code Duplication | if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) { |
|
146 | $this->readOnlyReason = $params['readOnlyReason']; |
||
147 | } |
||
148 | |||
149 | if ( isset( $params['loadMonitor'] ) ) { |
||
150 | $this->loadMonitorConfig = $params['loadMonitor']; |
||
151 | } else { |
||
152 | $this->loadMonitorConfig = [ 'class' => 'LoadMonitorNull' ]; |
||
153 | } |
||
154 | |||
155 | foreach ( $params['servers'] as $i => $server ) { |
||
156 | $this->mLoads[$i] = $server['load']; |
||
157 | if ( isset( $server['groupLoads'] ) ) { |
||
158 | foreach ( $server['groupLoads'] as $group => $ratio ) { |
||
159 | if ( !isset( $this->mGroupLoads[$group] ) ) { |
||
160 | $this->mGroupLoads[$group] = []; |
||
161 | } |
||
162 | $this->mGroupLoads[$group][$i] = $ratio; |
||
163 | } |
||
164 | } |
||
165 | } |
||
166 | |||
167 | if ( isset( $params['srvCache'] ) ) { |
||
168 | $this->srvCache = $params['srvCache']; |
||
169 | } else { |
||
170 | $this->srvCache = new EmptyBagOStuff(); |
||
171 | } |
||
172 | if ( isset( $params['memCache'] ) ) { |
||
173 | $this->memCache = $params['memCache']; |
||
174 | } else { |
||
175 | $this->memCache = new EmptyBagOStuff(); |
||
176 | } |
||
177 | if ( isset( $params['wanCache'] ) ) { |
||
178 | $this->wanCache = $params['wanCache']; |
||
179 | } else { |
||
180 | $this->wanCache = WANObjectCache::newEmpty(); |
||
181 | } |
||
182 | $this->profiler = isset( $params['profiler'] ) ? $params['profiler'] : null; |
||
183 | if ( isset( $params['trxProfiler'] ) ) { |
||
184 | $this->trxProfiler = $params['trxProfiler']; |
||
185 | } else { |
||
186 | $this->trxProfiler = new TransactionProfiler(); |
||
187 | } |
||
188 | |||
189 | $this->errorLogger = isset( $params['errorLogger'] ) |
||
190 | ? $params['errorLogger'] |
||
191 | : function ( Exception $e ) { |
||
192 | trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING ); |
||
193 | }; |
||
194 | |||
195 | foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) { |
||
196 | $this->$key = isset( $params[$key] ) ? $params[$key] : new \Psr\Log\NullLogger(); |
||
197 | } |
||
198 | |||
199 | $this->host = isset( $params['hostname'] ) |
||
200 | ? $params['hostname'] |
||
201 | : ( gethostname() ?: 'unknown' ); |
||
202 | $this->cliMode = isset( $params['cliMode'] ) ? $params['cliMode'] : PHP_SAPI === 'cli'; |
||
203 | $this->agent = isset( $params['agent'] ) ? $params['agent'] : ''; |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * Get a LoadMonitor instance |
||
208 | * |
||
209 | * @return ILoadMonitor |
||
210 | */ |
||
211 | private function getLoadMonitor() { |
||
212 | if ( !isset( $this->loadMonitor ) ) { |
||
213 | $class = $this->loadMonitorConfig['class']; |
||
214 | $this->loadMonitor = new $class( |
||
215 | $this, $this->srvCache, $this->memCache, $this->loadMonitorConfig ); |
||
216 | $this->loadMonitor->setLogger( $this->replLogger ); |
||
217 | } |
||
218 | |||
219 | return $this->loadMonitor; |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * @param array $loads |
||
224 | * @param bool|string $domain Domain to get non-lagged for |
||
225 | * @param int $maxLag Restrict the maximum allowed lag to this many seconds |
||
226 | * @return bool|int|string |
||
227 | */ |
||
228 | private function getRandomNonLagged( array $loads, $domain = false, $maxLag = INF ) { |
||
229 | $lags = $this->getLagTimes( $domain ); |
||
230 | |||
231 | # Unset excessively lagged servers |
||
232 | foreach ( $lags as $i => $lag ) { |
||
233 | if ( $i != 0 ) { |
||
234 | # How much lag this server nominally is allowed to have |
||
235 | $maxServerLag = isset( $this->mServers[$i]['max lag'] ) |
||
236 | ? $this->mServers[$i]['max lag'] |
||
237 | : self::MAX_LAG_DEFAULT; // default |
||
238 | # Constrain that futher by $maxLag argument |
||
239 | $maxServerLag = min( $maxServerLag, $maxLag ); |
||
240 | |||
241 | $host = $this->getServerName( $i ); |
||
242 | if ( $lag === false && !is_infinite( $maxServerLag ) ) { |
||
243 | $this->replLogger->error( "Server $host (#$i) is not replicating?" ); |
||
244 | unset( $loads[$i] ); |
||
245 | } elseif ( $lag > $maxServerLag ) { |
||
246 | $this->replLogger->warning( "Server $host (#$i) has >= $lag seconds of lag" ); |
||
247 | unset( $loads[$i] ); |
||
248 | } |
||
249 | } |
||
250 | } |
||
251 | |||
252 | # Find out if all the replica DBs with non-zero load are lagged |
||
253 | $sum = 0; |
||
254 | foreach ( $loads as $load ) { |
||
255 | $sum += $load; |
||
256 | } |
||
257 | if ( $sum == 0 ) { |
||
258 | # No appropriate DB servers except maybe the master and some replica DBs with zero load |
||
259 | # Do NOT use the master |
||
260 | # Instead, this function will return false, triggering read-only mode, |
||
261 | # and a lagged replica DB will be used instead. |
||
262 | return false; |
||
263 | } |
||
264 | |||
265 | if ( count( $loads ) == 0 ) { |
||
266 | return false; |
||
267 | } |
||
268 | |||
269 | # Return a random representative of the remainder |
||
270 | return ArrayUtils::pickRandom( $loads ); |
||
271 | } |
||
272 | |||
273 | public function getReaderIndex( $group = false, $domain = false ) { |
||
274 | if ( count( $this->mServers ) == 1 ) { |
||
275 | # Skip the load balancing if there's only one server |
||
276 | return $this->getWriterIndex(); |
||
277 | } elseif ( $group === false && $this->mReadIndex >= 0 ) { |
||
278 | # Shortcut if generic reader exists already |
||
279 | return $this->mReadIndex; |
||
280 | } |
||
281 | |||
282 | # Find the relevant load array |
||
283 | if ( $group !== false ) { |
||
284 | if ( isset( $this->mGroupLoads[$group] ) ) { |
||
285 | $nonErrorLoads = $this->mGroupLoads[$group]; |
||
286 | } else { |
||
287 | # No loads for this group, return false and the caller can use some other group |
||
288 | $this->connLogger->info( __METHOD__ . ": no loads for group $group" ); |
||
289 | |||
290 | return false; |
||
291 | } |
||
292 | } else { |
||
293 | $nonErrorLoads = $this->mLoads; |
||
294 | } |
||
295 | |||
296 | if ( !count( $nonErrorLoads ) ) { |
||
297 | throw new InvalidArgumentException( "Empty server array given to LoadBalancer" ); |
||
298 | } |
||
299 | |||
300 | # Scale the configured load ratios according to the dynamic load if supported |
||
301 | $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $domain ); |
||
302 | |||
303 | $laggedReplicaMode = false; |
||
304 | |||
305 | # No server found yet |
||
306 | $i = false; |
||
307 | # First try quickly looking through the available servers for a server that |
||
308 | # meets our criteria |
||
309 | $currentLoads = $nonErrorLoads; |
||
310 | while ( count( $currentLoads ) ) { |
||
311 | if ( $this->mAllowLagged || $laggedReplicaMode ) { |
||
312 | $i = ArrayUtils::pickRandom( $currentLoads ); |
||
313 | } else { |
||
314 | $i = false; |
||
315 | if ( $this->mWaitForPos && $this->mWaitForPos->asOfTime() ) { |
||
316 | # ChronologyProtecter causes mWaitForPos to be set via sessions. |
||
317 | # This triggers doWait() after connect, so it's especially good to |
||
318 | # avoid lagged servers so as to avoid just blocking in that method. |
||
319 | $ago = microtime( true ) - $this->mWaitForPos->asOfTime(); |
||
320 | # Aim for <= 1 second of waiting (being too picky can backfire) |
||
321 | $i = $this->getRandomNonLagged( $currentLoads, $domain, $ago + 1 ); |
||
322 | } |
||
323 | if ( $i === false ) { |
||
324 | # Any server with less lag than it's 'max lag' param is preferable |
||
325 | $i = $this->getRandomNonLagged( $currentLoads, $domain ); |
||
326 | } |
||
327 | if ( $i === false && count( $currentLoads ) != 0 ) { |
||
328 | # All replica DBs lagged. Switch to read-only mode |
||
329 | $this->replLogger->error( "All replica DBs lagged. Switch to read-only mode" ); |
||
330 | $i = ArrayUtils::pickRandom( $currentLoads ); |
||
331 | $laggedReplicaMode = true; |
||
332 | } |
||
333 | } |
||
334 | |||
335 | if ( $i === false ) { |
||
336 | # pickRandom() returned false |
||
337 | # This is permanent and means the configuration or the load monitor |
||
338 | # wants us to return false. |
||
339 | $this->connLogger->debug( __METHOD__ . ": pickRandom() returned false" ); |
||
340 | |||
341 | return false; |
||
342 | } |
||
343 | |||
344 | $serverName = $this->getServerName( $i ); |
||
345 | $this->connLogger->debug( __METHOD__ . ": Using reader #$i: $serverName..." ); |
||
346 | |||
347 | $conn = $this->openConnection( $i, $domain ); |
||
348 | if ( !$conn ) { |
||
349 | $this->connLogger->warning( __METHOD__ . ": Failed connecting to $i/$domain" ); |
||
350 | unset( $nonErrorLoads[$i] ); |
||
351 | unset( $currentLoads[$i] ); |
||
352 | $i = false; |
||
353 | continue; |
||
354 | } |
||
355 | |||
356 | // Decrement reference counter, we are finished with this connection. |
||
357 | // It will be incremented for the caller later. |
||
358 | if ( $domain !== false ) { |
||
359 | $this->reuseConnection( $conn ); |
||
360 | } |
||
361 | |||
362 | # Return this server |
||
363 | break; |
||
364 | } |
||
365 | |||
366 | # If all servers were down, quit now |
||
367 | if ( !count( $nonErrorLoads ) ) { |
||
368 | $this->connLogger->error( "All servers down" ); |
||
369 | } |
||
370 | |||
371 | if ( $i !== false ) { |
||
372 | # Replica DB connection successful. |
||
373 | # Wait for the session master pos for a short time. |
||
374 | if ( $this->mWaitForPos && $i > 0 ) { |
||
375 | $this->doWait( $i ); |
||
376 | } |
||
377 | if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group === false ) { |
||
378 | $this->mReadIndex = $i; |
||
379 | # Record if the generic reader index is in "lagged replica DB" mode |
||
380 | if ( $laggedReplicaMode ) { |
||
381 | $this->laggedReplicaMode = true; |
||
382 | } |
||
383 | } |
||
384 | $serverName = $this->getServerName( $i ); |
||
385 | $this->connLogger->debug( |
||
386 | __METHOD__ . ": using server $serverName for group '$group'" ); |
||
387 | } |
||
388 | |||
389 | return $i; |
||
390 | } |
||
391 | |||
392 | public function waitFor( $pos ) { |
||
393 | $this->mWaitForPos = $pos; |
||
394 | $i = $this->mReadIndex; |
||
395 | |||
396 | if ( $i > 0 ) { |
||
397 | if ( !$this->doWait( $i ) ) { |
||
398 | $this->laggedReplicaMode = true; |
||
399 | } |
||
400 | } |
||
401 | } |
||
402 | |||
403 | public function waitForOne( $pos, $timeout = null ) { |
||
404 | $this->mWaitForPos = $pos; |
||
405 | |||
406 | $i = $this->mReadIndex; |
||
407 | if ( $i <= 0 ) { |
||
408 | // Pick a generic replica DB if there isn't one yet |
||
409 | $readLoads = $this->mLoads; |
||
410 | unset( $readLoads[$this->getWriterIndex()] ); // replica DBs only |
||
411 | $readLoads = array_filter( $readLoads ); // with non-zero load |
||
412 | $i = ArrayUtils::pickRandom( $readLoads ); |
||
413 | } |
||
414 | |||
415 | View Code Duplication | if ( $i > 0 ) { |
|
416 | $ok = $this->doWait( $i, true, $timeout ); |
||
417 | } else { |
||
418 | $ok = true; // no applicable loads |
||
419 | } |
||
420 | |||
421 | return $ok; |
||
422 | } |
||
423 | |||
424 | public function waitForAll( $pos, $timeout = null ) { |
||
425 | $this->mWaitForPos = $pos; |
||
426 | $serverCount = count( $this->mServers ); |
||
427 | |||
428 | $ok = true; |
||
429 | for ( $i = 1; $i < $serverCount; $i++ ) { |
||
430 | View Code Duplication | if ( $this->mLoads[$i] > 0 ) { |
|
431 | $ok = $this->doWait( $i, true, $timeout ) && $ok; |
||
432 | } |
||
433 | } |
||
434 | |||
435 | return $ok; |
||
436 | } |
||
437 | |||
438 | public function getAnyOpenConnection( $i ) { |
||
439 | foreach ( $this->mConns as $connsByServer ) { |
||
440 | if ( !empty( $connsByServer[$i] ) ) { |
||
441 | return reset( $connsByServer[$i] ); |
||
442 | } |
||
443 | } |
||
444 | |||
445 | return false; |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Wait for a given replica DB to catch up to the master pos stored in $this |
||
450 | * @param int $index Server index |
||
451 | * @param bool $open Check the server even if a new connection has to be made |
||
452 | * @param int $timeout Max seconds to wait; default is mWaitTimeout |
||
453 | * @return bool |
||
454 | */ |
||
455 | protected function doWait( $index, $open = false, $timeout = null ) { |
||
456 | $close = false; // close the connection afterwards |
||
457 | |||
458 | // Check if we already know that the DB has reached this point |
||
459 | $server = $this->getServerName( $index ); |
||
460 | $key = $this->srvCache->makeGlobalKey( __CLASS__, 'last-known-pos', $server ); |
||
461 | /** @var DBMasterPos $knownReachedPos */ |
||
462 | $knownReachedPos = $this->srvCache->get( $key ); |
||
463 | if ( $knownReachedPos && $knownReachedPos->hasReached( $this->mWaitForPos ) ) { |
||
464 | $this->replLogger->debug( __METHOD__ . |
||
465 | ": replica DB $server known to be caught up (pos >= $knownReachedPos)." ); |
||
466 | return true; |
||
467 | } |
||
468 | |||
469 | // Find a connection to wait on, creating one if needed and allowed |
||
470 | $conn = $this->getAnyOpenConnection( $index ); |
||
471 | if ( !$conn ) { |
||
472 | if ( !$open ) { |
||
473 | $this->replLogger->debug( __METHOD__ . ": no connection open for $server" ); |
||
474 | |||
475 | return false; |
||
476 | } else { |
||
477 | $conn = $this->openConnection( $index, self::DOMAIN_ANY ); |
||
478 | if ( !$conn ) { |
||
479 | $this->replLogger->warning( __METHOD__ . ": failed to connect to $server" ); |
||
480 | |||
481 | return false; |
||
482 | } |
||
483 | // Avoid connection spam in waitForAll() when connections |
||
484 | // are made just for the sake of doing this lag check. |
||
485 | $close = true; |
||
486 | } |
||
487 | } |
||
488 | |||
489 | $this->replLogger->info( __METHOD__ . ": Waiting for replica DB $server to catch up..." ); |
||
490 | $timeout = $timeout ?: $this->mWaitTimeout; |
||
491 | $result = $conn->masterPosWait( $this->mWaitForPos, $timeout ); |
||
492 | |||
493 | if ( $result == -1 || is_null( $result ) ) { |
||
494 | // Timed out waiting for replica DB, use master instead |
||
495 | $msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}"; |
||
496 | $this->replLogger->warning( "$msg" ); |
||
497 | $ok = false; |
||
498 | } else { |
||
499 | $this->replLogger->info( __METHOD__ . ": Done" ); |
||
500 | $ok = true; |
||
501 | // Remember that the DB reached this point |
||
502 | $this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY ); |
||
503 | } |
||
504 | |||
505 | if ( $close ) { |
||
506 | $this->closeConnection( $conn ); |
||
507 | } |
||
508 | |||
509 | return $ok; |
||
510 | } |
||
511 | |||
512 | /** |
||
513 | * @see ILoadBalancer::getConnection() |
||
514 | * |
||
515 | * @param int $i |
||
516 | * @param array $groups |
||
517 | * @param bool $domain |
||
518 | * @return Database |
||
519 | * @throws DBConnectionError |
||
520 | */ |
||
521 | public function getConnection( $i, $groups = [], $domain = false ) { |
||
522 | if ( $i === null || $i === false ) { |
||
523 | throw new InvalidArgumentException( 'Attempt to call ' . __METHOD__ . |
||
524 | ' with invalid server index' ); |
||
525 | } |
||
526 | |||
527 | if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) { |
||
528 | $domain = false; // local connection requested |
||
529 | } |
||
530 | |||
531 | $groups = ( $groups === false || $groups === [] ) |
||
532 | ? [ false ] // check one "group": the generic pool |
||
533 | : (array)$groups; |
||
534 | |||
535 | $masterOnly = ( $i == self::DB_MASTER || $i == $this->getWriterIndex() ); |
||
536 | $oldConnsOpened = $this->connsOpened; // connections open now |
||
537 | |||
538 | if ( $i == self::DB_MASTER ) { |
||
539 | $i = $this->getWriterIndex(); |
||
540 | } else { |
||
541 | # Try to find an available server in any the query groups (in order) |
||
542 | foreach ( $groups as $group ) { |
||
543 | $groupIndex = $this->getReaderIndex( $group, $domain ); |
||
544 | if ( $groupIndex !== false ) { |
||
545 | $i = $groupIndex; |
||
546 | break; |
||
547 | } |
||
548 | } |
||
549 | } |
||
550 | |||
551 | # Operation-based index |
||
552 | if ( $i == self::DB_REPLICA ) { |
||
553 | $this->mLastError = 'Unknown error'; // reset error string |
||
554 | # Try the general server pool if $groups are unavailable. |
||
555 | $i = in_array( false, $groups, true ) |
||
556 | ? false // don't bother with this if that is what was tried above |
||
557 | : $this->getReaderIndex( false, $domain ); |
||
558 | # Couldn't find a working server in getReaderIndex()? |
||
559 | if ( $i === false ) { |
||
560 | $this->mLastError = 'No working replica DB server: ' . $this->mLastError; |
||
561 | // Throw an exception |
||
562 | $this->reportConnectionError(); |
||
563 | return null; // not reached |
||
564 | } |
||
565 | } |
||
566 | |||
567 | # Now we have an explicit index into the servers array |
||
568 | $conn = $this->openConnection( $i, $domain ); |
||
569 | if ( !$conn ) { |
||
570 | // Throw an exception |
||
571 | $this->reportConnectionError(); |
||
572 | return null; // not reached |
||
573 | } |
||
574 | |||
575 | # Profile any new connections that happen |
||
576 | if ( $this->connsOpened > $oldConnsOpened ) { |
||
577 | $host = $conn->getServer(); |
||
578 | $dbname = $conn->getDBname(); |
||
579 | $this->trxProfiler->recordConnection( $host, $dbname, $masterOnly ); |
||
580 | } |
||
581 | |||
582 | if ( $masterOnly ) { |
||
583 | # Make master-requested DB handles inherit any read-only mode setting |
||
584 | $conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $domain, $conn ) ); |
||
585 | } |
||
586 | |||
587 | return $conn; |
||
588 | } |
||
589 | |||
590 | public function reuseConnection( $conn ) { |
||
591 | $serverIndex = $conn->getLBInfo( 'serverIndex' ); |
||
592 | $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); |
||
593 | if ( $serverIndex === null || $refCount === null ) { |
||
594 | /** |
||
595 | * This can happen in code like: |
||
596 | * foreach ( $dbs as $db ) { |
||
597 | * $conn = $lb->getConnection( $lb::DB_REPLICA, [], $db ); |
||
598 | * ... |
||
599 | * $lb->reuseConnection( $conn ); |
||
600 | * } |
||
601 | * When a connection to the local DB is opened in this way, reuseConnection() |
||
602 | * should be ignored |
||
603 | */ |
||
604 | return; |
||
605 | } elseif ( $conn instanceof DBConnRef ) { |
||
606 | // DBConnRef already handles calling reuseConnection() and only passes the live |
||
607 | // Database instance to this method. Any caller passing in a DBConnRef is broken. |
||
608 | $this->connLogger->error( __METHOD__ . ": got DBConnRef instance.\n" . |
||
609 | ( new RuntimeException() )->getTraceAsString() ); |
||
610 | |||
611 | return; |
||
612 | } |
||
613 | |||
614 | if ( $this->disabled ) { |
||
615 | return; // DBConnRef handle probably survived longer than the LoadBalancer |
||
616 | } |
||
617 | |||
618 | $domain = $conn->getDomainID(); |
||
619 | if ( !isset( $this->mConns['foreignUsed'][$serverIndex][$domain] ) ) { |
||
620 | throw new InvalidArgumentException( __METHOD__ . |
||
621 | ": connection $serverIndex/$domain not found; it may have already been freed." ); |
||
622 | } elseif ( $this->mConns['foreignUsed'][$serverIndex][$domain] !== $conn ) { |
||
623 | throw new InvalidArgumentException( __METHOD__ . |
||
624 | ": connection $serverIndex/$domain mismatched; it may have already been freed." ); |
||
625 | } |
||
626 | $conn->setLBInfo( 'foreignPoolRefCount', --$refCount ); |
||
627 | if ( $refCount <= 0 ) { |
||
628 | $this->mConns['foreignFree'][$serverIndex][$domain] = $conn; |
||
629 | unset( $this->mConns['foreignUsed'][$serverIndex][$domain] ); |
||
630 | if ( !$this->mConns['foreignUsed'][$serverIndex] ) { |
||
631 | unset( $this->mConns[ 'foreignUsed' ][$serverIndex] ); // clean up |
||
632 | } |
||
633 | $this->connLogger->debug( __METHOD__ . ": freed connection $serverIndex/$domain" ); |
||
634 | } else { |
||
635 | $this->connLogger->debug( __METHOD__ . |
||
636 | ": reference count for $serverIndex/$domain reduced to $refCount" ); |
||
637 | } |
||
638 | } |
||
639 | |||
640 | public function getConnectionRef( $db, $groups = [], $domain = false ) { |
||
641 | $domain = ( $domain !== false ) ? $domain : $this->localDomain; |
||
642 | |||
643 | return new DBConnRef( $this, $this->getConnection( $db, $groups, $domain ) ); |
||
644 | } |
||
645 | |||
646 | public function getLazyConnectionRef( $db, $groups = [], $domain = false ) { |
||
651 | |||
652 | /** |
||
653 | * @see ILoadBalancer::openConnection() |
||
654 | * |
||
655 | * @param int $i |
||
656 | * @param bool $domain |
||
657 | * @return bool|Database |
||
658 | * @throws DBAccessError |
||
659 | */ |
||
660 | public function openConnection( $i, $domain = false ) { |
||
661 | if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) { |
||
662 | $domain = false; // local connection requested |
||
663 | } |
||
699 | |||
700 | /** |
||
701 | * Open a connection to a foreign DB, or return one if it is already open. |
||
702 | * |
||
703 | * Increments a reference count on the returned connection which locks the |
||
704 | * connection to the requested domain. This reference count can be |
||
705 | * decremented by calling reuseConnection(). |
||
706 | * |
||
707 | * If a connection is open to the appropriate server already, but with the wrong |
||
708 | * database, it will be switched to the right database and returned, as long as |
||
709 | * it has been freed first with reuseConnection(). |
||
710 | * |
||
711 | * On error, returns false, and the connection which caused the |
||
712 | * error will be available via $this->mErrorConnection. |
||
713 | * |
||
714 | * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError. |
||
715 | * |
||
716 | * @param int $i Server index |
||
717 | * @param string $domain Domain ID to open |
||
718 | * @return Database |
||
719 | */ |
||
720 | private function openForeignConnection( $i, $domain ) { |
||
782 | |||
783 | /** |
||
784 | * Test if the specified index represents an open connection |
||
785 | * |
||
786 | * @param int $index Server index |
||
787 | * @access private |
||
788 | * @return bool |
||
789 | */ |
||
790 | private function isOpen( $index ) { |
||
797 | |||
798 | /** |
||
799 | * Really opens a connection. Uncached. |
||
800 | * Returns a Database object whether or not the connection was successful. |
||
801 | * @access private |
||
802 | * |
||
803 | * @param array $server |
||
804 | * @param string|bool $dbNameOverride Use "" to not select any database |
||
805 | * @return Database |
||
806 | * @throws DBAccessError |
||
807 | * @throws InvalidArgumentException |
||
808 | */ |
||
809 | protected function reallyOpenConnection( array $server, $dbNameOverride = false ) { |
||
868 | |||
869 | /** |
||
870 | * @throws DBConnectionError |
||
871 | */ |
||
872 | private function reportConnectionError() { |
||
899 | |||
900 | public function getWriterIndex() { |
||
903 | |||
904 | public function haveIndex( $i ) { |
||
907 | |||
908 | public function isNonZeroLoad( $i ) { |
||
911 | |||
912 | public function getServerCount() { |
||
915 | |||
916 | public function getServerName( $i ) { |
||
927 | |||
928 | public function getServerInfo( $i ) { |
||
935 | |||
936 | public function setServerInfo( $i, array $serverInfo ) { |
||
939 | |||
940 | public function getMasterPos() { |
||
958 | |||
959 | public function disable() { |
||
963 | |||
964 | public function closeAll() { |
||
978 | |||
979 | public function closeConnection( IDatabase $conn ) { |
||
999 | |||
1000 | public function commitAll( $fname = __METHOD__ ) { |
||
1026 | |||
1027 | public function finalizeMasterChanges() { |
||
1036 | |||
1037 | public function approveMasterChanges( array $options ) { |
||
1069 | |||
1070 | public function beginMasterChanges( $fname = __METHOD__ ) { |
||
1101 | |||
1102 | public function commitMasterChanges( $fname = __METHOD__ ) { |
||
1135 | |||
1136 | public function runMasterPostTrxCallbacks( $type ) { |
||
1168 | |||
1169 | public function rollbackMasterChanges( $fname = __METHOD__ ) { |
||
1183 | |||
1184 | public function suppressTransactionEndCallbacks() { |
||
1189 | |||
1190 | /** |
||
1191 | * @param IDatabase $conn |
||
1192 | */ |
||
1193 | private function applyTransactionRoundFlags( IDatabase $conn ) { |
||
1203 | |||
1204 | /** |
||
1205 | * @param IDatabase $conn |
||
1206 | */ |
||
1207 | private function undoTransactionRoundFlags( IDatabase $conn ) { |
||
1212 | |||
1213 | public function flushReplicaSnapshots( $fname = __METHOD__ ) { |
||
1218 | |||
1219 | public function hasMasterConnection() { |
||
1222 | |||
1223 | public function hasMasterChanges() { |
||
1231 | |||
1232 | public function lastMasterChangeTimestamp() { |
||
1240 | |||
1241 | public function hasOrMadeRecentMasterChanges( $age = null ) { |
||
1247 | |||
1248 | public function pendingMasterChangeCallers() { |
||
1256 | |||
1257 | public function getLaggedReplicaMode( $domain = false ) { |
||
1273 | |||
1274 | /** |
||
1275 | * @param bool $domain |
||
1276 | * @return bool |
||
1277 | * @deprecated 1.28; use getLaggedReplicaMode() |
||
1278 | */ |
||
1279 | public function getLaggedSlaveMode( $domain = false ) { |
||
1282 | |||
1283 | public function laggedReplicaUsed() { |
||
1286 | |||
1287 | /** |
||
1288 | * @return bool |
||
1289 | * @since 1.27 |
||
1290 | * @deprecated Since 1.28; use laggedReplicaUsed() |
||
1291 | */ |
||
1292 | public function laggedSlaveUsed() { |
||
1295 | |||
1296 | public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) { |
||
1313 | |||
1314 | /** |
||
1315 | * @param string $domain Domain ID, or false for the current domain |
||
1316 | * @param IDatabase|null DB master connectionl used to avoid loops [optional] |
||
1317 | * @return bool |
||
1318 | */ |
||
1319 | private function masterRunningReadOnly( $domain, IDatabase $conn = null ) { |
||
1343 | |||
1344 | public function allowLagged( $mode = null ) { |
||
1352 | |||
1353 | public function pingAll() { |
||
1363 | |||
1364 | View Code Duplication | public function forEachOpenConnection( $callback, array $params = [] ) { |
|
1374 | |||
1375 | public function forEachOpenMasterConnection( $callback, array $params = [] ) { |
||
1387 | |||
1388 | View Code Duplication | public function forEachOpenReplicaConnection( $callback, array $params = [] ) { |
|
1401 | |||
1402 | public function getMaxLag( $domain = false ) { |
||
1422 | |||
1423 | public function getLagTimes( $domain = false ) { |
||
1440 | |||
1441 | public function safeGetLag( IDatabase $conn ) { |
||
1448 | |||
1449 | public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) { |
||
1483 | |||
1484 | public function setTransactionListener( $name, callable $callback = null ) { |
||
1496 | |||
1497 | public function setTableAliases( array $aliases ) { |
||
1500 | |||
1501 | public function setDomainPrefix( $prefix ) { |
||
1523 | |||
1524 | /** |
||
1525 | * Make PHP ignore user aborts/disconnects until the returned |
||
1526 | * value leaves scope. This returns null and does nothing in CLI mode. |
||
1527 | * |
||
1528 | * @return ScopedCallback|null |
||
1529 | */ |
||
1530 | View Code Duplication | final protected function getScopedPHPBehaviorForCommit() { |
|
1540 | |||
1541 | function __destruct() { |
||
1545 | } |
||
1546 |
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..