Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
12.75% |
13 / 102 |
|
4.76% |
1 / 21 |
CRAP | |
0.00% |
0 / 1 |
| Maintenance | |
12.87% |
13 / 101 |
|
4.76% |
1 / 21 |
1208.76 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| finalSetup | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| setupUserTest | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| createChild | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| getConnection | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| getSearchConfig | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
| getMetaStore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| decideCluster | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
| loadSpecialVars | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| done | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| output | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| outputIndented | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| error | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| disablePoolCountersAndLogging | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| maybeCreateMetastore | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| requireCirrusReady | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| requireManagedCluster | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getBackCompatOption | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
| unwrap | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| safeCount | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| safeRefresh | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace CirrusSearch\Maintenance; |
| 4 | |
| 5 | use CirrusSearch\Connection; |
| 6 | use CirrusSearch\MetaStore\MetaStoreIndex; |
| 7 | use CirrusSearch\SearchConfig; |
| 8 | use CirrusSearch\UserTestingEngine; |
| 9 | use Elastica\Index; |
| 10 | use MediaWiki\Maintenance\Maintenance as MWMaintenance; |
| 11 | use MediaWiki\MediaWikiServices; |
| 12 | use MediaWiki\Settings\SettingsBuilder; |
| 13 | use MediaWiki\Status\Status; |
| 14 | use RuntimeException; |
| 15 | use StatusValue; |
| 16 | |
| 17 | // Maintenance class is loaded before autoload, so we need to pull the interface |
| 18 | require_once __DIR__ . '/Printer.php'; |
| 19 | |
| 20 | /** |
| 21 | * Cirrus helpful extensions to Maintenance. |
| 22 | * |
| 23 | * @license GPL-2.0-or-later |
| 24 | */ |
| 25 | abstract class Maintenance extends MWMaintenance implements Printer { |
| 26 | /** |
| 27 | * @var string The string to indent output with |
| 28 | */ |
| 29 | protected static $indent = null; |
| 30 | |
| 31 | /** |
| 32 | * @var Connection|null |
| 33 | */ |
| 34 | private $connection; |
| 35 | |
| 36 | /** |
| 37 | * @var SearchConfig |
| 38 | */ |
| 39 | protected $searchConfig; |
| 40 | |
| 41 | public function __construct( ?SearchConfig $searchConfig = null ) { |
| 42 | parent::__construct(); |
| 43 | if ( $searchConfig !== null ) { |
| 44 | $this->searchConfig = $searchConfig; |
| 45 | } |
| 46 | $this->addOption( 'cluster', 'Perform all actions on the specified elasticsearch cluster', |
| 47 | false, true ); |
| 48 | $this->addOption( 'userTestTrigger', 'Use config var and profiles set in the user testing ' . |
| 49 | 'framework, e.g. --userTestTrigger=trigger', false, true ); |
| 50 | $this->requireExtension( 'CirrusSearch' ); |
| 51 | } |
| 52 | |
| 53 | public function finalSetup( SettingsBuilder $settingsBuilder ) { |
| 54 | parent::finalSetup( $settingsBuilder ); |
| 55 | |
| 56 | if ( $this->hasOption( 'userTestTrigger' ) ) { |
| 57 | $this->setupUserTest(); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Setup config vars with the UserTest framework |
| 63 | */ |
| 64 | private function setupUserTest() { |
| 65 | // Configure the UserTesting framework |
| 66 | // Useful in case an index needs to be built with a |
| 67 | // test config that is not meant to be the default. |
| 68 | // This is realistically only usefull to test across |
| 69 | // multiple clusters. |
| 70 | // Perhaps setting $wgCirrusSearchIndexBaseName to an |
| 71 | // alternate value would testing on the same cluster |
| 72 | // but this index would not receive updates. |
| 73 | $trigger = $this->getOption( 'userTestTrigger' ); |
| 74 | $engine = UserTestingEngine::fromConfig( $this->getConfig() ); |
| 75 | $status = $engine->decideTestByTrigger( $trigger ); |
| 76 | if ( !$status->isActive() ) { |
| 77 | $this->fatalError( "Unknown user test trigger: $trigger" ); |
| 78 | } |
| 79 | $engine->activateTest( $status ); |
| 80 | } |
| 81 | |
| 82 | /** @inheritDoc */ |
| 83 | public function createChild( string $maintClass, ?string $classFile = null ): MWMaintenance { |
| 84 | $child = parent::createChild( $maintClass, $classFile ); |
| 85 | if ( $child instanceof self ) { |
| 86 | $child->searchConfig = $this->searchConfig; |
| 87 | } |
| 88 | |
| 89 | return $child; |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * @param string|null $cluster |
| 94 | * @return Connection |
| 95 | */ |
| 96 | public function getConnection( $cluster = null ) { |
| 97 | if ( $cluster ) { |
| 98 | $connection = Connection::getPool( $this->getSearchConfig(), $cluster ); |
| 99 | } else { |
| 100 | if ( $this->connection === null ) { |
| 101 | $cluster = $this->decideCluster(); |
| 102 | $this->connection = Connection::getPool( $this->getSearchConfig(), $cluster ); |
| 103 | } |
| 104 | $connection = $this->connection; |
| 105 | } |
| 106 | |
| 107 | $connection->setTimeout( $this->getSearchConfig()->get( 'CirrusSearchMaintenanceTimeout' ) ); |
| 108 | |
| 109 | return $connection; |
| 110 | } |
| 111 | |
| 112 | public function getSearchConfig(): SearchConfig { |
| 113 | if ( $this->searchConfig == null ) { |
| 114 | $this->searchConfig = MediaWikiServices::getInstance() |
| 115 | ->getConfigFactory() |
| 116 | ->makeConfig( 'CirrusSearch' ); |
| 117 | if ( !$this->searchConfig instanceof SearchConfig ) { |
| 118 | // We shouldn't ever get here ... but the makeConfig type signature returns the parent |
| 119 | // class of SearchConfig so just being extra careful... |
| 120 | throw new \RuntimeException( 'Expected instanceof CirrusSearch\SearchConfig, but received ' . |
| 121 | get_class( $this->searchConfig ) ); |
| 122 | } |
| 123 | } |
| 124 | return $this->searchConfig; |
| 125 | } |
| 126 | |
| 127 | public function getMetaStore( ?Connection $conn = null ): MetaStoreIndex { |
| 128 | return new MetaStoreIndex( $conn ?? $this->getConnection(), $this, $this->getSearchConfig() ); |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * @return string|null |
| 133 | */ |
| 134 | protected function decideCluster() { |
| 135 | $config = $this->getSearchConfig(); |
| 136 | $assignment = $config->getClusterAssignment(); |
| 137 | |
| 138 | $cluster = $this->getOption( 'cluster', null ); |
| 139 | if ( $cluster !== null && $config->has( 'CirrusSearchServers' ) ) { |
| 140 | $this->fatalError( 'Not configured for cluster operations.' ); |
| 141 | } |
| 142 | if ( $cluster === null ) { |
| 143 | $cluster = $assignment->getSearchCluster(); |
| 144 | } |
| 145 | if ( $this->requireManagedCluster() && !$assignment->canManageCluster( $cluster ) ) { |
| 146 | $this->fatalError( |
| 147 | "Named cluster ($cluster) is not configured for maintenance operations. " . |
| 148 | "Allowed clusters: " . implode( ", ", $assignment->getManagedClusters() ) |
| 149 | ); |
| 150 | } |
| 151 | return $cluster; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Execute a callback function at the end of initialisation |
| 156 | */ |
| 157 | public function loadSpecialVars() { |
| 158 | parent::loadSpecialVars(); |
| 159 | if ( self::$indent === null ) { |
| 160 | // First script gets no indentation |
| 161 | self::$indent = ''; |
| 162 | } else { |
| 163 | // Others get one tab beyond the last |
| 164 | self::$indent .= "\t"; |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Call to signal that execution of this maintenance script is complete so |
| 170 | * the next one gets the right indentation. |
| 171 | */ |
| 172 | public function done() { |
| 173 | self::$indent = substr( self::$indent, 1 ); |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * @param string $message |
| 178 | * @param string|null $channel |
| 179 | */ |
| 180 | public function output( $message, $channel = null ) { |
| 181 | parent::output( $message ); |
| 182 | } |
| 183 | |
| 184 | /** @inheritDoc */ |
| 185 | public function outputIndented( $message ) { |
| 186 | $this->output( self::$indent . $message ); |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * @param string $err |
| 191 | * @param int $die deprecated, do not use |
| 192 | */ |
| 193 | public function error( $err, $die = 0 ) { |
| 194 | parent::error( $err, $die ); |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | * Disable all pool counters and cirrus query logs. |
| 199 | * Only useful for maint scripts |
| 200 | * |
| 201 | * Ideally this method could be run in the constructor |
| 202 | * but apparently globals are reset just before the |
| 203 | * call to execute() |
| 204 | */ |
| 205 | protected function disablePoolCountersAndLogging() { |
| 206 | global $wgPoolCounterConf, $wgCirrusSearchLogElasticRequests; |
| 207 | |
| 208 | // Make sure we don't flood the pool counter |
| 209 | unset( $wgPoolCounterConf['CirrusSearch-Search'] ); |
| 210 | |
| 211 | // Don't skew the dashboards by logging these requests to |
| 212 | // the global request log. |
| 213 | $wgCirrusSearchLogElasticRequests = false; |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Create metastore only if the alias does not already exist |
| 218 | * @return MetaStoreIndex |
| 219 | */ |
| 220 | protected function maybeCreateMetastore() { |
| 221 | $metastore = new MetaStoreIndex( |
| 222 | $this->getConnection(), |
| 223 | $this, |
| 224 | $this->getSearchConfig() ); |
| 225 | $status = $metastore->createIfNecessary(); |
| 226 | $this->unwrap( $status ); |
| 227 | return $metastore; |
| 228 | } |
| 229 | |
| 230 | protected function requireCirrusReady() { |
| 231 | // If the version does not exist it's certainly because nothing has been indexed. |
| 232 | if ( !$this->getMetaStore()->cirrusReady() ) { |
| 233 | throw new RuntimeException( |
| 234 | "Cirrus meta store does not exist, you must index your data first" |
| 235 | ); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * @return bool True if this script only operates on clusters specified |
| 241 | * in CirrusSearchManagedClusters. Can be set to false for read-only |
| 242 | * scripts that don't care where they read from. |
| 243 | */ |
| 244 | protected function requireManagedCluster() { |
| 245 | return true; |
| 246 | } |
| 247 | |
| 248 | /** |
| 249 | * Provides support for backward compatible CLI options |
| 250 | * |
| 251 | * Requires either one or neither of the two options to be provided. |
| 252 | * |
| 253 | * @param string $current The current option to request |
| 254 | * @param string $bc The old option to provide BC support for |
| 255 | * @param bool $required True if the option must be provided. When false and no option |
| 256 | * is provided null is returned. |
| 257 | * @return mixed |
| 258 | */ |
| 259 | protected function getBackCompatOption( string $current, string $bc, bool $required = true ) { |
| 260 | if ( $this->hasOption( $current ) && $this->hasOption( $bc ) ) { |
| 261 | $this->error( "\nERROR: --$current cannot be provided with --$bc" ); |
| 262 | $this->maybeHelp( true ); |
| 263 | } elseif ( $this->hasOption( $current ) ) { |
| 264 | return $this->getOption( $current ); |
| 265 | } elseif ( $this->hasOption( $bc ) ) { |
| 266 | return $this->getOption( $bc ); |
| 267 | } elseif ( $required ) { |
| 268 | $this->error( "\nERROR: Param $current is required" ); |
| 269 | $this->maybeHelp( true ); |
| 270 | } else { |
| 271 | return null; |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * Helper method for Status returning methods, such as via ConfigUtils |
| 277 | * |
| 278 | * @template T |
| 279 | * @param Status<T> $status |
| 280 | * @return T |
| 281 | */ |
| 282 | protected function unwrap( Status $status ) { |
| 283 | if ( !$status->isGood() ) { |
| 284 | $this->fatalError( (string)$status ); |
| 285 | } |
| 286 | return $status->getValue(); |
| 287 | } |
| 288 | |
| 289 | /** |
| 290 | * Count the number of doc in this index. |
| 291 | * @param Index $index |
| 292 | * @return int |
| 293 | */ |
| 294 | protected function safeCount( Index $index, int $attempts = 3 ): int { |
| 295 | return ConfigUtils::safeCountOrFail( |
| 296 | $index, |
| 297 | function ( StatusValue $error ): never { |
| 298 | $this->fatalError( $error ); |
| 299 | }, |
| 300 | $attempts |
| 301 | ); |
| 302 | } |
| 303 | |
| 304 | /** |
| 305 | * Refresh the index. |
| 306 | * @param Index $index |
| 307 | * @param int $attempts |
| 308 | * @return void |
| 309 | */ |
| 310 | protected function safeRefresh( Index $index, int $attempts = 3 ): void { |
| 311 | ConfigUtils::safeRefreshOrFail( |
| 312 | $index, |
| 313 | function ( StatusValue $error ): never { |
| 314 | $this->fatalError( $error ); |
| 315 | }, |
| 316 | $attempts |
| 317 | ); |
| 318 | } |
| 319 | |
| 320 | } |