Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.00% covered (success)
98.00%
49 / 50
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
MultiClusterAssignment
98.00% covered (success)
98.00%
49 / 50
88.89% covered (warning)
88.89%
8 / 9
22
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 evalGroupStrategy
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 initClusters
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 uniqueId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWritableClusters
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 canWriteToCluster
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSearchCluster
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCrossClusterName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServerList
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
1<?php
2
3namespace CirrusSearch\Assignment;
4
5use CirrusSearch\SearchConfig;
6use Wikimedia\Assert\Assert;
7
8class MultiClusterAssignment implements ClusterAssignment {
9    /** @var SearchConfig */
10    private $config;
11    /** @var array[][]|null 2d array mapping (replica, group) to connection configuration */
12    private $clusters;
13    /** @var string */
14    private $group;
15
16    public function __construct( SearchConfig $config ) {
17        $this->config = $config;
18        $groupConfig = $config->get( 'CirrusSearchReplicaGroup' );
19        if ( $groupConfig === null ) {
20            throw new \RuntimeException( 'CirrusSearchReplicaGroup is null' );
21        }
22        if ( is_string( $groupConfig ) ) {
23            $groupConfig = [
24                'type' => 'constant',
25                'group' => $groupConfig,
26            ];
27        }
28        $this->group = $this->evalGroupStrategy( $groupConfig );
29    }
30
31    /**
32     * @param array $groupConfig
33     * @return string
34     */
35    private function evalGroupStrategy( array $groupConfig ) {
36        // Determine which group this wiki belongs to
37        switch ( $groupConfig['type'] ) {
38        case 'constant':
39            return $groupConfig['group'];
40        case 'roundrobin':
41            $wikiId = $this->config->getWikiId();
42            $mod = count( $groupConfig['groups'] );
43            Assert::precondition( $mod > 0, "At least one replica group must be defined for roundrobin" );
44            $idx = crc32( $wikiId ) % $mod;
45            return $groupConfig['groups'][$idx];
46        default:
47            throw new \RuntimeException( "Unknown replica group type: {$groupConfig['type']}" );
48        }
49    }
50
51    private function initClusters(): array {
52        $clusters = [];
53        // We could require the input come in this shape, instead of reshaping
54        // it when we start, but it seemed awkward to work with.
55        foreach ( $this->config->get( 'CirrusSearchClusters' ) as $name => $config ) {
56            $replica = $config['replica'] ?? $name;
57            // Tempting to skip everything that doesn't match $this->group, but we have
58            // to also track single group replicas with arbitrary group names.
59            $group = $config['group'] ?? 'default';
60            unset( $config['replica'], $config['group'] );
61            if ( isset( $clusters[$replica][$group] ) ) {
62                throw new \RuntimeException( "Multiple clusters for replica: $replica group: $group" );
63            }
64            $clusters[$replica][$group] = $config;
65        }
66        return $clusters;
67    }
68
69    /**
70     * @param string $cluster Name of requested cluster
71     * @return string Uniquely identifies the connection properties.
72     */
73    public function uniqueId( $cluster ) {
74        return "{$this->group}:$cluster";
75    }
76
77    /**
78     * @return string[] List of CirrusSearch cluster names to write to.
79     */
80    public function getWritableClusters(): array {
81        $clusters = $this->config->get( 'CirrusSearchWriteClusters' );
82        if ( $clusters !== null ) {
83            return $clusters;
84        }
85        // No explicitly configured set of write clusters. Write to all known replicas.
86        if ( $this->clusters === null ) {
87            $this->clusters = $this->initClusters();
88        }
89        return array_keys( $this->clusters );
90    }
91
92    /**
93     * Check if a cluster is configured to accept writes
94     *
95     * @param string $cluster
96     * @return bool
97     */
98    public function canWriteToCluster( $cluster ) {
99        return in_array( $cluster, $this->getWritableClusters() );
100    }
101
102    /**
103     * @return string Name of the default search cluster.
104     */
105    public function getSearchCluster() {
106        return $this->config->get( 'CirrusSearchDefaultCluster' );
107    }
108
109    /**
110     * @return string Name to prefix indices with when
111     *  using cross-cluster-search.
112     */
113    public function getCrossClusterName() {
114        return $this->group;
115    }
116
117    /**
118     * @param string|null $replica
119     * @return string[]|array[]
120     */
121    public function getServerList( $replica = null ): array {
122        if ( $this->clusters === null ) {
123            $this->clusters = $this->initClusters();
124        }
125        $replica ??= $this->config->get( 'CirrusSearchDefaultCluster' );
126        if ( !isset( $this->clusters[$replica] ) ) {
127            $available = implode( ',', array_keys( $this->clusters ) );
128            throw new \RuntimeException( "Missing replica <$replica>, have <$available>" );
129        } elseif ( isset( $this->clusters[$replica][$this->group] ) ) {
130            return $this->clusters[$replica][$this->group];
131        } elseif ( count( $this->clusters[$replica] ) === 1 ) {
132            // If a replica only has a single elasticsearch cluster then by
133            // definition everything goes there.
134            return reset( $this->clusters[$replica] );
135        } else {
136            throw new \RuntimeException( "Missing replica: $replica group: {$this->group}" );
137        }
138    }
139}