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