Handbuch:Wartungsskripte schreiben
Dies ist eine Schritt-für-Schritt-Anleitung zum Schreiben eines Wartungsskripts basierend auf der Maintenance
-Klasse (siehe Maintenance.php ), die in MediaWiki 1.16 eingeführt wurde, um das Schreiben von MediaWiki-Wartungsskripten für die Befehlszeile zu vereinfachen.
Leitfaden
Wir werden ein helloWorld.php
-Wartungsskript durchgehen, das einfach "Hello, World" ausgibt. Dieses Programm enthält das Minimum an benötigtem Code (siehe auch Copyright-Kopfzeilen)
Das folgende Beispielprogramm gibt „Hello, World!“ aus.
MediaWiki Code
- Befehl
$ ./maintenance/run HelloWorld Hello, World!
- Dateiname
maintenance/HelloWorld.php
- Code
<?php require_once __DIR__ . '/Maintenance.php'; /** * Kurze einzeilige Beschreibung von 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;
MediaWiki-Erweiterung
- Befehl
$ ./maintenance/run MyExtension:HelloWorld Hello, World!
- Dateiname
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"; /** * Kurze einzeilige Beschreibung von 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;
Textbausteine erklärt
require_once __DIR__ . "/Maintenance.php";
Wir fügen Maintenance.php
ein. Dies definiert class Maintenance
, das die Grundlage für alle Wartungsskripte bildet, einschließlich der Möglichkeiten, Befehlszeilenargumente zu analysieren, Eingaben auf der Konsole zu lesen, sich mit einer Datenbank zu verbinden, usw.
class HelloWorld extends Maintenance {
}
Wir deklarieren unsere Unterklasse Maintenance.
$maintClass = HelloWorld::class;
require_once RUN_MAINTENANCE_IF_MAIN;
Weist die Klasse Maintenance an, das Skript mit unserer Klasse HelloWorld
auszuführen, aber nur, wenn es von der Kommandozeile aus ausgeführt wird.
Intern lädt RUN_MAINTENANCE_IF_MAIN
eine weitere Datei doMaintenance.php, welche die MediaWiki-Klassen und die Konfiguration automatisch lädt und dann unsere Funktion execute()
ausführt.
public function execute() {
}
Die Funktion execute()
ist der Einstiegspunkt für Wartungsskripte und enthält die Hauptlogik Ihres Skripts. Vermeiden Sie die Ausführung von Code aus dem Baukasten.
Wenn unser Programm von der Kommandozeile aus gestartet wird, kümmert sich das Wartung-Framework um die Initialisierung des MediaWiki-Kerns, der Konfiguration usw. und ruft dann diese Funktion auf.
Hilfe aufrufen
Eine der eingebauten Funktionen, über die alle Wartungsskripte verfügen, ist eine --help
-Option. Das obige Beispiel Boilerplate würde die folgende Hilfeseite erzeugen:
$ php helloWorld.php --help Usage: php helloWorld.php […] Generic maintenance parameters: --help (-h): Diese Hilfemeldung anzeigen --quiet (-q): Ob Nicht-Fehler-Ausgaben unterdrückt werden sollen --conf: Ort der LocalSettings.php, wenn nicht Standard --wiki: Zum Festlegen der Wiki ID --server: Das Protokoll und der Servername, die in der URL verwendet werden sollen --profiler: Profiler-Ausgabeformat (normalerweise "Text") …
Eine Beschreibung hinzufügen
"Aber, für was ist dieses Wartungsskript?" Ich kann dich fragen hören.
Wir können eine Beschreibung oben in die Ausgabe von "--help
" einfügen, indem wir die Methode addDescription
in unserem Konstruktor verwenden:
public function __construct() {
parent::__construct();
$this->addDescription( 'Say hello.' );
}
Die Ausgabe gibt uns nun die Beschreibung:
$ php helloWorld.php --help Say hello. Usage: php helloWorld.php [--help] …
Parsing von Optionen und Argumenten
Die Welt zu grüßen ist schön und gut, aber wir wollen auch Einzelpersonen begrüßen können.
Um eine Kommandozeilenoption hinzuzufügen, fügst du einen Konstruktor zu class HelloWorld
hinzu, der Maintenance
's addOption()
aufruft und aktualisierst die Methode execute()
, um die neue Option zu verwenden.
Die Parameter von addOption()
sind $name, $description, $required = false, $withArg = false, $shortName = false
, also:
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!" );
}
Diesmal ändert sich die Ausgabe des helloWorld.php
-Skripts bei der Ausführung je nach dem bereitgestellten Argument:
$ 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
Erweiterungen
MediaWiki Version: | ≥ 1.28 Gerrit change 301709 |
Wenn dein Wartungsskript für eine Erweiterung gedacht ist, solltest du eine Anforderung hinzufügen, dass die Erweiterung installiert ist:
public function __construct() {
parent::__construct();
$this->addOption( 'name', 'Who to say Hello to' );
$this->requireExtension( 'FooBar' );
}
Dies liefert eine nützliche Fehlermeldung, wenn die Erweiterung nicht aktiviert ist. Zum Beispiel könnte während der lokalen Entwicklung eine bestimmte Erweiterung noch nicht in LocalSettings.php aktiviert sein oder beim Betrieb einer Wikifarm könnte eine Erweiterung auf einer Teilmenge von Wikis aktiviert sein.
Beachten Sie, dass kein Code anders als über die Funktion execute()
ausgeführt werden darf.
Der Versuch, MediaWiki-Kerndienste, -Klassen oder -Funktionen aufzurufen oder Ihren eigenen Erweiterungscode vorher aufzurufen, führt zu Fehlern oder ist unzuverlässig und wird nicht unterstützt (z.B. außerhalb der Klassendeklaration oder im Konstruktor).
Profiling
Wartungsskripte unterstützen eine --profiler
Option, mit der die Codeausführung während einer Seitenaktion verfolgt und der Prozentsatz der gesamten Codeausführung gemeldet werden kann, der auf eine bestimmte Funktion entfällt.
Siehe Manual:Profiling .
Tests erstellen
Es wird empfohlen, für deine Wartungsskripte Tests zu schreiben, wie für jede andere Klasse auch. Hilfe und Beispiele findest du im Handbuch Wartungsskripte.
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;
}
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.
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.
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...