Manuel:Écrire des scripts de maintenance

This page is a translated version of the page Manual:Writing maintenance scripts and the translation is 58% complete.
Outdated translations are marked like this.

Ceci est un tutoriel pas à pas sur la manière d'écrire un script de maintenance basé sur la classe Maintenance (voir Maintenance.php ) introduite dans la version 1.16 de MediaWiki pour rendre plus simple l'écriture des lignes de commande des scripts de maintenance MediaWiki.

Squelette de code

Nous suivrons l'exemple d'un script de maintenance helloWorld.php qui ne fait qu'afficher « Hello, World ». Ce programme contient le code minimal nécessaire (voir aussi En-têtes des droits d'auteur):

Le programme ci-dessous imprime « Hello, World! ».

La manière d'appeler les scripts de maintenance a changé en 2023 avec MediaWiki 1.40, avec le nouveau run.php utilisé pour exécuter tous les scripts de maintenance, plutôt que de les appeler directement par leur nom de fichier (bien que ce cas reste encore supporté). Ce tutoriel couvre les deux méthodes et note les différences entre les systèmes.

Noyau MediaWiki

Commande
$ ./maintenance/run HelloWorld
Hello, World!
Nom de fichier
maintenance/HelloWorld.php
Code
<?php

require_once __DIR__ . '/Maintenance.php';

/**
 * Brève description sur une ligne de Hello world.
 *
 * @since 1.17
 * @ingroup Maintenance
 */
class HelloWorld extends Maintenance {
	public function execute() {
		$this->output( "Hello, World!\n" );
	}
}

$maintClass = HelloWorld::class;
require_once RUN_MAINTENANCE_IF_MAIN;

Extension MediaWiki

Commande
$ ./maintenance/run MyExtension:HelloWorld
Hello, World!
Nom de fichier
extensions/MyExtension/maintenance/HelloWorld.php
Code
<?php

namespace MediaWiki\Extension\MyExtension\Maintenance;

use Maintenance;

$IP = getenv( 'MW_INSTALL_PATH' );
if ( $IP === false ) {
	$IP = __DIR__ . '/../../..';
}
require_once "$IP/maintenance/Maintenance.php";

/**
 * Brève description sur une ligne de Hello world.
 */
class HelloWorld extends Maintenance {
	public function __construct() {
		parent::__construct();
		$this->requireExtension( 'Extension' );
	}

	public function execute() {
		$this->output( "Hello, World!\n" );
	}
}

$maintClass = HelloWorld::class;
require_once RUN_MAINTENANCE_IF_MAIN;

Explication du squelette de code

require_once __DIR__ . "/Maintenance.php";

Nous incluons Maintenance.php. Cela définit class Maintenance qui contient les bases de tous les scripts de maintenance, y compris les facilités pour analyser syntaxiquement les arguments de la ligne de commande, la lecture des valeurs d'entrée sur la console, la connexion à une base de données, etc.

class HelloWorld extends Maintenance {
}

Nous déclarons notre sous-classe Maintenance.

$maintClass = HelloWorld::class;
require_once RUN_MAINTENANCE_IF_MAIN;

Nous demandons à la classe Maintenance d'exécuter le script en utilisant notre classe HelloWorld, seulement si l'exécution a lieu à partir de la ligne de commande.

En interne, RUN_MAINTENANCE_IF_MAIN charge un autre fichier doMaintenance.php qui charge automatiquement les classes et les paramètres MediaWiki, puis exécute notre méthode execute()

	public function execute() {
	}

La méthode execute() est le point d'entrée des scripts de maintenance et c'est là que sera codée la logique principale de votre script. Evitez d'exécuter du code à partir du constructeur.

Lorsque notre programme sera lancé depuis la ligne de commande, l'environnement de maintenance du noyau prendra en charge l'initialisation du noyau MediaWiki, celui de la configuration, etc, puis il invoquera cette méthode.

Commande Help

Une des fonctionnalités incluses dont bénéficient tous les scripts de maintenance est l'option --help. Le code d'exemple ci-dessus produira la page d'aide suivante :

$ php helloWorld.php --help

Usage: php helloWorld.php […]

Generic maintenance parameters:
    --help (-h): Afficher ce message d'aide
    --quiet (-q): Indique s'il faut supprimer la sortie qui n'est pas en erreur
    --conf: Emplacement de LocalSettings.php, sinon la valeur par défaut
    --wiki: Pour spécifier l'ID du wiki
    --server: Le protocole et le nom du serveur à utiliser dans l' URL
    --profiler: Format de sortie du profileur (généralement "texte")
