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