Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.63% covered (warning)
52.63%
40 / 76
35.29% covered (danger)
35.29%
6 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connection
52.63% covered (warning)
52.63%
40 / 76
35.29% covered (danger)
35.29%
6 / 17
165.20
0.00% covered (danger)
0.00%
0 / 1
 getPool
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 clearPool
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 __sleep
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getClusterName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSettings
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getServerList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxConnectionAttempts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArchiveIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllIndexSuffixes
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 extractIndexSuffix
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getIndexSuffixForNamespace
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 pickIndexSuffixForNamespaces
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getAllIndexSuffixesForNamespaces
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 destroyClient
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getClusterConnections
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch;
4
5use Exception;
6use LogicException;
7use MediaWiki\Extension\Elastica\ElasticaConnection;
8use MediaWiki\MainConfigNames;
9use MediaWiki\MediaWikiServices;
10use Wikimedia\Assert\Assert;
11
12/**
13 * Forms and caches connection to Elasticsearch as well as client objects
14 * that contain connection information like \Elastica\Index and \Elastica\Type.
15 *
16 * @license GPL-2.0-or-later
17 */
18class Connection extends ElasticaConnection {
19
20    /**
21     * Suffix of the index that holds content articles.
22     */
23    public const CONTENT_INDEX_SUFFIX = 'content';
24
25    /**
26     * Suffix of the index that holds non-content articles.
27     */
28    public const GENERAL_INDEX_SUFFIX = 'general';
29
30    /**
31     * Suffix of the index that hosts content title suggestions
32     */
33    public const TITLE_SUGGEST_INDEX_SUFFIX = 'titlesuggest';
34
35    /**
36     * Suffix of the index that hosts archive data
37     */
38    public const ARCHIVE_INDEX_SUFFIX = 'archive';
39
40    /**
41     * Name of the page document type.
42     */
43    public const PAGE_DOC_TYPE = 'page';
44
45    /**
46     * Name of the archive document type
47     */
48    public const ARCHIVE_DOC_TYPE = 'archive';
49
50    /**
51     * "Alternative" index suffix
52     */
53    public const ALT_SUFFIX = 'alt';
54
55    /**
56     * Map of index types (suffix names) indexed by mapping type.
57     */
58    private const SUFFIX_MAPPING = [
59        self::PAGE_DOC_TYPE => [
60            self::CONTENT_INDEX_SUFFIX,
61            self::GENERAL_INDEX_SUFFIX,
62        ],
63        self::ARCHIVE_DOC_TYPE => [
64            self::ARCHIVE_INDEX_SUFFIX
65        ],
66    ];
67
68    /**
69     * @var SearchConfig
70     */
71    protected $config;
72
73    /**
74     * @var string
75     */
76    protected $cluster;
77
78    /**
79     * @var ClusterSettings|null
80     */
81    private $clusterSettings;
82
83    /**
84     * @var self[][]
85     */
86    private static $pool = [];
87
88    /**
89     * @param SearchConfig $config
90     * @param string|null $cluster
91     * @return self
92     */
93    public static function getPool( SearchConfig $config, $cluster = null ): self {
94        $assignment = $config->getClusterAssignment();
95        $cluster ??= $assignment->getSearchCluster();
96        $wiki = $config->getWikiId();
97        $clusterId = $assignment->uniqueId( $cluster );
98        return self::$pool[$wiki][$clusterId] ?? new self( $config, $cluster );
99    }
100
101    /**
102     * Pool state must be cleared when forking. Also useful
103     * in tests.
104     */
105    public static function clearPool() {
106        self::$pool = [];
107    }
108
109    /**
110     * @param SearchConfig $config
111     * @param string|null $cluster Name of cluster to use, or
112     *  null for the default cluster.
113     */
114    public function __construct( SearchConfig $config, $cluster = null ) {
115        $this->config = $config;
116        $assignment = $config->getClusterAssignment();
117        $this->cluster = $cluster ?? $assignment->getSearchCluster();
118        $this->setConnectTimeout( $this->getSettings()->getConnectTimeout() );
119        // overwrites previous connection if it exists, but these
120        // seemed more centralized than having the entry points
121        // all call a static method unnecessarily.
122        // TODO: Assumes all $config that return same wiki id have same config, but there
123        // are places that expect they can wrap config with new values and use them.
124        $clusterId = $assignment->uniqueId( $this->cluster );
125        self::$pool[$config->getWikiId()][$clusterId] = $this;
126    }
127
128    /**
129     * @return never
130     */
131    public function __sleep() {
132        throw new \RuntimeException( 'Attempting to serialize ES connection' );
133    }
134
135    /**
136     * @return string
137     */
138    public function getClusterName() {
139        return $this->cluster;
140    }
141
142    /**
143     * @return ClusterSettings
144     */
145    public function getSettings() {
146        if ( $this->clusterSettings === null ) {
147            $this->clusterSettings = new ClusterSettings( $this->config, $this->cluster );
148        }
149        return $this->clusterSettings;
150    }
151
152    /**
153     * @return string[]|array[] Either a list of hostnames, for default
154     *  connection configuration or an array of arrays giving full connection
155     *  specifications.
156     */
157    public function getServerList() {
158        return $this->config->getClusterAssignment()->getServerList( $this->cluster );
159    }
160
161    /**
162     * How many times can we attempt to connect per host?
163     *
164     * @return int
165     */
166    public function getMaxConnectionAttempts() {
167        return $this->config->get( CirrusConfigNames::ConnectionAttempts );
168    }
169
170    /**
171     * Fetch the Elastica Index for archive.
172     * @param mixed $name basename of index
173     * @return \Elastica\Index
174     */
175    public function getArchiveIndex( $name ) {
176        return $this->getIndex( $name, self::ARCHIVE_INDEX_SUFFIX );
177    }
178
179    /**
180     * Get all index types we support, content, general, plus custom ones
181     *
182     * @param string|null $documentType the document type name the index must support to be returned
183     * can be self::PAGE_DOC_TYPE for content and general indices but also self::ARCHIVE_DOC_TYPE
184     * for the archive index. Defaults to Connection::PAGE_DOC_TYPE.
185     * set to null to return all known index types (only suited for maintenance tasks, not for read/write operations).
186     * @return string[]
187     */
188    public function getAllIndexSuffixes( $documentType = self::PAGE_DOC_TYPE ) {
189        Assert::parameter( $documentType === null || isset( self::SUFFIX_MAPPING[$documentType] ),
190            '$documentType', "Unknown mapping type $documentType" );
191        $indexSuffixes = [];
192
193        if ( $documentType === null ) {
194            foreach ( self::SUFFIX_MAPPING as $types ) {
195                $indexSuffixes = array_merge( $indexSuffixes, $types );
196            }
197            $indexSuffixes = array_merge(
198                $indexSuffixes,
199                array_values( $this->config->get( CirrusConfigNames::NamespaceMappings ) )
200            );
201        } else {
202            $indexSuffixes = array_merge(
203                $indexSuffixes,
204                self::SUFFIX_MAPPING[$documentType],
205                $documentType === self::PAGE_DOC_TYPE ?
206                    array_values( $this->config->get( CirrusConfigNames::NamespaceMappings ) ) : []
207            );
208        }
209
210        if ( !$this->getSettings()->isPrivateCluster()
211            || !$this->config->get( CirrusConfigNames::EnableArchive )
212        ) {
213            $indexSuffixes = array_diff( $indexSuffixes, [ self::ARCHIVE_INDEX_SUFFIX ] );
214        }
215
216        return $indexSuffixes;
217    }
218
219    /**
220     * @param string $name
221     * @return string
222     * @throws Exception
223     */
224    public function extractIndexSuffix( $name ) {
225        $matches = [];
226        $possible = implode( '|', array_map( 'preg_quote', $this->getAllIndexSuffixes( null ) ) );
227        if ( !preg_match( "/_($possible)_[^_]+$/", $name, $matches ) ) {
228            throw new LogicException( "Can't parse index name: $name" );
229        }
230
231        return $matches[1];
232    }
233
234    /**
235     * Get the index suffix for a given namespace
236     * @param int $namespace A namespace id
237     * @return string
238     */
239    public function getIndexSuffixForNamespace( $namespace ) {
240        $mappings = $this->config->get( CirrusConfigNames::NamespaceMappings );
241        if ( isset( $mappings[$namespace] ) ) {
242            return $mappings[$namespace];
243        }
244        $defaultSearch = $this->config->get( MainConfigNames::NamespacesToBeSearchedDefault );
245        if ( isset( $defaultSearch[$namespace] ) && $defaultSearch[$namespace] ) {
246            return self::CONTENT_INDEX_SUFFIX;
247        }
248
249        return MediaWikiServices::getInstance()->getNamespaceInfo()->isContent( $namespace ) ?
250            self::CONTENT_INDEX_SUFFIX : self::GENERAL_INDEX_SUFFIX;
251    }
252
253    /**
254     * @param int[]|null $namespaces List of namespaces to check
255     * @return string|false The suffix to use (e.g. content or general) to
256     *  query the namespaces, or false if all need to be queried.
257     */
258    public function pickIndexSuffixForNamespaces( ?array $namespaces = null ) {
259        $indexSuffixes = [];
260        if ( $namespaces ) {
261            foreach ( $namespaces as $namespace ) {
262                $indexSuffixes[] = $this->getIndexSuffixForNamespace( $namespace );
263            }
264            $indexSuffixes = array_unique( $indexSuffixes );
265        }
266        if ( count( $indexSuffixes ) === 1 ) {
267            return $indexSuffixes[0];
268        } else {
269            return false;
270        }
271    }
272
273    /**
274     * @param int[]|null $namespaces List of namespaces to check
275     * @return string[] the list of all index suffixes mathing the namespaces
276     */
277    public function getAllIndexSuffixesForNamespaces( $namespaces = null ) {
278        if ( $namespaces ) {
279            $indexSuffixes = [];
280            foreach ( $namespaces as $namespace ) {
281                $indexSuffixes[] = $this->getIndexSuffixForNamespace( $namespace );
282            }
283            return array_unique( $indexSuffixes );
284        }
285        // If no namespaces provided all indices are needed
286        $mappings = $this->config->get( CirrusConfigNames::NamespaceMappings );
287        return array_merge( self::SUFFIX_MAPPING[self::PAGE_DOC_TYPE],
288            array_values( $mappings ) );
289    }
290
291    public function destroyClient() {
292        self::$pool = [];
293        parent::destroyClient();
294    }
295
296    /**
297     * @param string[] $clusters array of cluster names
298     * @param SearchConfig $config the search config
299     * @return self[] array of connection indexed by cluster name
300     */
301    public static function getClusterConnections( array $clusters, SearchConfig $config ) {
302        $connections = [];
303        foreach ( $clusters as $name ) {
304            $connections[$name] = self::getPool( $config, $name );
305        }
306        return $connections;
307    }
308
309    /**
310     * @return SearchConfig
311     */
312    public function getConfig() {
313        return $this->config;
314    }
315}