…

Ajouter une description

« Mais quel est le but de ce script de maintenance ? » Je vous entends déjà me le demander.

Il est possible de mettre une description au début de la sortie du « --help » en utilisant la méthode addDescription dans notre constructeur :

	public function __construct() {
		parent::__construct();

		$this->addDescription( 'Say hello.' );
	}

Maintenant la sortie nous donne la description :

$ php helloWorld.php --help

Say hello.

Usage: php helloWorld.php [--help]
…

Analyse des options et des arguments

Saluer le monde est très sympathique et très bien, mais nous voulons être capable de saluer également des individus.

Pour ajouter une option de commande en ligne, ajoutez un constructeur à la class HelloWorld qui appelle addOption() de Maintenance et qui met à jour la méthode execute() pour utiliser la nouvelle option. Les paramètres de addOption() sont $name, $description, $required = false, $withArg = false, $shortName = false, donc :

	public function __construct() {
		parent::__construct();

		$this->addDescription( 'Say hello.' );
		$this->addOption( 'name', 'Who to say Hello to', false, true );
	}

	public function execute() {
		$name = $this->getOption( 'name', 'World' );
		$this->output( "Hello, $name!" );
	}

Cette fois, à l'exécution, la sortie du script helloWorld.php change en fonction des arguments fournis :

$ php helloWorld.php
Hello, World!
$ php helloWorld.php --name=Mark
Hello, Mark!
$ php helloWorld.php --help

Say hello.

Usage: php helloWorld.php […]
…
Script specific parameters:
    --name: Who to say Hello to

Extensions

Version de MediaWiki :
1.28
Gerrit change 301709

Si votre script de maintenance concerne une extension, alors vous devez ajouter la condition que celle-ci soit installée :

	public function __construct() {
		parent::__construct();
		$this->addOption( 'name', 'Who to say Hello to' );

		$this->requireExtension( 'FooBar' );
	}

Cela fournit un message d'erreur utile quand l'extension n'est pas activée. Par exemple pendant le développement local, une extension particulière peut ne pas avoir encore été activée dans le fichier LocalSettings.php, ou si vous avez une ferme de wikis, l'extension peut n'être active que sur un sous-ensemble de wikis.

Notez-bien qu'aucun code ne peut être exécuté ailleurs que via la méthode execute(). Toute tentative pour accéder aux services du noyau MediaWiki, aux classes, aux fonctions, ou encore pour appeler le code de votre propre extension avant ceci, sortira en erreur ou sera non fiable et non pris en charge (comme étant hors de la déclaration de la classe, ou dans le constructeur).

Profilage

Les scripts de maintenance ont une option --profiler, qui peut être utilisée pour suivre l'exécution du code pendant une action de page et renvoyer le pourcentage du code total exécuté dépensé dans une fonction donnée. Voir Manuel:Profilage .

Ecrire des tests

Il est recommandé d'écrire des tests pour vos scripts de maintenance, comme pour toute autre classe. Voir le guide des scripts de maintenance pour l'aide et les exemples.

Long-Running Scripts

If your script is designed to operate on a large number of things (e.g. all or potentially many pages or revisions), it is recommended to apply the following best practices. Keep in mind that "all revisions" can mean billions of entries and months of runtime on large sites like English Wikipedia.

Batching

When processing a large number of items, it is best to do so in batches of relatively small size - typically between 100 or 1000, depending on the time needed to process each entry.

Batching must be based on a database field (or combination of fields) covered by a unique database index, typically a primary key. Using page_id or rev_id are typical examples.

Batching is achieved by structuring your script into an inner loop and an outer loop: The inner loop processes a batch of IDs, and the outer loop queries the database to get the next batch of IDs. The outer loop needs to keep track of where the last batch ended, and the next batch should start.

For a script that operates on pages, it would look something like this:

$batchStart = 0;

// We assume that processPages() will write to the database, so we use the primary DB.
$dbw = $this->getPrimaryDB();

