Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
UpdateSpecialPages
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
5 / 5
25
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
13
 reopenAndWaitForReplicas
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 doSpecialPageCacheUpdates
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 outputElapsedTime
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Update for cached special pages.
4 * Run this script periodically if you have miser mode enabled.
5 *
6 * @license GPL-2.0-or-later
7 * @file
8 * @ingroup Maintenance
9 */
10
11// @codeCoverageIgnoreStart
12require_once __DIR__ . '/Maintenance.php';
13// @codeCoverageIgnoreEnd
14
15use MediaWiki\MainConfigNames;
16use MediaWiki\Maintenance\Maintenance;
17use MediaWiki\SpecialPage\QueryPage;
18use Wikimedia\Rdbms\IDatabase;
19
20/**
21 * Maintenance script to update cached special pages.
22 *
23 * @ingroup Maintenance
24 */
25class UpdateSpecialPages extends Maintenance {
26    public function __construct() {
27        parent::__construct();
28        $this->addOption( 'list', 'List special page names' );
29        $this->addOption( 'only', 'Only update "page"; case sensitive, ' .
30            'check correct case by calling this script with --list. ' .
31            'Ex: --only=BrokenRedirects', false, true );
32        $this->addOption( 'override', 'Also update pages that have updates disabled' );
33    }
34
35    public function execute() {
36        $dbw = $this->getPrimaryDB();
37        $config = $this->getConfig();
38        $specialPageFactory = $this->getServiceContainer()->getSpecialPageFactory();
39
40        $this->doSpecialPageCacheUpdates( $dbw );
41
42        $queryCacheLimit = (int)$config->get( MainConfigNames::QueryCacheLimit );
43        $disabledQueryPages = QueryPage::getDisabledQueryPages( $config );
44        foreach ( QueryPage::getPages() as $page ) {
45            [ , $special ] = $page;
46            $limit = $page[2] ?? $queryCacheLimit;
47
48            # --list : just show the name of pages
49            if ( $this->hasOption( 'list' ) ) {
50                $this->output( "$special [QueryPage]\n" );
51                continue;
52            }
53
54            if ( !$this->hasOption( 'override' )
55                && isset( $disabledQueryPages[$special] )
56            ) {
57                $this->output( sprintf( "%-30s [QueryPage] disabled\n", $special ) );
58                continue;
59            }
60
61            $specialObj = $specialPageFactory->getPage( $special );
62            if ( !$specialObj ) {
63                $this->output( "No such special page: $special\n" );
64                return;
65            }
66            if ( $specialObj instanceof QueryPage ) {
67                $queryPage = $specialObj;
68            } else {
69                $class = get_class( $specialObj );
70                $this->fatalError( "$class is not an instance of QueryPage.\n" );
71            }
72
73            if ( !$this->hasOption( 'only' ) || $this->getOption( 'only' ) === $queryPage->getName() ) {
74                $this->output( sprintf( '%-30s [QueryPage] ', $special ) );
75                if ( $queryPage->isExpensive() ) {
76                    $t1 = microtime( true );
77                    # Do the query
78                    $num = $queryPage->recache( $limit );
79                    $t2 = microtime( true );
80                    if ( $num === false ) {
81                        $this->output( "FAILED: database error\n" );
82                    } else {
83                        $this->output( "got $num rows in " );
84                        $this->outputElapsedTime( $t2 - $t1 );
85                    }
86                    # Reopen any connections that have closed
87                    $this->reopenAndWaitForReplicas();
88                } else {
89                    // Check if this page was expensive before and now cheap
90                    $cached = $queryPage->getCachedTimestamp();
91                    if ( $cached ) {
92                        $queryPage->deleteAllCachedData();
93                        $this->reopenAndWaitForReplicas();
94                        $this->output( "cheap, but deleted cached data\n" );
95                    } else {
96                        $this->output( "cheap, skipped\n" );
97                    }
98                }
99                if ( $this->hasOption( 'only' ) ) {
100                    break;
101                }
102            }
103        }
104    }
105
106    /**
107     * Re-open any closed db connection, and wait for replicas
108     *
109     * Queries that take a really long time, might cause the
110     * mysql connection to "go away"
111     */
112    private function reopenAndWaitForReplicas() {
113        $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
114        $lb = $lbFactory->getMainLB();
115        if ( !$lb->pingAll() ) {
116            // We don't want the tests to sleep for 10 seconds, so mark this as ignored because there is no reason to
117            // test it.
118            // @codeCoverageIgnoreStart
119            $this->output( "\n" );
120            do {
121                $this->error( "Connection failed, reconnecting in 10 seconds..." );
122                sleep( 10 );
123                $this->waitForReplication();
124            } while ( !$lb->pingAll() );
125            $this->output( "Reconnected\n\n" );
126            // @codeCoverageIgnoreEnd
127        }
128        $this->waitForReplication();
129    }
130
131    public function doSpecialPageCacheUpdates( IDatabase $dbw ) {
132        foreach ( $this->getConfig()->get( MainConfigNames::SpecialPageCacheUpdates ) as $special => $call ) {
133            # --list : just show the name of pages
134            if ( $this->hasOption( 'list' ) ) {
135                $this->output( "$special [callback]\n" );
136                continue;
137            }
138
139            if ( !$this->hasOption( 'only' ) || $this->getOption( 'only' ) === $special ) {
140                $this->output( sprintf( '%-30s [callback] ', $special ) );
141                if ( !is_callable( $call ) ) {
142                    $this->error( "Uncallable function $call!" );
143                    continue;
144                }
145                $t1 = microtime( true );
146                $call( $dbw );
147                $t2 = microtime( true );
148
149                $this->output( "completed in " );
150                $this->outputElapsedTime( $t2 - $t1 );
151
152                # Wait for the replica DB to catch up
153                $this->reopenAndWaitForReplicas();
154            }
155        }
156    }
157
158    /**
159     * Outputs the time that was elapsed to update the cache update for
160     * a script.
161     *
162     * @param float $elapsed
163     * @return void
164     */
165    private function outputElapsedTime( float $elapsed ) {
166        $hours = intval( $elapsed / 3600 );
167        $minutes = intval( (int)$elapsed % 3600 / 60 );
168        $seconds = $elapsed - $hours * 3600 - $minutes * 60;
169        if ( $hours ) {
170            $this->output( $hours . 'h ' );
171        }
172        if ( $minutes ) {
173            $this->output( $minutes . 'm ' );
174        }
175        $this->output( sprintf( "%.2fs\n", $seconds ) );
176    }
177}
178
179// @codeCoverageIgnoreStart
180$maintClass = UpdateSpecialPages::class;
181require_once RUN_MAINTENANCE_IF_MAIN;
182// @codeCoverageIgnoreEnd