Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
52.63% |
40 / 76 |
|
35.29% |
6 / 17 |
CRAP | |
0.00% |
0 / 1 |
| Connection | |
52.63% |
40 / 76 |
|
35.29% |
6 / 17 |
165.20 | |
0.00% |
0 / 1 |
| getPool | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| clearPool | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| __sleep | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getClusterName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getSettings | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getServerList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getMaxConnectionAttempts | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getArchiveIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getAllIndexSuffixes | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
7 | |||
| extractIndexSuffix | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| getIndexSuffixForNamespace | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
| pickIndexSuffixForNamespaces | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| getAllIndexSuffixesForNamespaces | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| destroyClient | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getClusterConnections | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| getConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace CirrusSearch; |
| 4 | |
| 5 | use Exception; |
| 6 | use LogicException; |
| 7 | use MediaWiki\Extension\Elastica\ElasticaConnection; |
| 8 | use MediaWiki\MainConfigNames; |
| 9 | use MediaWiki\MediaWikiServices; |
| 10 | use 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 | */ |
| 18 | class 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 | } |