LoadMonitor::getServerStates()   F
last analyzed

Complexity

Conditions 20
Paths 248

Size

Total Lines 112
Code Lines 68

Duplication

Lines 10
Ratio 8.93 %

Importance

Changes 0
Metric Value
cc 20
eloc 68
nc 248
nop 2
dl 10
loc 112
rs 3.6664
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 * @ingroup Database
20
 */
21
22
use Psr\Log\LoggerInterface;
23
use Wikimedia\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
24
25
/**
26
 * Basic DB load monitor with no external dependencies
27
 * Uses memcached to cache the replication lag for a short time
28
 *
29
 * @ingroup Database
30
 */
31
class LoadMonitor implements ILoadMonitor {
32
	/** @var ILoadBalancer */
33
	protected $parent;
34
	/** @var BagOStuff */
35
	protected $srvCache;
36
	/** @var BagOStuff */
37
	protected $mainCache;
38
	/** @var LoggerInterface */
39
	protected $replLogger;
40
41
	/** @var float Moving average ratio (e.g. 0.1 for 10% weight to new weight) */
42
	private $movingAveRatio;
43
44
	const VERSION = 1; // cache key version
45
46
	public function __construct(
47
		ILoadBalancer $lb, BagOStuff $srvCache, BagOStuff $cache, array $options = []
48
	) {
49
		$this->parent = $lb;
50
		$this->srvCache = $srvCache;
51
		$this->mainCache = $cache;
52
		$this->replLogger = new \Psr\Log\NullLogger();
53
54
		$this->movingAveRatio = isset( $options['movingAveRatio'] )
55
			? $options['movingAveRatio']
56
			: 0.1;
57
	}
58
59
	public function setLogger( LoggerInterface $logger ) {
60
		$this->replLogger = $logger;
61
	}
62
63
	public function scaleLoads( array &$weightByServer, $domain ) {
64
		$serverIndexes = array_keys( $weightByServer );
65
		$states = $this->getServerStates( $serverIndexes, $domain );
66
		$coefficientsByServer = $states['weightScales'];
67
		foreach ( $weightByServer as $i => $weight ) {
68
			if ( isset( $coefficientsByServer[$i] ) ) {
69
				$weightByServer[$i] = $weight * $coefficientsByServer[$i];
70
			} else { // server recently added to config?
71
				$host = $this->parent->getServerName( $i );
72
				$this->replLogger->error( __METHOD__ . ": host $host not in cache" );
73
			}
74
		}
75
	}
76
77
	public function getLagTimes( array $serverIndexes, $domain ) {
78
		$states = $this->getServerStates( $serverIndexes, $domain );
79
80
		return $states['lagTimes'];
81
	}
82
83
	protected function getServerStates( array $serverIndexes, $domain ) {
84
		$writerIndex = $this->parent->getWriterIndex();
85
		if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == $writerIndex ) {
86
			# Single server only, just return zero without caching
87
			return [
88
				'lagTimes' => [ $writerIndex => 0 ],
89
				'weightScales' => [ $writerIndex => 1.0 ]
90
			];
91
		}
92
93
		$key = $this->getCacheKey( $serverIndexes );
94
		# Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
95
		$ttl = mt_rand( 4e6, 5e6 ) / 1e6;
96
		# Keep keys around longer as fallbacks
97
		$staleTTL = 60;
98
99
		# (a) Check the local APC cache
100
		$value = $this->srvCache->get( $key );
101
		if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
102
			$this->replLogger->debug( __METHOD__ . ": got lag times ($key) from local cache" );
103
			return $value; // cache hit
104
		}
105
		$staleValue = $value ?: false;
106
107
		# (b) Check the shared cache and backfill APC
108
		$value = $this->mainCache->get( $key );
109
		if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
110
			$this->srvCache->set( $key, $value, $staleTTL );
111
			$this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
112
113
			return $value; // cache hit
114
		}
115
		$staleValue = $value ?: $staleValue;
116
117
		# (c) Cache key missing or expired; regenerate and backfill
