Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
PruneUnusedLinkTargetRows
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 2
90
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3// @codeCoverageIgnoreStart
4require_once __DIR__ . '/Maintenance.php';
5// @codeCoverageIgnoreEnd
6
7/**
8 * Maintenance script that cleans unused rows in linktarget table
9 *
10 * @ingroup Maintenance
11 * @since 1.39
12 */
13class PruneUnusedLinkTargetRows extends Maintenance {
14    public function __construct() {
15        parent::__construct();
16        $this->addDescription(
17            'Clean unused rows in linktarget table'
18        );
19        $this->addOption(
20            'sleep',
21            'Sleep time (in seconds) between every batch. Default: 0',
22            false,
23            true
24        );
25        $this->addOption( 'dry', 'Dry run', false );
26        $this->addOption( 'start', 'Start after this lt_id', false, true );
27        $this->setBatchSize( 50 );
28    }
29
30    public function execute() {
31        $dbw = $this->getPrimaryDB();
32        $dbr = $this->getReplicaDB();
33        $maxLtId = (int)$dbr->newSelectQueryBuilder()
34            ->select( 'MAX(lt_id)' )
35            ->from( 'linktarget' )
36            ->caller( __METHOD__ )
37            ->fetchField();
38        // To avoid race condition of newly added linktarget rows
39        // being deleted before getting a chance to be used, let's ignore the newest ones.
40        $maxLtId = min( [ $maxLtId - 1, (int)( $maxLtId * 0.99 ) ] );
41
42        $ltCounter = (int)$this->getOption( 'start', 0 );
43
44        $this->output( "Deleting unused linktarget rows...\n" );
45        $deleted = 0;
46        $linksMigration = $this->getServiceContainer()->getLinksMigration();
47        while ( $ltCounter < $maxLtId ) {
48            $batchMaxLtId = min( $ltCounter + $this->getBatchSize(), $maxLtId ) + 1;
49            $this->output( "Checking lt_id between $ltCounter and $batchMaxLtId...\n" );
50            $queryBuilder = $dbr->newSelectQueryBuilder()
51                ->select( [ 'lt_id' ] )
52                ->from( 'linktarget' );
53            $queryBuilder->where( [
54                $dbr->expr( 'lt_id', '<', $batchMaxLtId ),
55                $dbr->expr( 'lt_id', '>', $ltCounter )
56            ] );
57            foreach ( $linksMigration::$mapping as $table => $tableData ) {
58                $queryBuilder->leftJoin( $table, null, $tableData['target_id'] . '=lt_id' );
59                $queryBuilder->andWhere( [
60                    $tableData['target_id'] => null
61                ] );
62            }
63            $ltIdsToDelete = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
64            if ( !$ltIdsToDelete ) {
65                $ltCounter += $this->getBatchSize();
66                continue;
67            }
68
69            // Run against primary as well with a faster query plan, just to be safe.
70            // Also having a bit of time in between helps in cases of immediate removal and insertion of use.
71            $queryBuilder = $dbr->newSelectQueryBuilder()
72                ->select( [ 'lt_id' ] )
73                ->from( 'linktarget' )
74                ->where( [
75                    'lt_id' => $ltIdsToDelete,
76                ] );
77            foreach ( $linksMigration::$mapping as $table => $tableData ) {
78                $queryBuilder->leftJoin( $table, null, $tableData['target_id'] . '=lt_id' );
79                $queryBuilder->andWhere( [
80                    $tableData['target_id'] => null
81                ] );
82            }
83            $ltIdsToDelete = $queryBuilder->caller( __METHOD__ )->fetchFieldValues();
84            if ( !$ltIdsToDelete ) {
85                $ltCounter += $this->getBatchSize();
86                continue;
87            }
88
89            if ( !$this->getOption( 'dry' ) ) {
90                $dbw->newDeleteQueryBuilder()
91                    ->deleteFrom( 'linktarget' )
92                    ->where( [ 'lt_id' => $ltIdsToDelete ] )
93                    ->caller( __METHOD__ )->execute();
94            }
95            $deleted += count( $ltIdsToDelete );
96            $ltCounter += $this->getBatchSize();
97
98            // Sleep between batches for replication to catch up
99            $this->waitForReplication();
100            $sleep = (int)$this->getOption( 'sleep', 0 );
101            if ( $sleep > 0 ) {
102                sleep( $sleep );
103            }
104        }
105
106        $this->output(
107            "Completed clean up linktarget table, "
108            . "$deleted rows deleted.\n"
109        );
110
111        return true;
112    }
113
114}
115
116// @codeCoverageIgnoreStart
117$maintClass = PruneUnusedLinkTargetRows::class;
118require_once RUN_MAINTENANCE_IF_MAIN;
119// @codeCoverageIgnoreEnd