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