118
		if ( $this->mainCache->lock( $key, 0, 10 ) ) {
119
			# Let this process alone update the cache value
120
			$cache = $this->mainCache;
121
			/** @noinspection PhpUnusedLocalVariableInspection */
122
			$unlocker = new ScopedCallback( function () use ( $cache, $key ) {
0 ignored issues
show
Unused Code introduced by
$unlocker is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
123
				$cache->unlock( $key );
124
			} );
125
		} elseif ( $staleValue ) {
126
			# Could not acquire lock but an old cache exists, so use it
127
			return $staleValue;
128
		}
129
130
		$lagTimes = [];
131
		$weightScales = [];
132
		$movAveRatio = $this->movingAveRatio;
133
		foreach ( $serverIndexes as $i ) {
134
			if ( $i == $this->parent->getWriterIndex() ) {
135
				$lagTimes[$i] = 0; // master always has no lag
136
				$weightScales[$i] = 1.0; // nominal weight
137
				continue;
138
			}
139
140
			$conn = $this->parent->getAnyOpenConnection( $i );
141
			if ( $conn ) {
142
				$close = false; // already open
143
			} else {
144
				$conn = $this->parent->openConnection( $i, $domain );
145
				$close = true; // new connection
146
			}
147
148
			$lastWeight = isset( $staleValue['weightScales'][$i] )
149
				? $staleValue['weightScales'][$i]
150
				: 1.0;
151
			$coefficient = $this->getWeightScale( $i, $conn ?: null );
0 ignored issues
show
Bug introduced by
It seems like $conn ?: null can also be of type boolean; however, LoadMonitor::getWeightScale() does only seem to accept null|object<IDatabase>, 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...
152
			$newWeight = $movAveRatio * $coefficient + ( 1 - $movAveRatio ) * $lastWeight;
153
154
			// Scale from 10% to 100% of nominal weight
155
			$weightScales[$i] = max( $newWeight, .10 );
156
157 View Code Duplication
			if ( !$conn ) {
158
				$lagTimes[$i] = false;
159
				$host = $this->parent->getServerName( $i );
160
				$this->replLogger->error( __METHOD__ . ": host $host is unreachable" );
161
				continue;
162
			}
163
164
			if ( $conn->getLBInfo( 'is static' ) ) {
165
				$lagTimes[$i] = 0;
166
			} else {
167
				$lagTimes[$i] = $conn->getLag();
168 View Code Duplication
				if ( $lagTimes[$i] === false ) {
169
					$host = $this->parent->getServerName( $i );
170
					$this->replLogger->error( __METHOD__ . ": host $host is not replicating?" );
171
				}
172
			}
173
174
			if ( $close ) {
175
				# Close the connection to avoid sleeper connections piling up.
176
				# Note that the caller will pick one of these DBs and reconnect,
177
				# which is slightly inefficient, but this only matters for the lag
178
				# time cache miss cache, which is far less common that cache hits.
179
				$this->parent->closeConnection( $conn );
0 ignored issues
show
Bug introduced by
It seems like $conn can also be of type boolean; however, ILoadBalancer::closeConnection() does only seem to accept object<IDatabase>, 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...
180
			}
181
		}
182
183
		# Add a timestamp key so we know when it was cached
184
		$value = [
185
			'lagTimes' => $lagTimes,
186
			'weightScales' => $weightScales,
187
			'timestamp' => microtime( true )
188
		];
189
		$this->mainCache->set( $key, $value, $staleTTL );
190
		$this->srvCache->set( $key, $value, $staleTTL );
191
		$this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
192
193
		return $value;
194
	}
195
196
	/**
197
	 * @param integer $index Server index
198
	 * @param IDatabase|null $conn Connection handle or null on connection failure
199
	 * @return float
200
	 */
201
	protected function getWeightScale( $index, IDatabase $conn = null ) {
202
		return $conn ? 1.0 : 0.0;
203
	}
204
205
	private function getCacheKey( array $serverIndexes ) {
206
		sort( $serverIndexes );
207
		// Lag is per-server, not per-DB, so key on the master DB name
208
		return $this->srvCache->makeGlobalKey(
209
			'lag-times',
210
			self::VERSION,
211
			$this->parent->getServerName( $this->parent->getWriterIndex() ),
212
			implode( '-', $serverIndexes )
213
		);
214
	}
215
}
216