]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/libs/rdbms/lbfactory/LBFactoryMulti.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.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
28 /**
29  * A multi-database, multi-master factory for Wikimedia and similar installations.
30  * Ignores the old configuration globals.
31  *
32  * @ingroup Database
33  */
34 class LBFactoryMulti extends LBFactory {
35         /** @var array A map of database names to section names */
36         private $sectionsByDB;
37
38         /**
39          * @var array A 2-d map. For each section, gives a map of server names to
40          * load ratios
41          */
42         private $sectionLoads;
43
44         /**
45          * @var array[] Server info associative array
46          * @note The host, hostName and load entries will be overridden
47          */
48         private $serverTemplate;
49
50         // Optional settings
51
52         /** @var array A 3-d map giving server load ratios for each section and group */
53         private $groupLoadsBySection = [];
54
55         /** @var array A 3-d map giving server load ratios by DB name */
56         private $groupLoadsByDB = [];
57
58         /** @var array A map of hostname to IP address */
59         private $hostsByName = [];
60
61         /** @var array A map of external storage cluster name to server load map */
62         private $externalLoads = [];
63
64         /**
65          * @var array A set of server info keys overriding serverTemplate for
66          * external storage
67          */
68         private $externalTemplateOverrides;
69
70         /**
71          * @var array A 2-d map overriding serverTemplate and
72          * externalTemplateOverrides on a server-by-server basis. Applies to both
73          * core and external storage
74          */
75         private $templateOverridesByServer;
76
77         /** @var array A 2-d map overriding the server info by section */
78         private $templateOverridesBySection;
79
80         /** @var array A 2-d map overriding the server info by external storage cluster */
81         private $templateOverridesByCluster;
82
83         /** @var array An override array for all master servers */
84         private $masterTemplateOverrides;
85
86         /**
87          * @var array|bool A map of section name to read-only message. Missing or
88          * false for read/write
89          */
90         private $readOnlyBySection = [];
91
92         /** @var array Load balancer factory configuration */
93         private $conf;
94
95         /** @var LoadBalancer[] */
96         private $mainLBs = [];
97
98         /** @var LoadBalancer[] */
99         private $extLBs = [];
100
101         /** @var string */
102         private $loadMonitorClass = 'LoadMonitor';
103
104         /** @var string */
105         private $lastDomain;
106
107         /** @var string */
108         private $lastSection;
109
110         /**
111          * @see LBFactory::__construct()
112          *
113          * Template override precedence (highest => lowest):
114          *   - templateOverridesByServer
115          *   - masterTemplateOverrides
116          *   - templateOverridesBySection/templateOverridesByCluster
117          *   - externalTemplateOverrides
118          *   - serverTemplate
119          * Overrides only work on top level keys (so nested values will not be merged).
120          *
121          * Server configuration maps should be of the format Database::factory() requires.
122          * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
123          * data can be before the load balancer tries to avoid using it. The map can have 'is static'
124          * set to disable blocking  replication sync checks (intended for archive servers with
125          * unchanging data).
126          *
127          * @param array $conf Parameters of LBFactory::__construct() as well as:
128          *   - sectionsByDB                Map of database names to section names.
129          *   - sectionLoads                2-d map. For each section, gives a map of server names to
130          *                                 load ratios. For example:
131          *                                 [
132          *                                     'section1' => [
133          *                                         'db1' => 100,
134          *                                         'db2' => 100
135          *                                     ]
136          *                                 ]
137          *   - serverTemplate              Server configuration map intended for Database::factory().
138          *                                 Note that "host", "hostName" and "load" entries will be
139          *                                 overridden by "sectionLoads" and "hostsByName".
140          *   - groupLoadsBySection         3-d map giving server load ratios for each section/group.
141          *                                 For example:
142          *                                 [
143          *                                     'section1' => [
144          *                                         'group1' => [
145          *                                             'db1' => 100,
146          *                                             'db2' => 100
147          *                                         ]
148          *                                     ]
149          *                                 ]
150          *   - groupLoadsByDB              3-d map giving server load ratios by DB name.
151          *   - hostsByName                 Map of hostname to IP address.
152          *   - externalLoads               Map of external storage cluster name to server load map.
153          *   - externalTemplateOverrides   Set of server configuration maps overriding
154          *                                 "serverTemplate" for external storage.
155          *   - templateOverridesByServer   2-d map overriding "serverTemplate" and
156          *                                 "externalTemplateOverrides" on a server-by-server basis.
157          *                                 Applies to both core and external storage.
158          *   - templateOverridesBySection  2-d map overriding the server configuration maps by section.
159          *   - templateOverridesByCluster  2-d map overriding the server configuration maps by external
160          *                                 storage cluster.
161          *   - masterTemplateOverrides     Server configuration map overrides for all master servers.
162          *   - loadMonitorClass            Name of the LoadMonitor class to always use.
163          *   - readOnlyBySection           A map of section name to read-only message.
164          *                                 Missing or false for read/write.
165          */
166         public function __construct( array $conf ) {
167                 parent::__construct( $conf );
168
169                 $this->conf = $conf;
170                 $required = [ 'sectionsByDB', 'sectionLoads', 'serverTemplate' ];
171                 $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
172                         'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
173                         'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
174                         'readOnlyBySection', 'loadMonitorClass' ];
175
176                 foreach ( $required as $key ) {
177                         if ( !isset( $conf[$key] ) ) {
178                                 throw new InvalidArgumentException( __CLASS__ . ": $key is required." );
179                         }
180                         $this->$key = $conf[$key];
181                 }
182
183                 foreach ( $optional as $key ) {
184                         if ( isset( $conf[$key] ) ) {
185                                 $this->$key = $conf[$key];
186                         }
187                 }
188         }
189
190         /**
191          * @param bool|string $domain
192          * @return string
193          */
194         private function getSectionForDomain( $domain = false ) {
195                 if ( $this->lastDomain === $domain ) {
196                         return $this->lastSection;
197                 }
198                 list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
199                 if ( isset( $this->sectionsByDB[$dbName] ) ) {
200                         $section = $this->sectionsByDB[$dbName];
201                 } else {
202                         $section = 'DEFAULT';
203                 }
204                 $this->lastSection = $section;
205                 $this->lastDomain = $domain;
206
207                 return $section;
208         }
209
210         /**
211          * @param bool|string $domain
212          * @return LoadBalancer
213          */
214         public function newMainLB( $domain = false ) {
215                 list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
216                 $section = $this->getSectionForDomain( $domain );
217                 if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
218                         $groupLoads = $this->groupLoadsByDB[$dbName];
219                 } else {
220                         $groupLoads = [];
221                 }
222
223                 if ( isset( $this->groupLoadsBySection[$section] ) ) {
224                         $groupLoads = array_merge_recursive(
225                                 $groupLoads, $this->groupLoadsBySection[$section] );
226                 }
227
228                 $readOnlyReason = $this->readOnlyReason;
229                 // Use the LB-specific read-only reason if everything isn't already read-only
230                 if ( $readOnlyReason === false && isset( $this->readOnlyBySection[$section] ) ) {
231                         $readOnlyReason = $this->readOnlyBySection[$section];
232                 }
233
234                 $template = $this->serverTemplate;
235                 if ( isset( $this->templateOverridesBySection[$section] ) ) {
236                         $template = $this->templateOverridesBySection[$section] + $template;
237                 }
238
239                 return $this->newLoadBalancer(
240                         $template,
241                         $this->sectionLoads[$section],
242                         $groupLoads,
243                         $readOnlyReason
244                 );
245         }
246
247         /**
248          * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
249          * @return LoadBalancer
250          */
251         public function getMainLB( $domain = false ) {
252                 $section = $this->getSectionForDomain( $domain );
253                 if ( !isset( $this->mainLBs[$section] ) ) {
254                         $this->mainLBs[$section] = $this->newMainLB( $domain );
255                 }
256
257                 return $this->mainLBs[$section];
258         }
259
260         public function newExternalLB( $cluster ) {
261                 if ( !isset( $this->externalLoads[$cluster] ) ) {
262                         throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
263                 }
264                 $template = $this->serverTemplate;
265                 if ( $this->externalTemplateOverrides ) {
266                         $template = $this->externalTemplateOverrides + $template;
267                 }
268                 if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
269                         $template = $this->templateOverridesByCluster[$cluster] + $template;
270                 }
271
272                 return $this->newLoadBalancer(
273                         $template,
274                         $this->externalLoads[$cluster],
275                         [],
276                         $this->readOnlyReason
277                 );
278         }
279
280         public function getExternalLB( $cluster ) {
281                 if ( !isset( $this->extLBs[$cluster] ) ) {
282                         $this->extLBs[$cluster] = $this->newExternalLB( $cluster );
283                 }
284
285                 return $this->extLBs[$cluster];
286         }
287
288         public function getAllMainLBs() {
289                 $lbs = [];
290                 foreach ( $this->sectionsByDB as $db => $section ) {
291                         if ( !isset( $lbs[$section] ) ) {
292                                 $lbs[$section] = $this->getMainLB( $db );
293                         }
294                 }
295
296                 return $lbs;
297         }
298
299         public function getAllExternalLBs() {
300                 $lbs = [];
301                 foreach ( $this->externalLoads as $cluster => $unused ) {
302                         $lbs[$cluster] = $this->getExternalLB( $cluster );
303                 }
304
305                 return $lbs;
306         }
307
308         /**
309          * Make a new load balancer object based on template and load array
310          *
311          * @param array $template
312          * @param array $loads
313          * @param array $groupLoads
314          * @param string|bool $readOnlyReason
315          * @return LoadBalancer
316          */
317         private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
318                 $lb = new LoadBalancer( array_merge(
319                         $this->baseLoadBalancerParams(),
320                         [
321                                 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
322                                 'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
323                                 'readOnlyReason' => $readOnlyReason
324                         ]
325                 ) );
326                 $this->initLoadBalancer( $lb );
327
328                 return $lb;
329         }
330
331         /**
332          * Make a server array as expected by LoadBalancer::__construct, using a template and load array
333          *
334          * @param array $template
335          * @param array $loads
336          * @param array $groupLoads
337          * @return array
338          */
339         private function makeServerArray( $template, $loads, $groupLoads ) {
340                 $servers = [];
341                 $master = true;
342                 $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
343                 foreach ( $groupLoadsByServer as $server => $stuff ) {
344                         if ( !isset( $loads[$server] ) ) {
345                                 $loads[$server] = 0;
346                         }
347                 }
348                 foreach ( $loads as $serverName => $load ) {
349                         $serverInfo = $template;
350                         if ( $master ) {
351                                 $serverInfo['master'] = true;
352                                 if ( $this->masterTemplateOverrides ) {
353                                         $serverInfo = $this->masterTemplateOverrides + $serverInfo;
354                                 }
355                                 $master = false;
356                         } else {
357                                 $serverInfo['replica'] = true;
358                         }
359                         if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
360                                 $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
361                         }
362                         if ( isset( $groupLoadsByServer[$serverName] ) ) {
363                                 $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
364                         }
365                         if ( isset( $this->hostsByName[$serverName] ) ) {
366                                 $serverInfo['host'] = $this->hostsByName[$serverName];
367                         } else {
368                                 $serverInfo['host'] = $serverName;
369                         }
370                         $serverInfo['hostName'] = $serverName;
371                         $serverInfo['load'] = $load;
372                         $serverInfo += [ 'flags' => IDatabase::DBO_DEFAULT ];
373
374                         $servers[] = $serverInfo;
375                 }
376
377                 return $servers;
378         }
379
380         /**
381          * Take a group load array indexed by group then server, and reindex it by server then group
382          * @param array $groupLoads
383          * @return array
384          */
385         private function reindexGroupLoads( $groupLoads ) {
386                 $reindexed = [];
387                 foreach ( $groupLoads as $group => $loads ) {
388                         foreach ( $loads as $server => $load ) {
389                                 $reindexed[$server][$group] = $load;
390                         }
391                 }
392
393                 return $reindexed;
394         }
395
396         /**
397          * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
398          * @return array [database name, table prefix]
399          */
400         private function getDBNameAndPrefix( $domain = false ) {
401                 $domain = ( $domain === false )
402                         ? $this->localDomain
403                         : DatabaseDomain::newFromId( $domain );
404
405                 return [ $domain->getDatabase(), $domain->getTablePrefix() ];
406         }
407
408         /**
409          * Execute a function for each tracked load balancer
410          * The callback is called with the load balancer as the first parameter,
411          * and $params passed as the subsequent parameters.
412          * @param callable $callback
413          * @param array $params
414          */
415         public function forEachLB( $callback, array $params = [] ) {
416                 foreach ( $this->mainLBs as $lb ) {
417                         call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
418                 }
419                 foreach ( $this->extLBs as $lb ) {
420                         call_user_func_array( $callback, array_merge( [ $lb ], $params ) );
421                 }
422         }
423 }