while ( true ) {
    $pageIds = $dbw->newSelectQueryBuilder()
			->select( [ 'page_id' ] )
			->from( 'page' )
			->where( ... ) // the relevant condition for your use use
			->where( $dbw->expr( 'page_id', '>=', $batchStart ) ) // batch condition
			->oderBy( 'page_id' ) // go over pages in ascending order of page IDs
			->limit( $this->getBatchSize() ) // don't forget setBatchSize() in the constructor
			->caller( __METHOD__ )
			->fetchFieldValues();

    if ( !$pageIds ) {
        // no more pages found, we are done
        break;
    }
    
    // Do something for each page
    foreach ( $pageIds as $id ) {
        $this->updatePage( $dbw, $id ); 
    }
    
    // Now commit any changes to the database.
    // This will automatically call waitForReplication(), to avoid replication lag.
    $this->commitTransaction( $dbw, __METHOD__ );
    
    // The next batch should start at the ID following the last ID in the batch
    $batchStart = end( $pageIds ) +1;
}
The Maintenance base class provides some utility functions for managing batch size. You can call setBatchSize() in the constructor of your maintenance script class to set the default batch size. This will automatically add a --batch-size command line option, and you can use getBatchSize() to get the batch size to use in your queries.

Recoverability

Long running scripts may be interrupted for a number of reasons - a database server being shut down, the server running the script getting rebooted, exception because of data corruption, programming errors, etc. Because of this, it is important to provide a way to re-start the script's operation somewhere close to where it was interrupted.

Two things are needed for this: outputting the start of each batch, and providing a command line option for starting at a specific position.

Assuming we have defined a command line option called --start-from, we can adjust the code above as follows:

$batchStart = $this->getOption( 'start-from', 0 );

//...

while ( true ) {
    //...

    // Do something for each page
    $this->output( "Processing batch starting at $batchStart...\n" );
    foreach ( $pageIds as $id ) {
        //...
    }
    
    //...
}

$this->output( "Done.\n" );

This way, if the script gets interrupted, we can easily re-start it:

$ maintenance/run myscript
Processing batch starting at 0...
Processing batch starting at 1022...
Processing batch starting at 2706...
Processing batch starting at 3830...
^C

$ maintenance/run myscript --start-from 3830
Processing batch starting at 3830...
Processing batch starting at 5089...
Processing batch starting at 6263...
Done.

Note that this assumes that the script's operation is idempotent - that is, it doesn't matter if a few pages get processed multiple times.

To make sure we know where the script left off even when the server that is running the script is rebooted, it is recommended to pipe the script's output to a log file. A convenient way to do this is the tee command. Also, to avoid interruption and loss of information when your SSH connection to the server fails, remember to run the script through screen or tmux.

Sharding

If a script performs slow operations for each entries, it can be useful to run multiple instances of the script in parallel, using sharding.

Sharding should not be used for scripts that perform database updates at high speed without significant delay between updates. All updates go to the same primary DB server, and hammering it from multiple instances of the script will not make it go faster. It may even slow down the process, because auf the increased need to manage locks and maintain transaction isolation.

The simplest way to implement sharding is based on the modulo of the ID used for patching: We define a sharding factor (N) and a shard number (S) on the command line, we can define the shard condition as ID mod N = S, with 0 <= S < N. All instances of the script that are to run parallel use the same sharding factor N, and a different shard number S. Each script instance will only process IDs that match its shard condition.

The shard condition could be integrated into the database query, but that may interfere with the efficient use of indexes. Instead, we will implement sharding in code, and just multiply the batch factory accordingly. We can adjust the above code as follows:

$batchStart = $this->getOption( 'start-from', 0 );
$shardingFactor = $this->getOption( 'sharding-factor', 1 );
$shardNumber = $this->getOption( 'shard-number', 0 );

// ...

if ( $shardNumber >= $shardingFactor ) {
    $this->fatalError( "Shard number ($shardNumber) must be less than the sharding factor ($shardingFactor)!\n" );
}

if ( $shardingFactor > 1 ) {
    $this->output( "Starting run for shard $shardNumber/$shardingFactor\n" );
}

while ( true ) {
    $pageIds = $dbw->newSelectQueryBuilder()
            //...
            // multiply the batch size by the sharding factor
			->limit( $this->getBatchSize() * $shardingFactor )
			->caller( __METHOD__ )
			->fetchFieldValues();

    // ...

    // Do something for each page
    foreach ( $pageIds as $id ) {
        // process only the IDs matching the shard condition!
        if ( $id % $shardingFactor !== $shardNumber ) {
            continue;
        }
    
        $this->updatePage( $dbw, $id ); 
    }
    
    // ...
}

We can then start multiple instances of the script, operating on different shards

$ maintenance/run myscript --sharding-factor 3 --shard-number 0
Starting run for shard 0/3
Processing batch starting at 0...
^A1

$ maintenance/run myscript --sharding-factor 3 --shard-number 1
Starting run for shard 1/3
Processing batch starting at 0...
^A2

$ maintenance/run myscript --sharding-factor 3 --shard-number 2
Starting run for shard 2/3
Processing batch starting at 0...