Merge "StringUtils: Add a utility for checking if a string is a valid regex"
[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, $owner = null ) {
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 $owner
180 );
181 }
182
183 public function getMainLB( $domain = false ) {
184 $section = $this->getSectionForDomain( $domain );
185
186 if ( !isset( $this->mainLBs[$section] ) ) {
187 $this->mainLBs[$section] = $this->newMainLB( $domain, $this->getOwnershipId() );
188 }
189
190 return $this->mainLBs[$section];
191 }
192
193 public function newExternalLB( $cluster, $owner = null ) {
194 if ( !isset( $this->externalLoads[$cluster] ) ) {
195 throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
196 }
197
198 return $this->newLoadBalancer(
199 array_merge(
200 $this->serverTemplate,
201 $this->externalTemplateOverrides,
202 $this->templateOverridesByCluster[$cluster] ?? []
203 ),
204 [ ILoadBalancer::GROUP_GENERIC => $this->externalLoads[$cluster] ],
205 $this->readOnlyReason,
206 $owner
207 );
208 }
209
210 public function getExternalLB( $cluster ) {
211 if ( !isset( $this->externalLBs[$cluster] ) ) {
212 $this->externalLBs[$cluster] =
213 $this->newExternalLB( $cluster, $this->getOwnershipId() );
214 }
215
216 return $this->externalLBs[$cluster];
217 }
218
219 public function getAllMainLBs() {
220 $lbs = [];
221 foreach ( $this->sectionsByDB as $db => $section ) {
222 if ( !isset( $lbs[$section] ) ) {
223 $lbs[$section] = $this->getMainLB( $db );
224 }
225 }
226
227 return $lbs;
228 }
229
230 public function getAllExternalLBs() {
231 $lbs = [];
232 foreach ( $this->externalLoads as $cluster => $unused ) {
233 $lbs[$cluster] = $this->getExternalLB( $cluster );
234 }
235
236 return $lbs;
237 }
238
239 public function forEachLB( $callback, array $params = [] ) {
240 foreach ( $this->mainLBs as $lb ) {
241 $callback( $lb, ...$params );
242 }
243 foreach ( $this->externalLBs as $lb ) {
244 $callback( $lb, ...$params );
245 }
246 }
247
248 /**
249 * @param bool|string $domain
250 * @return string
251 */
252 private function getSectionForDomain( $domain = false ) {
253 if ( $this->lastDomain === $domain ) {
254 return $this->lastSection;
255 }
256
257 $database = $this->getDomainDatabase( $domain );
258 $section = $this->sectionsByDB[$database] ?? self::CLUSTER_MAIN_DEFAULT;
259 $this->lastSection = $section;
260 $this->lastDomain = $domain;
261
262 return $section;
263 }
264
265 /**
266 * Make a new load balancer object based on template and load array
267 *
268 * @param array $serverTemplate Server config map
269 * @param int[][] $groupLoads Map of (group => host => load)
270 * @param string|bool $readOnlyReason
271 * @param int|null $owner
272 * @return LoadBalancer
273 */
274 private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason, $owner ) {
275 $lb = new LoadBalancer( array_merge(
276 $this->baseLoadBalancerParams( $owner ),
277 [
278 'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ),
279 'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
280 'readOnlyReason' => $readOnlyReason
281 ]
282 ) );
283 $this->initLoadBalancer( $lb );
284
285 return $lb;
286 }
287
288 /**
289 * Make a server array as expected by LoadBalancer::__construct()
290 *
291 * @param array $serverTemplate Server config map
292 * @param int[][] $groupLoads Map of (group => host => load)
293 * @return array[] List of server config maps
294 */
295 private function makeServerArray( array $serverTemplate, array $groupLoads ) {
296 // The master server is the first host explicitly listed in the generic load group
297 if ( !$groupLoads[ILoadBalancer::GROUP_GENERIC] ) {
298 throw new UnexpectedValueException( "Empty generic load array; no master defined." );
299 }
300
301 $groupLoadsByHost = $this->reindexGroupLoads( $groupLoads );
302 // Get the ordered map of (host => load); the master server is first
303 $genericLoads = $groupLoads[ILoadBalancer::GROUP_GENERIC];
304 // Implictly append any hosts that only appear in custom load groups
305 $genericLoads += array_fill_keys( array_keys( $groupLoadsByHost ), 0 );
306
307 $servers = [];
308 foreach ( $genericLoads as $host => $load ) {
309 $servers[] = array_merge(
310 $serverTemplate,
311 $servers ? [] : $this->masterTemplateOverrides,
312 $this->templateOverridesByServer[$host] ?? [],
313 [
314 'host' => $this->hostsByName[$host] ?? $host,
315 'hostName' => $host,
316 'load' => $load,
317 'groupLoads' => $groupLoadsByHost[$host] ?? []
318 ]
319 );
320 }
321
322 return $servers;
323 }
324
325 /**
326 * Take a group load array indexed by group then server, and reindex it by server then group
327 * @param int[][] $groupLoads Map of (group => host => load)
328 * @return int[][] Map of (host => group => load)
329 */
330 private function reindexGroupLoads( array $groupLoads ) {
331 $reindexed = [];
332
333 foreach ( $groupLoads as $group => $loadByHost ) {
334 foreach ( $loadByHost as $host => $load ) {
335 $reindexed[$host][$group] = $load;
336 }
337 }
338
339 return $reindexed;
340 }
341
342 /**
343 * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
344 * @return string
345 */
346 private function getDomainDatabase( $domain = false ) {
347 return ( $domain === false )
348 ? $this->localDomain->getDatabase()
349 : DatabaseDomain::newFromId( $domain )->getDatabase();
350 }
351 }