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