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