Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
14.44% covered (danger)
14.44%
13 / 90
5.56% covered (danger)
5.56%
1 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Maintenance
14.61% covered (danger)
14.61%
13 / 89
5.56% covered (danger)
5.56%
1 / 18
937.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 finalSetup
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setupUserTest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 createChild
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getConnection
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getSearchConfig
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 decideCluster
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 loadSpecialVars
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 done
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 output
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 outputIndented
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 error
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disablePoolCountersAndLogging
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 requireManagedCluster
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBackCompatOption
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 unwrap
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 safeCount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 safeRefresh
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch\Maintenance;
4
5use CirrusSearch\CirrusConfigNames;
6use CirrusSearch\CirrusSearch;
7use CirrusSearch\Connection;
8use CirrusSearch\PoolCounterKey;
9use CirrusSearch\SearchConfig;
10use CirrusSearch\UserTestingEngine;
11use Elastica\Index;
12use MediaWiki\Maintenance\Maintenance as MWMaintenance;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Settings\SettingsBuilder;
15use MediaWiki\Status\Status;
16use RuntimeException;
17use StatusValue;
18
19// Maintenance class is loaded before autoload, so we need to pull the interface
20require_once __DIR__ . '/Printer.php';
21
22/**
23 * Cirrus helpful extensions to Maintenance.
24 *
25 * @license GPL-2.0-or-later
26 */
27abstract 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}