rdbms: assorted LBFactoryMulti/LBFactorySimple cleanups
[lhc/web/wiklou.git] / includes / libs / rdbms / lbfactory / LBFactoryMulti.php
1 <?php
2 /**
3 * Advanced generator of database load balancing objects for database farms.
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 namespace Wikimedia\Rdbms;
25
26 use InvalidArgumentException;
27 use UnexpectedValueException;
28
29 /**
30 * A multi-database, multi-master factory for Wikimedia and similar installations.
31 * Ignores the old configuration globals.
32 *
33 * @ingroup Database
34 */
35 class LBFactoryMulti extends LBFactory {
36 /** @var LoadBalancer[] */
37 private $mainLBs = [];
38 /** @var LoadBalancer[] */
39 private $externalLBs = [];
40
41 /** @var string[] Map of (hostname => IP address) */
42 private $hostsByName = [];
43 /** @var string[] Map of (database name => section name) */
44 private $sectionsByDB = [];
45 /** @var int[][][] Map of (section => group => host => load ratio) */
46 private $groupLoadsBySection = [];
47 /** @var int[][][] Map of (database => group => host => load ratio) */
48 private $groupLoadsByDB = [];
49 /** @var int[][] Map of (cluster => host => load ratio) */
50 private $externalLoads = [];
51 /** @var array Server config map ("host", "hostName", "load", and "groupLoads" are ignored) */
52 private $serverTemplate = [];
53 /** @var array Server config map overriding "serverTemplate" for external storage */
54 private $externalTemplateOverrides = [];
55 /** @var array[] Map of (section => server config map overrides) */
56 private $templateOverridesBySection = [];
57 /** @var array[] Map of (cluster => server config map overrides) for external storage */
58 private $templateOverridesByCluster = [];
59 /** @var array Server config override map for all main and external master servers */
60 private $masterTemplateOverrides = [];
61 /** @var array[] Map of (host => server config map overrides) for main and external servers */
62 private $templateOverridesByServer = [];
63 /** @var string[]|bool[] A map of section name to read-only message */
64 private $readOnlyBySection = [];
65
66 /** @var string An ILoadMonitor class */
67 private $loadMonitorClass;
68
69 /** @var string */
70 private $lastDomain;
71 /** @var string */
72 private $lastSection;
73
74 /**
75 * Template override precedence (highest => lowest):
76 * - templateOverridesByServer
77 * - masterTemplateOverrides
78 * - templateOverridesBySection/templateOverridesByCluster
79 * - externalTemplateOverrides
80 * - serverTemplate
81 * Overrides only work on top level keys (so nested values will not be merged).
82 *
83 * Server config maps should be of the format Database::factory() requires.
84 * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
85 * data can be before the load balancer tries to avoid using it. The map can have 'is static'
86 * set to disable blocking replication sync checks (intended for archive servers with
87 * unchanging data).
88
89 * @see LBFactory::__construct()
90 * @param array $conf Additional parameters include:
91 * - hostsByName Optional (hostname => IP address) map.
92 * - sectionsByDB Optional map of (database => section name).
93 * For example:
94 * [
95 * 'DEFAULT' => 'section1',
96 * 'database1' => 'section2'
97 * ]
98 * - sectionLoads Optional map of (section => host => load ratio); the first
99 * host in each section is the master server for that section.
100 * For example:
101 * [
102 * 'dbmaser' => 0,
103 * 'dbreplica1' => 100,
104 * 'dbreplica2' => 100
105 * ]
106 * - groupLoadsBySection Optional map of (section => group => host => load ratio);
107 * any ILoadBalancer::GROUP_GENERIC group will be ignored.
108 * For example:
109 * [
110 * 'section1' => [
111 * 'group1' => [
112 * 'dbreplica3 => 100,
113 * 'dbreplica4' => 100
114 * ]
115 * ]
116 * ]
117 * - groupLoadsByDB Optional (database => group => host => load ratio) map.
118 * - externalLoads Optional (cluster => host => load ratio) map.
119 * - serverTemplate server config map for Database::factory().
120 * Note that "host", "hostName" and "load" entries will be
121 * overridden by "groupLoadsBySection" and "hostsByName".
122 * - externalTemplateOverrides Optional server config map overrides for external
123 * stores; respects the override precedence described above.
124 * - templateOverridesBySection Optional (section => server config map overrides) map;
125 * respects the override precedence described above.
126 * - templateOverridesByCluster Optional (external cluster => server config map overrides)
127 * map; respects the override precedence described above.
128 * - masterTemplateOverrides Optional server config map overrides for masters;
129 * respects the override precedence described above.
130 * - templateOverridesByServer Optional (host => server config map overrides) map;
131 * respects the override precedence described above
132 * and applies to both core and external storage.
133 * - loadMonitorClass Name of the LoadMonitor class to always use. [optional]
134 * - readOnlyBySection Optional map of (section name => message text or false).
135 * String values make sections read only, whereas anything
136 * else does not restrict read/write mode.
137 */
138 public function __construct( array $conf ) {
139 parent::__construct( $conf );
140
141 $this->hostsByName = $conf['hostsByName'] ?? [];
142 $this->sectionsByDB = $conf['sectionsByDB'];
143 $this->groupLoadsBySection = $conf['groupLoadsBySection'] ?? [];
144 foreach ( ( $conf['sectionLoads'] ?? [] ) as $section => $loadByHost ) {
145 $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] = $loadByHost;
146 }
147 $this->groupLoadsByDB = $conf['groupLoadsByDB'] ?? [];
148 $this->externalLoads = $conf['externalLoads'] ?? [];
149 $this->serverTemplate = $conf['serverTemplate'] ?? [];
150 $this->externalTemplateOverrides = $conf['externalTemplateOverrides'] ?? [];
151 $this->templateOverridesBySection = $conf['templateOverridesBySection'] ?? [];
152 $this->templateOverridesByCluster = $conf['templateOverridesByCluster'] ?? [];
153 $this->masterTemplateOverrides = $conf['masterTemplateOverrides'] ?? [];
154 $this->templateOverridesByServer = $conf['templateOverridesByServer'] ?? [];
155 $this->readOnlyBySection = $conf['readOnlyBySection'] ?? [];
156
157 $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
158 }
159
160 public function newMainLB( $domain = false ) {
161 $section = $this->getSectionForDomain( $domain );
162 if ( !isset( $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] ) ) {
163 throw new UnexpectedValueException( "Section '$section' has no hosts defined." );
164 }
165
166 $dbGroupLoads = $this->groupLoadsByDB[$this->getDomainDatabase( $domain )] ?? [];
167 unset( $dbGroupLoads[ILoadBalancer::GROUP_GENERIC] ); // cannot override
168
169 return $this->newLoadBalancer(
170 array_merge(
171 $this->serverTemplate,
172 $this->templateOverridesBySection[$section] ?? []
173 ),
174 array_merge( $this->groupLoadsBySection[$section], $dbGroupLoads ),
175 // Use the LB-specific read-only reason if everything isn't already read-only
176 is_string( $this->readOnlyReason )
177 ? $this->readOnlyReason
178 : ( $this->readOnlyBySection[$section] ?? false )
179 );
180 }
181
182 public function getMainLB( $domain = false ) {
183 $section = $this->getSectionForDomain( $domain );
184
185 if ( !isset( $this->mainLBs[$section] ) ) {
186 $this->mainLBs[$section] = $this->newMainLB( $domain );
187 }
188
189 return $this->mainLBs[$section];
190 }
191
192 public function newExternalLB( $cluster ) {
193 if ( !isset( $this->externalLoads[$cluster] ) ) {
194 throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
195 }
196
197 return $this->newLoadBalancer(
198 array_merge(
199 $this->serverTemplate,
200 $this->externalTemplateOverrides,
201 $this->templateOverridesByCluster[$cluster] ?? []
202 ),
203 [ ILoadBalancer::GROUP_GENERIC => $this->externalLoads[$cluster] ],
204 $this->readOnlyReason
205 );
206 }
207
208 public function getExternalLB( $cluster ) {
209 if ( !isset( $this->externalLBs[$cluster] ) ) {
210 $this->externalLBs[$cluster] = $this->newExternalLB( $cluster );
211 }
212
213 return $this->externalLBs[$cluster];
214 }
215
216 public function getAllMainLBs() {
217 $lbs = [];
218 foreach ( $this->sectionsByDB as $db => $section ) {
219 if ( !isset( $lbs[$section] ) ) {
220 $lbs[$section] = $this->getMainLB( $db );
221 }
222 }
223
224 return $lbs;
225 }
226
227 public function getAllExternalLBs() {
228 $lbs = [];
229 foreach ( $this->externalLoads as $cluster => $unused ) {
230 $lbs[$cluster] = $this->getExternalLB( $cluster );
231 }
232
233 return $lbs;
234 }
235
236 public function forEachLB( $callback, array $params = [] ) {
237 foreach ( $this->mainLBs as $lb ) {
238 $callback( $lb, ...$params );
239 }
240 foreach ( $this->externalLBs as $lb ) {
241 $callback( $lb, ...$params );
242 }
243 }
244
245 /**
246 * @param bool|string $domain
247 * @return string
248 */
249 private function getSectionForDomain( $domain = false ) {
250 if ( $this->lastDomain === $domain ) {
251 return $this->lastSection;
252 }
253
254 $database = $this->getDomainDatabase( $domain );
255 $section = $this->sectionsByDB[$database] ?? self::CLUSTER_MAIN_DEFAULT;
256 $this->lastSection = $section;
257 $this->lastDomain = $domain;
258
259 return $section;
260 }
261
262 /**
263 * Make a new load balancer object based on template and load array
264 *
265 * @param array $serverTemplate Server config map
266 * @param int[][] $groupLoads Map of (group => host => load)
267 * @param string|bool $readOnlyReason
268 * @return LoadBalancer
269 */
270 private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason ) {
271 $lb = new LoadBalancer( array_merge(
272 $this->baseLoadBalancerParams(),
273 [
274 'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ),
275 'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
276 'readOnlyReason' => $readOnlyReason
277 ]
278 ) );
279 $this->initLoadBalancer( $lb );
280
281 return $lb;
282 }
283
284 /**
285 * Make a server array as expected by LoadBalancer::__construct()
286 *
287 * @param array $serverTemplate Server config map
288 * @param int[][] $groupLoads Map of (group => host => load)
289 * @return array[] List of server config maps
290 */
291 private function makeServerArray( array $serverTemplate, array $groupLoads ) {
292 // The master server is the first host explicitly listed in the generic load group
293 if ( !$groupLoads[ILoadBalancer::GROUP_GENERIC] ) {
294 throw new UnexpectedValueException( "Empty generic load array; no master defined." );
295 }
296
297 $groupLoadsByHost = $this->reindexGroupLoads( $groupLoads );
298 // Get the ordered map of (host => load); the master server is first
299 $genericLoads = $groupLoads[ILoadBalancer::GROUP_GENERIC];
300 // Implictly append any hosts that only appear in custom load groups
301 $genericLoads += array_fill_keys( array_keys( $groupLoadsByHost ), 0 );
302
303 $servers = [];
304 foreach ( $genericLoads as $host => $load ) {
305 $servers[] = array_merge(
306 $serverTemplate,
307 $servers ? [] : $this->masterTemplateOverrides,
308 $this->templateOverridesByServer[$host] ?? [],
309 [
310 'host' => $this->hostsByName[$host] ?? $host,
311 'hostName' => $host,
312 'load' => $load,
313 'groupLoads' => $groupLoadsByHost[$host] ?? []
314 ]
315 );
316 }
317
318 return $servers;
319 }
320
321 /**
322 * Take a group load array indexed by group then server, and reindex it by server then group
323 * @param int[][] $groupLoads Map of (group => host => load)
324 * @return int[][] Map of (host => group => load)
325 */
326 private function reindexGroupLoads( array $groupLoads ) {
327 $reindexed = [];
328
329 foreach ( $groupLoads as $group => $loadByHost ) {
330 foreach ( $loadByHost as $host => $load ) {
331 $reindexed[$host][$group] = $load;
332 }
333 }
334
335 return $reindexed;
336 }
337
338 /**
339 * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
340 * @return string
341 */
342 private function getDomainDatabase( $domain = false ) {
343 return ( $domain === false )
344 ? $this->localDomain->getDatabase()
345 : DatabaseDomain::newFromId( $domain )->getDatabase();
346 }
347 }