Manuel:Écrire des scripts de maintenance
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 du 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! ».
Noyau MediaWiki
- Commande
$ php maintenance/run.php HelloWorld Hello, World!
- Nom de fichier
maintenance/HelloWorld.php
- Code
MediaWiki 1.39 and before, required to run the script directly MediaWiki 1.40+, only compatible with run.php <?php // @codeCoverageIgnoreStart require_once __DIR__ . '/Maintenance.php'; // @codeCoverageIgnoreEnd /** * 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" ); } } // @codeCoverageIgnoreStart $maintClass = HelloWorld::class; require_once RUN_MAINTENANCE_IF_MAIN; // @codeCoverageIgnoreEnd
<?php /** * Brief oneline description of Hello world. * * @since 1.40 * @ingroup Maintenance */ class HelloWorld extends Maintenance { public function execute() { $this->output( "Hello, World!\n" ); } } $maintClass = HelloWorld::class;
Extension MediaWiki
- Commande
$ php maintenance/run.php MyExtension:HelloWorld Hello, World!
- Nom de fichier
extensions/MyExtension/maintenance/HelloWorld.php- Code
MediaWiki 1.39 and before, required to run the script directly MediaWiki 1.40+, only compatible with run.php <?php namespace MediaWiki\Extension\MyExtension\Maintenance; use Maintenance; // @codeCoverageIgnoreStart $IP = getenv( 'MW_INSTALL_PATH' ); if ( $IP === false ) { $IP = __DIR__ . '/../../..'; } require_once "$IP/maintenance/Maintenance.php"; // @codeCoverageIgnoreEnd /** * 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" ); } } // @codeCoverageIgnoreStart $maintClass = HelloWorld::class; require_once RUN_MAINTENANCE_IF_MAIN; // @codeCoverageIgnoreEnd
<?php namespace MediaWiki\Extension\MyExtension\Maintenance; use Maintenance; /** * Brief oneline description of 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;
Explication du squelette de code
| MediaWiki 1.39 and before | MediaWiki 1.40+ |
|---|---|
// @codeCoverageIgnoreStart
require_once __DIR__ . '/Maintenance.php';
// @codeCoverageIgnoreEnd
// @codeCoverageIgnoreStart
$IP = getenv( 'MW_INSTALL_PATH' );
if ( $IP === false ) {
$IP = __DIR__ . '/../../..';
}
require_once "$IP/maintenance/Maintenance.php";
// @codeCoverageIgnoreEnd
|
No longer necessary, unless you need to support running the script directly rather than through run.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.
Ceci est exclu des tests de couverture parce que :
class HelloWorld extends Maintenance {
}
Nous déclarons notre sous-classe Maintenance.
| MediaWiki 1.39 and before | MediaWiki 1.40+ |
|---|---|
// @codeCoverageIgnoreStart
$maintClass = HelloWorld::class;
require_once RUN_MAINTENANCE_IF_MAIN;
// @codeCoverageIgnoreEnd
|
// @codeCoverageIgnoreStart
$maintClass = HelloWorld::class;
// @codeCoverageIgnoreEnd
|
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()
Ceci est également exclu de la couverture de test car il s'agit du code d'un squelette et il ne peut pas être marqué comme testé par PHPUnit.
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, si différent de 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.
Scripts à exécution longue
Si votre script est conçu pour fonctionner sur un grand nombre d'objets (c'est à dir, toutes ou potentiellement plusieurs pages ou révisions), il est recommandé d'appliquer les meilleures pratiques suivantes. Gardez à l'esprit que toutes les révisions peut signifier des milliards d'entrées et des mois d'exécution sur les grands sites tels que la Wikipedia anglaise.
Traiter par lots
Lorsqu'on traite un grand nombre d'articles, il est préférable de le faire par lots de taille relativement petite - généralement entre 100 et 1000, selon le temps nécessaire pour traiter chaque entrée.
Le traitement par lot doit être basé sur un champ de la base de données (ou une combinaison de champs) couvert par un index unique de base de données unique, généralement une clé primaire.
L'utilisation de page_id et rev_id sont des exemples typiques.
Le traitement par lot réalisé en structurant votre script en une boucle interne et une boucle externe: la boucle interne traite un lot d'ID, et la boucle externe interroge la base de données pour obtenir le prochain lot d'identifiants. La boucle externe doit connaître l'endroit où s'est terminé le dernier lot, et là où le prochain lot doit commencer.
Pour un script qui fonctionne avec des pages, cela ressemblerait à ceci :
$batchStart = 0;
// Nous supposons que processPages () écrira dans la base de données, c'est pourquoi nous utilisons la base de données primaire.
$dbw = $this->getPrimaryDB();
while ( true ) {
$pageIds = $dbw->newSelectQueryBuilder()
->select( [ 'page_id' ] )
->from( 'page' )
->where( ... ) // la condition pertinente dans votre cas utilise
->where( $dbw->expr( 'page_id', '>=', $batchStart ) ) // condition du lot
->oderBy( 'page_id' ) // passez sur les pages dans l'ordre croissant des identifiants de page
->limit( $this->getBatchSize() ) // ne pas oublier setBatchSize() dans le constructeur
->caller( __METHOD__ )
->fetchFieldValues();
if ( !$pageIds ) {
// il n'y a plus de pages, nous avons terminé
break;
}
// Faire quelque chose pour chaque page
foreach ( $pageIds as $id ) {
$this->updatePage( $dbw, $id );
}
// Maintenant, validez les modifications faites à la base de données.
// Cela appellera automatiquement waitForReplication(), pour éviter le retard de la réplication.
$this->commitTransaction( $dbw, __METHOD__ );
// Le prochain lot doit commencer par l'identifiant qui suit le dernier identifiant du lot.
$batchStart = end( $pageIds ) +1;
}
setBatchSize() dans le constructeur de votre classe de script de maintenance pour définir la taille par défaut du lot. Cela ajoutera automatiquement une option de ligne de commande --batch-size et vous pouvez utiliser getBatchSize() pour obtenir la taille du lot à utiliser dans vos requêtes.
Récupérabilité
Les scripts dont la durée d'exécution est assez longue, peuvent être interrompus pour plusieurs raisons - un serveur de base de données s'est arrêté, le serveur exécutant le script a été redémarré, une exception due à des données corrompues, des erreurs de programmation, etc. A cause de cela, il est important de fournir un moyen pour redémarrer l'opération du script à un endroit proche de là où il a été interrompu.
Deux choses sont nécessaires pour cela : tracer le début de chaque lot, et fournir une option sur la ligne de commande pour commencer à une position spécifique.
En supposant que nous ayons défini une option de ligne de commande appelée --start-from, nous pouvons ajuster le code ci-dessus comme suit :
$batchStart = $this->getOption( 'start-from', 0 );
//...
while ( true ) {
//...
// Faire quelque chose pour chaque page
$this->output( "Processing batch starting at $batchStart...\n" );
foreach ( $pageIds as $id ) {
//...
}
//...
}
$this->output( "Done.\n" );
De cette façon, si le script est interrompu, nous pouvons facilement le redémarrer :
$ php maintenance/run.php myscript
Processing batch starting at 0...
Processing batch starting at 1022...
Processing batch starting at 2706...
Processing batch starting at 3830...
^C
$ php maintenance/run.php myscript --start-from 3830
Processing batch starting at 3830...
Processing batch starting at 5089...
Processing batch starting at 6263...
Done.
Noter que cela suppose que l'opération du script est idempotente - c'est à dire qu'il n'est pas important si quelques pages sont traitées plusieurs fois.
tee. De plus, pour éviter l'interruption et la perte d'informations lorsque votre connexion SSH avec le serveur échoue, n'oubliez pas d'exécuter le script à travers screen ou tmux.Sharding
Lorsqu'un script réalise des opérations lentes sur chaque entrée, il peut être utile de lancer plusieurs instances en parallèle en utilisant le sharding, c'est à dire le découpage.
La manière la plus simple d'implémenter le sharding est basée sur le modulo utilisé pour patcher :
On définit un facteur de fragmentation (N) et un numéro de fragmentation (S) sur la ligne de commande, avec la possibilité d'avoir une condition de fragmentation de ID mod N = S avec 0 <= S < N.
Toutes les instances du script qui doivent être exécutées en parallèle utilisent le même facteur de fragmentation N, et un numéro de fragmentation différent S.
Chaque instance de script ne traite que les ID qui satisfont sa condition de fragmentation.
La condition de fragmentation peut être intégrée dans la requête à la base de données, mais cela interfère avec l'utilisation efficace des indices. Au lieu de cela, nous allons mettre en œuvre la fragmentation dans le code, et multiplier la fabrique de batch en conséquence. Le code ci-dessus peut s'ajuster ainsi :
$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()
//...
// multiplier la taille du lot par le facteur de sharding
->limit( $this->getBatchSize() * $shardingFactor )
->caller( __METHOD__ )
->fetchFieldValues();
// ...
// Faire quelque traitement pour chaque page
foreach ( $pageIds as $id ) {
// traiter uniquement les identifiants correspondant à la condition du shard !
if ( $id % $shardingFactor !== $shardNumber ) {
continue;
}
$this->updatePage( $dbw, $id );
}
// ...
}
Nous pouvons alors démarrer plusieurs instances du script, fonctionnant sur différents fragments
$ php maintenance/run.php myscript --sharding-factor 3 --shard-number 0
Starting run for shard 0/3
Processing batch starting at 0...
^A1
$ php maintenance/run.php myscript --sharding-factor 3 --shard-number 1
Starting run for shard 1/3
Processing batch starting at 0...
^A2
$ php maintenance/run.php myscript --sharding-factor 3 --shard-number 2
Starting run for shard 2/3
Processing batch starting at 0...