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