Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.13% covered (warning)
71.13%
101 / 142
30.77% covered (danger)
30.77%
4 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
LBFactoryMulti
71.13% covered (warning)
71.13%
101 / 142
30.77% covered (danger)
30.77%
4 / 13
81.46
0.00% covered (danger)
0.00%
0 / 1
 __construct
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
6.50
 newMainLB
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
3.02
 resolveDomainInstance
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
7.05
 getMainLB
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 newExternalLB
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 getExternalLB
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getAllMainLBs
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getAllExternalLBs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getLBsForOwner
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 newLoadBalancer
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 makeServerConfigArrays
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 getSectionFromDatabase
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 reconfigure
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
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 */
20namespace Wikimedia\Rdbms;
21
22use InvalidArgumentException;
23use LogicException;
24use UnexpectedValueException;
25
26/**
27 * LoadBalancer manager for sites with several "main" database clusters
28 *
29 * Each database cluster consists of a "primary" server and any number of replica servers,
30 * all of which converge, as soon as possible, to contain the same schemas and records. If
31 * a replication topology has multiple primaries, then the "primary" is merely the preferred
32 * co-primary for the current context (e.g. datacenter).
33 *
34 * For single-primary topologies, the schemas and records of the primary define the "dataset".
35 * For multiple-primary topologies, the "dataset" is the convergent result of applying/merging
36 * all committed events (regardless of the co-primary they originated on); it possible that no
37 * co-primary has yet converged upon this state at any given time (especially when there are
38 * frequent writes and co-primaries are geographically distant).
39 *
40 * A "main" cluster contain a "main" dataset, which consists of data that is compact, highly
41 * relational (e.g. read by JOIN queries), and essential to one or more sites. The "external"
42 * clusters each store an "external" dataset, which consists of data that is non-relational
43 * (e.g. key/value pairs), self-contained (e.g. JOIN queries and transactions thereof never
44 * involve a main dataset), or too bulky to reside in a main dataset (e.g. text blobs).
45 *
46 * The class allows for large site farms to split up their data in the following ways:
47 *   - Vertically shard compact site-specific data by site (e.g. page/comment metadata)
48 *   - Vertically shard compact global data by module (e.g. account/notification data)
49 *   - Horizontally shard any bulk data by blob key (e.g. page/comment content blobs)
50 *
51 * @ingroup Database
52 */
53class LBFactoryMulti extends LBFactory {
54    /** @var array<string,LoadBalancer> Map of (main section => tracked LoadBalancer) */
55    private $mainLBs = [];
56    /** @var array<string,LoadBalancer> Map of (external cluster => tracked LoadBalancer) */
57    private $externalLBs = [];
58
59    /** @var string[] Map of (server name => IP address) */
60    private $hostsByServerName;
61    /** @var string[] Map of (database name => main section) */
62    private $sectionsByDB;
63    /** @var int[][][] Map of (main section => group => server name => load ratio) */
64    private $groupLoadsBySection;
65    /** @var int[][] Map of (external cluster => server name => load ratio) */
66    private $externalLoadsByCluster;
67    /** @var array Server config map ("host", "serverName", "load", and "groupLoads" ignored) */
68    private $serverTemplate;
69    /** @var array Server config map overriding "serverTemplate" for all external servers */
70    private $externalTemplateOverrides;
71    /** @var array[] Map of (main section => server config map overrides) */
72    private $templateOverridesBySection;
73    /** @var array[] Map of (external cluster => server config map overrides) */
74    private $templateOverridesByCluster;
75    /** @var array Server config override map for all main/external primary DB servers */
76    private $masterTemplateOverrides;
77    /** @var array[] Map of (server name => server config map overrides) for all servers */
78    private $templateOverridesByServer;
79    /** @var string[]|bool[] A map of (main section => read-only message) */
80    private $readOnlyBySection;
81    /** @var array Configuration for the LoadMonitor to use within LoadBalancer instances */
82    private $loadMonitorConfig;
83    /** @var DatabaseDomain[] Map of (domain ID => domain instance) */
84    private $nonLocalDomainCache = [];
85
86    /**
87     * Template override precedence (highest => lowest):
88     *   - templateOverridesByServer
89     *   - masterTemplateOverrides
90     *   - templateOverridesBySection/templateOverridesByCluster
91     *   - externalTemplateOverrides
92     *   - serverTemplate
93     * Overrides only work on top level keys (so nested values will not be merged).
94     *
95     * Server config maps should be of the format Database::factory() requires.
96     * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
97     * data can be before the load balancer tries to avoid using it. The map can have 'is static'
98     * set to disable blocking  replication sync checks (intended for archive servers with
99     * unchanging data).
100     *
101     * @see LBFactory::__construct()
102     * @param array $conf Additional parameters include:
103     *   - hostsByName: map of (server name => IP address). [optional]
104     *   - sectionsByDB: map of (database => main section). The database name "DEFAULT" is
105     *      interpreted as a catch-all for all databases not otherwise mentioned. If no section
106     *      name is specified for "DEFAULT", then the catch-all section is assumed to be named
107     *      "DEFAULT". [optional]
108     *   - sectionLoads: map of (main section => server name => load ratio); the first host
109     *      listed in each section is the primary DB server for that section. [optional]
110     *   - groupLoadsBySection: map of (main section => group => server name => group load ratio).
111     *      Any ILoadBalancer::GROUP_GENERIC group will be ignored. [optional]
112     *   - externalLoads: map of (cluster => server name => load ratio) map. [optional]
113     *   - serverTemplate: server config map for Database::factory().
114     *      Note that "host", "serverName" and "load" entries will be overridden by
115     *      "groupLoadsBySection" and "hostsByName". [optional]
116     *   - externalTemplateOverrides: server config map overrides for external stores;
117     *      respects the override precedence described above. [optional]
118     *   - templateOverridesBySection: map of (main section => server config map overrides);
119     *      respects the override precedence described above. [optional]
120     *   - templateOverridesByCluster: map of (external cluster => server config map overrides);
121     *      respects the override precedence described above. [optional]
122     *   - masterTemplateOverrides: server config map overrides for masters;
123     *      respects the override precedence described above. [optional]
124     *   - templateOverridesByServer: map of (server name => server config map overrides);
125     *      respects the override precedence described above and applies to both core
126     *      and external storage. [optional]
127     *   - loadMonitor: LoadMonitor::__construct() parameters with "class" field. [optional]
128     *   - readOnlyBySection: map of (main section => message text or false).
129     *      String values make sections read only, whereas anything else does not
130     *      restrict read/write mode. [optional]
131     *   - configCallback: A callback that returns a conf array that can be passed to
132     *      the reconfigure() method. This will be used to autoReconfigure() to load
133     *      any updated configuration.
134     */
135    public function __construct( array $conf ) {
136        parent::__construct( $conf );
137
138        $this->hostsByServerName = $conf['hostsByName'] ?? [];
139        $this->sectionsByDB = $conf['sectionsByDB'];
140        $this->sectionsByDB += [ self::CLUSTER_MAIN_DEFAULT => self::CLUSTER_MAIN_DEFAULT ];
141        $this->groupLoadsBySection = $conf['groupLoadsBySection'] ?? [];
142        foreach ( ( $conf['sectionLoads'] ?? [] ) as $section => $loadsByServerName ) {
143            $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] = $loadsByServerName;
144        }
145        $this->externalLoadsByCluster = $conf['externalLoads'] ?? [];
146        $this->serverTemplate = $conf['serverTemplate'] ?? [];
147        $this->externalTemplateOverrides = $conf['externalTemplateOverrides'] ?? [];
148        $this->templateOverridesBySection = $conf['templateOverridesBySection'] ?? [];
149        $this->templateOverridesByCluster = $conf['templateOverridesByCluster'] ?? [];
150        $this->masterTemplateOverrides = $conf['masterTemplateOverrides'] ?? [];
151        $this->templateOverridesByServer = $conf['templateOverridesByServer'] ?? [];
152        $this->readOnlyBySection = $conf['readOnlyBySection'] ?? [];
153
154        if ( isset( $conf['loadMonitor'] ) ) {
155            $this->loadMonitorConfig = $conf['loadMonitor'];
156        } elseif ( isset( $conf['loadMonitorClass'] ) ) { // b/c
157            $this->loadMonitorConfig = [ 'class' => $conf['loadMonitorClass'] ];
158        } else {
159            $this->loadMonitorConfig = [ 'class' => LoadMonitor::class ];
160        }
161
162        foreach ( $this->externalLoadsByCluster as $cluster => $_ ) {
163            if ( isset( $this->groupLoadsBySection[$cluster] ) ) {
164                throw new LogicException(
165                    "External cluster '$cluster' has the same name as a main section/cluster"
166                );
167            }
168        }
169    }
170
171    public function newMainLB( $domain = false ): ILoadBalancerForOwner {
172        $domainInstance = $this->resolveDomainInstance( $domain );
173        $database = $domainInstance->getDatabase();
174        $section = $this->getSectionFromDatabase( $database );
175
176        if ( !isset( $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] ) ) {
177            throw new UnexpectedValueException( "Section '$section' has no hosts defined." );
178        }
179
180        return $this->newLoadBalancer(
181            $section,
182            array_merge(
183                $this->serverTemplate,
184                $this->templateOverridesBySection[$section] ?? []
185            ),
186            $this->groupLoadsBySection[$section],
187            // Use the LB-specific read-only reason if everything isn't already read-only
188            is_string( $this->readOnlyReason )
189                ? $this->readOnlyReason
190                : ( $this->readOnlyBySection[$section] ?? false )
191        );
192    }
193
194    /**
195     * @param DatabaseDomain|string|false $domain
196     * @return DatabaseDomain
197     */
198    private function resolveDomainInstance( $domain ) {
199        if ( $domain instanceof DatabaseDomain ) {
200            return $domain; // already a domain instance
201        } elseif ( $domain === false || $domain === $this->localDomain->getId() ) {
202            return $this->localDomain;
203        } elseif ( isset( $this->domainAliases[$domain] ) ) {
204            // This array acts as both the original map and as instance cache.
205            // Instances pass-through DatabaseDomain::newFromId as-is.
206            $this->domainAliases[$domain] =
207                DatabaseDomain::newFromId( $this->domainAliases[$domain] );
208
209            return $this->domainAliases[$domain];
210        }
211
212        $cachedDomain = $this->nonLocalDomainCache[$domain] ?? null;
213        if ( $cachedDomain === null ) {
214            $cachedDomain = DatabaseDomain::newFromId( $domain );
215            $this->nonLocalDomainCache = [ $domain => $cachedDomain ];
216        }
217
218        return $cachedDomain;
219    }
220
221    public function getMainLB( $domain = false ): ILoadBalancer {
222        $domainInstance = $this->resolveDomainInstance( $domain );
223        $section = $this->getSectionFromDatabase( $domainInstance->getDatabase() );
224
225        if ( !isset( $this->mainLBs[$section] ) ) {
226            $this->mainLBs[$section] = $this->newMainLB( $domain );
227        }
228
229        return $this->mainLBs[$section];
230    }
231
232    public function newExternalLB( $cluster ): ILoadBalancerForOwner {
233        if ( !isset( $this->externalLoadsByCluster[$cluster] ) ) {
234            throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
235        }
236        return $this->newLoadBalancer(
237            $cluster,
238            array_merge(
239                $this->serverTemplate,
240                $this->externalTemplateOverrides,
241                $this->templateOverridesByCluster[$cluster] ?? []
242            ),
243            [ ILoadBalancer::GROUP_GENERIC => $this->externalLoadsByCluster[$cluster] ],
244            $this->readOnlyReason
245        );
246    }
247
248    public function getExternalLB( $cluster ): ILoadBalancer {
249        if ( !isset( $this->externalLBs[$cluster] ) ) {
250            $this->externalLBs[$cluster] = $this->newExternalLB(
251                $cluster
252            );
253        }
254
255        return $this->externalLBs[$cluster];
256    }
257
258    public function getAllMainLBs(): array {
259        $lbs = [];
260        foreach ( $this->sectionsByDB as $db => $section ) {
261            if ( !isset( $lbs[$section] ) ) {
262                $lbs[$section] = $this->getMainLB( $db );
263            }
264        }
265
266        return $lbs;
267    }
268
269    public function getAllExternalLBs(): array {
270        $lbs = [];
271        foreach ( $this->externalLoadsByCluster as $cluster => $unused ) {
272            $lbs[$cluster] = $this->getExternalLB( $cluster );
273        }
274
275        return $lbs;
276    }
277
278    protected function getLBsForOwner() {
279        foreach ( $this->mainLBs as $lb ) {
280            yield $lb;
281        }
282        foreach ( $this->externalLBs as $lb ) {
283            yield $lb;
284        }
285    }
286
287    /**
288     * Make a new load balancer object based on template and load array
289     *
290     * @param string $clusterName
291     * @param array $serverTemplate
292     * @param array $groupLoads
293     * @param string|false $readOnlyReason
294     * @return LoadBalancer
295     */
296    private function newLoadBalancer(
297        string $clusterName,
298        array $serverTemplate,
299        array $groupLoads,
300        $readOnlyReason
301    ) {
302        $lb = new LoadBalancer( array_merge(
303            $this->baseLoadBalancerParams(),
304            [
305                'servers' => $this->makeServerConfigArrays( $serverTemplate, $groupLoads ),
306                'loadMonitor' => $this->loadMonitorConfig,
307                'readOnlyReason' => $readOnlyReason,
308                'clusterName' => $clusterName
309            ]
310        ) );
311        $this->initLoadBalancer( $lb );
312
313        return $lb;
314    }
315
316    /**
317     * Make a server array as expected by LoadBalancer::__construct()
318     *
319     * @param array $serverTemplate Server config map
320     * @param int[][] $groupLoads Map of (group => server name => load)
321     * @return array[] List of server config maps
322     */
323    private function makeServerConfigArrays( array $serverTemplate, array $groupLoads ) {
324        // The primary DB server is the first host explicitly listed in the generic load group
325        if ( !$groupLoads[ILoadBalancer::GROUP_GENERIC] ) {
326            throw new UnexpectedValueException( "Empty generic load array; no primary DB defined." );
327        }
328        $groupLoadsByServerName = [];
329        foreach ( $groupLoads as $group => $loadByServerName ) {
330            foreach ( $loadByServerName as $serverName => $load ) {
331                $groupLoadsByServerName[$serverName][$group] = $load;
332            }
333        }
334
335        // Get the ordered map of (server name => load); the primary DB server is first
336        $genericLoads = $groupLoads[ILoadBalancer::GROUP_GENERIC];
337        // Implicitly append any hosts that only appear in custom load groups
338        $genericLoads += array_fill_keys( array_keys( $groupLoadsByServerName ), 0 );
339        $servers = [];
340        foreach ( $genericLoads as $serverName => $load ) {
341            $servers[] = array_merge(
342                $serverTemplate,
343                $servers ? [] : $this->masterTemplateOverrides,
344                $this->templateOverridesByServer[$serverName] ?? [],
345                [
346                    'host' => $this->hostsByServerName[$serverName] ?? $serverName,
347                    'serverName' => $serverName,
348                    'load' => $load,
349                    'groupLoads' => $groupLoadsByServerName[$serverName] ?? []
350                ]
351            );
352        }
353
354        return $servers;
355    }
356
357    /**
358     * @param string $database
359     * @return string Main section name
360     */
361    private function getSectionFromDatabase( $database ) {
362        return $this->sectionsByDB[$database]
363            ?? $this->sectionsByDB[self::CLUSTER_MAIN_DEFAULT]
364            ?? self::CLUSTER_MAIN_DEFAULT;
365    }
366
367    public function reconfigure( array $conf ): void {
368        if ( !$conf ) {
369            return;
370        }
371
372        foreach ( $this->mainLBs as $lb ) {
373            // Approximate what LBFactoryMulti::__construct does (T346365)
374            $groupLoads = $conf['groupLoadsBySection'][$lb->getClusterName()] ?? [];
375            $groupLoads[ILoadBalancer::GROUP_GENERIC] = $conf['sectionLoads'][$lb->getClusterName()];
376            $config = [
377                'servers' => $this->makeServerConfigArrays( $conf['serverTemplate'] ?? [], $groupLoads )
378            ];
379            $lb->reconfigure( $config );
380
381        }
382        foreach ( $this->externalLBs as $lb ) {
383            $groupLoads = [
384                ILoadBalancer::GROUP_GENERIC => $conf['externalLoads'][$lb->getClusterName()]
385            ];
386            $config = [
387                'servers' => $this->makeServerConfigArrays( $conf['serverTemplate'] ?? [], $groupLoads )
388            ];
389            $lb->reconfigure( $config );
390        }
391    }
392}