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