Příručka:Psaní údržbářských skriptů

This page is a translated version of the page Manual:Writing maintenance scripts and the translation is 100% complete.

Tato příručka krok za krokem popisuje, jak psát údržbářské skripty, které se spouštějí na straně serveru na příkazovém řádku. Jsou založené na třídě Maintenance (viz Maintenance.php ), která je implementovaná od MediaWiki verze 1.16

Standardní text

Projdeme si skript údržby helloWorld.php, který jednoduše vypíše "Hello, World". Tento skript obsahuje minimální množství kódu potřebného ke spuštění a hlavičku s uvedením autorských práv, která by neměla nikdy chybět (viz také hlavičky autorských práv):

Níže uvedený příklad programu vytiskne "Hello, World!".

Způsob vyvolání údržbových skriptů se změnil v roce 2023 s MediaWiki 1.40, přičemž nový run.php se používá ke spouštění všech údržbových skriptů, místo aby je přímo volal podle názvu souboru (ačkoli druhý zůstává zatím podporován). Tento tutoriál pokrývá obě metody a uvádí, kde existují rozdíly mezi systémy.

Jádro MediaWiki

Příkaz
$ ./maintenance/run HelloWorld
Hello, World!
Název souboru
maintenance/HelloWorld.php
Kód
<?php

require_once __DIR__ . '/Maintenance.php';

