Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
76 / 76 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
| UpdateSpecialPages | |
100.00% |
76 / 76 |
|
100.00% |
5 / 5 |
25 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| execute | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
13 | |||
| reopenAndWaitForReplicas | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| doSpecialPageCacheUpdates | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
| outputElapsedTime | |
100.00% |
8 / 8 |
|
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 |
| 12 | require_once __DIR__ . '/Maintenance.php'; |
| 13 | // @codeCoverageIgnoreEnd |
| 14 | |
| 15 | use MediaWiki\MainConfigNames; |
| 16 | use MediaWiki\Maintenance\Maintenance; |
| 17 | use MediaWiki\SpecialPage\QueryPage; |
| 18 | use Wikimedia\Rdbms\IDatabase; |
| 19 | |
| 20 | /** |
| 21 | * Maintenance script to update cached special pages. |
| 22 | * |
| 23 | * @ingroup Maintenance |
| 24 | */ |
| 25 | class 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; |
| 181 | require_once RUN_MAINTENANCE_IF_MAIN; |
| 182 | // @codeCoverageIgnoreEnd |