Completed
Branch master (8ab5d1)
by
unknown
31:47
created

LoadMonitor::getServerStates()   F

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

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

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

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