/**
 * Stručný jednořádkový popis 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;

Rozšíření MediaWiki

Příkaz
$ ./maintenance/run MyExtension:HelloWorld
Hello, World!
Název souboru
extensions/MyExtension/maintenance/HelloWorld.php
Kód
<?php

namespace MediaWiki\Extension\MyExtension\Maintenance;

use Maintenance;

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

/**
 * Stručný jednořádkový popis 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;

Vysvětlení standardního textu

require_once __DIR__ . "/Maintenance.php";

Nejprve vložíme soubor Maintenance.php. V něm je definována třída class Maintenance, základ všech údržbářských skriptů, která analyzuje argumenty příkazového řádku, čte vstupní data z konzole, zajišťuje připojení k databázi, atd.

class HelloWorld extends Maintenance {
}

Pak deklarujeme vlastní podtřídu, jako rozšíření třídy Maintenance.

$maintClass = HelloWorld::class;
require_once RUN_MAINTENANCE_IF_MAIN;

A tento kód říká třídě Maintenance, aby v případě, že je zavolána na příkazovém řádku, spouštěla skript přes naši třídu HelloWorld.

Interně to funguje tak, že RUN_MAINTENANCE_IF_MAIN natáhne soubor doMaintenance.php a ten zajistí automatické načtení tříd MediaWiki, konfigurace, a následné spuštění naší metody execute().

	public function execute() {
	}

Metoda execute() je vstupním bodem údržbářkých skriptů. Do ní umístěte hlavní logika vašeho skriptu a vyhněte se volání jakéhokoli kódu z konstruktoru.

Když je náš program spouštěn z příkazového řádku, základní framework údržby se postará o inicializaci jádra MediaWiki a konfigurace atd. a poté tuto metodu vyvolá.

Příkaz nápovědy

Jednou z vestavěných funkcí, kterou mají všechny skripty údržby, je možnost --help. Výše uvedený vzorový příklad by vytvořil následující stránku nápovědy:

$ php helloWorld.php --help

Usage: php helloWorld.php […]

Generic maintenance parameters:
    --help (-h): Zobrazit tuto zprávu nápovědy
    --quiet (-q): Zda se má potlačit nechybový výstup
    --conf: Umístění LocalSettings.php, pokud není výchozí
    --wiki: Pro specifikaci ID wiki
    --server: Protokol a název serveru, který se má použít v URL
    --profiler: Výstupní formát Profiler (obvykle "text")
…

Přidáváme popis

"Na co je ale dobrý tenhle údržbářský skript?" Už slyším, jak se ptáte.

Proto můžeme pomocí metody addDescription do našeho konstruktoru přidat popis, který se zobrazí hned na začátku výstupu při použití parametru "--help":

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

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

Takže teď už bude vracet náš popis:

$ php helloWorld.php --help

Say hello.

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

Volby a zpracování jejich hodnot

Pozdravit celičký svět je nepochybně správné a dobré, ale my chceme, aby ten skript uměl pozdravit i konkrétního jednotlivce.

Abyste přidali další volbu příkazového řádku, přidejte do třídy class HelloWorld konstruktor, co zavolá ze třídy Maintenance metodu addOption(), která zaktualizuje metodu execute(), aby zahrnula i vaši novou volbu. Parametry addOption() jsou $name, $description, $required = false, $withArg = false, $shortName = false, takže:

	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!" );
	}

V tuto chvíli se po spuštění výstup skriptu helloWorld.php bude měnit, podle toho jakou předáte hodnotu argumentu:

$ 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

Rozšíření

Verze MediaWiki:
1.28
Gerrit change 301709

Pokud váš údržbářský skript vyžaduje funkce, které nabízí nějaké rozšíření, měli byste přidat také požadavek, aby bylo nejprve tohle rozšíření nainstalováno:

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

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

To poskytuje chybovou zprávu helfpul, když rozšíření není povoleno. Například během místního vývoje nemusí být určité rozšíření ještě povoleno v LocalSettings.php nebo při provozování wiki farmy může být rozšíření povoleno na podmnožině wiki.

Uvědomte si, že žádný kód nelze spustit jinak než metodou execute(). Pokusy o volání základních služeb, tříd nebo funkcí MediaWiki nebo volání vlastního kódu rozšíření před tím způsobí chyby nebo jsou nespolehlivé a nepodporované (např. mimo deklaraci třídy nebo v konstruktoru).

Profilování

Skripty údržby podporují možnost --profiler, kterou lze použít ke sledování provádění kódu během akce stránky a hlášení procenta celkového spuštění kódu vynaloženého na jakoukoli konkrétní funkci. Podívejte se na stránku Manual:Profiling .

Psaní testů

Je doporučeno abyste pro vaše údržbářské skripty psali i testy, tak jako u každé jiné třídy. Nápovědu a ukázkové příklady, jak se to dělá najdete na stránce Jak psát testy pro údržbářské skripty.

Dlouho běžící skripty

Pokud je váš skript navržen tak, aby fungoval s velkým množstvím věcí (např. se všemi nebo potenciálně mnoha stránkami nebo revizemi), doporučujeme použít následující osvědčené postupy. Mějte na paměti, že "všechny revize" mohou znamenat miliardy záznamů a měsíce běhu na velkých webech, jako je anglická Wikipedie.

Dávkování

Při zpracování velkého počtu položek je nejlepší tak učinit v dávkách relativně malé velikosti – typicky mezi 100 až 1000, v závislosti na době potřebné ke zpracování každého záznamu.

Dávkování musí být založeno na databázovém poli (nebo kombinaci polí) pokrytém jedinečným databázovým indexem, obvykle primárním klíčem. Typickým příkladem je použití page_id nebo rev_id.

Dávkování je dosaženo strukturováním skriptu do vnitřní smyčky a vnější smyčky: Vnitřní smyčka zpracovává dávku ID a vnější smyčka se dotazuje databáze, aby získala další dávku ID. Vnější smyčka musí sledovat, kde skončila poslední dávka, a měla by začít další dávka.

Pro skript, který pracuje na stránkách, by to vypadalo asi takto:

$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;
}
Základní třída Maintenance poskytuje některé pomocné funkce pro správu velikosti dávky. Můžete zavolat setBatchSize() v konstruktoru vaší třídy skriptů údržby a nastavit výchozí velikost dávky. Tím se automaticky přidá možnost příkazového řádku --batch-size a můžete použít getBatchSize() k získání velikosti dávky pro použití ve vašich dotazech.

Obnovitelnost

Dlouho běžící skripty mohou být přerušeny z několika důvodů – vypnutí databázového serveru, restartování serveru, na kterém běží skript, výjimka z důvodu poškození dat, chyby programování atd. Z tohoto důvodu je důležité poskytnout způsob, jak znovu spustit činnost skriptu někde blízko místa, kde byla přerušena.

K tomu jsou potřeba dvě věci: Výstup začátku každé dávky a poskytnutí možnosti příkazového řádku pro spuštění na konkrétní pozici.

Za předpokladu, že jsme definovali volbu příkazového řádku nazvanou --start-from, můžeme výše uvedený kód upravit takto:

$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" );

Tímto způsobem, pokud dojde k přerušení skriptu, můžeme jej snadno znovu spustit:

$ 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.

Všimněte si, že to předpokládá, že operace skriptu je idempotentní - to znamená, že nezáleží na tom, zda je několik stránek zpracováno vícekrát.

Abychom se ujistili, že víme, kde skript skončil, i když je server, na kterém je skript spuštěn, restartován, doporučujeme přesměrovat výstup skriptu do souboru protokolu. Pohodlným způsobem, jak toho dosáhnout, je příkaz tee. Abyste předešli přerušení a ztrátě informací, když vaše připojení SSH k serveru selže, nezapomeňte spustit skript přes screen nebo tmux.

Sdílení

Pokud skript provádí pomalé operace pro každou položku, může být užitečné spouštět více instancí skriptu paralelně pomocí sdílení.

Sdílení by nemělo být používáno pro skripty, které provádějí aktualizace databáze vysokou rychlostí bez výrazného zpoždění mezi aktualizacemi. Všechny aktualizace jdou na stejný primární DB server a jejich použití z více instancí skriptu to nezrychlí. Může to dokonce zpomalit proces, protože se zvyšuje potřeba spravovat zámky a udržovat izolaci transakcí.

Nejjednodušší způsob implementace sdílení je založen na modulo ID použitého pro záplatování: Na příkazovém řádku definujeme faktor sharding (N) a číslo fragmentu (S), shard condition můžeme definovat jako ID mod N = S s 0 <= S < N. Všechny instance skriptu, které mají běžet paralelně, používají stejný faktor shardingu N a jiné číslo fragmentu S. Každá instance skriptu zpracuje pouze ID, která odpovídají jejímu stavu fragmentu.

Stav fragmentu by mohl být integrován do databázového dotazu, ale to může narušovat efektivní využití indexů. Místo toho zavedeme sharding v kódu a podle toho znásobíme továrnu na šarže. Výše uvedený kód můžeme upravit následovně:

$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 ); 
    }
    
    // ...
}

Poté můžeme spustit více instancí skriptu, které pracují na různých shardech

$ 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...