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