Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 3
FlowFixWorkflowLastUpdateTimestamp
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 3
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 output
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
UpdateWorkflowLastUpdateTimestampGenerator
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 3
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 getUpdateTimestamp
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
UpdateWorkflowLastUpdateTimestampWriter
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 3
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 write
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 arrayColumn
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Maintenance;
4
5use BatchRowIterator;
6use BatchRowUpdate;
7use BatchRowWriter;
8use Exception;
9use Flow\Container;
10use Flow\Data\ManagerGroup;
11use Flow\DbFactory;
12use Flow\Exception\DataModelException;
13use Flow\Exception\FlowException;
14use Flow\Exception\InvalidInputException;
15use Flow\Model\AbstractRevision;
16use Flow\Model\PostRevision;
17use Flow\Model\UUID;
18use Flow\Model\Workflow;
19use Flow\Repository\RootPostLoader;
20use Maintenance;
21use MediaWiki\MediaWikiServices;
22use MediaWiki\Utils\MWTimestamp;
23use MediaWiki\WikiMap\WikiMap;
24use RowUpdateGenerator;
25use stdClass;
26use Wikimedia\Rdbms\IReadableDatabase;
27use Wikimedia\Timestamp\TimestampException;
28
29$IP = getenv( 'MW_INSTALL_PATH' );
30if ( $IP === false ) {
31    $IP = __DIR__ . '/../../..';
32}
33
34require_once "$IP/maintenance/Maintenance.php";
35
36/**
37 * @ingroup Maintenance
38 */
39class FlowFixWorkflowLastUpdateTimestamp extends Maintenance {
40    public function __construct() {
41        parent::__construct();
42
43        $this->addDescription( 'Fixes any incorrect workflow_last_update_timestamp for topics' );
44
45        $this->setBatchSize( 10 );
46
47        $this->requireExtension( 'Flow' );
48    }
49
50    public function execute() {
51        global $wgFlowCluster;
52
53        /** @var DbFactory $dbFactory */
54        $dbFactory = Container::get( 'db.factory' );
55        $storage = Container::get( 'storage' );
56        $rootPostLoader = Container::get( 'loader.root_post' );
57        $dbr = $dbFactory->getDB( DB_REPLICA );
58
59        $iterator = new BatchRowIterator( $dbr, 'flow_workflow', 'workflow_id', $this->getBatchSize() );
60        $iterator->setFetchColumns( [ 'workflow_id', 'workflow_type', 'workflow_last_update_timestamp' ] );
61        $iterator->addConditions( [ 'workflow_wiki' => WikiMap::getCurrentWikiId() ] );
62        $iterator->setCaller( __METHOD__ );
63
64        $writer = new UpdateWorkflowLastUpdateTimestampWriter( $storage, $wgFlowCluster );
65        $writer->setCaller( __METHOD__ );
66
67        $updater = new BatchRowUpdate(
68            $iterator,
69            $writer,
70            new UpdateWorkflowLastUpdateTimestampGenerator( $storage, $rootPostLoader, $dbr )
71        );
72        $updater->setOutput( [ $this, 'output' ] );
73        $updater->execute();
74    }
75
76    /**
77     * parent::output() is a protected method, only way to access it from a
78     * callback in php5.3 is to make a public function. In 5.4 can replace with
79     * a Closure.
80     *
81     * @param string $out
82     * @param mixed|null $channel
83     */
84    public function output( $out, $channel = null ) {
85        parent::output( $out, $channel );
86    }
87}
88
89class UpdateWorkflowLastUpdateTimestampGenerator implements RowUpdateGenerator {
90    /**
91     * @var ManagerGroup
92     */
93    protected $storage;
94
95    /**
96     * @var RootPostLoader
97     */
98    protected $rootPostLoader;
99
100    /**
101     * @var IReadableDatabase
102     */
103    protected $db;
104
105    public function __construct( ManagerGroup $storage, RootPostLoader $rootPostLoader, IReadableDatabase $db ) {
106        $this->storage = $storage;
107        $this->rootPostLoader = $rootPostLoader;
108        $this->db = $db;
109    }
110
111    /**
112     * @param stdClass $row
113     * @return array
114     * @throws TimestampException
115     * @throws FlowException
116     * @throws InvalidInputException
117     */
118    public function update( $row ) {
119        $uuid = UUID::create( $row->workflow_id );
120
121        switch ( $row->workflow_type ) {
122            case 'discussion':
123                $revision = $this->storage->get( 'Header', $uuid );
124                break;
125
126            case 'topic':
127                // fetch topic (has same id as workflow) via RootPostLoader so
128                // all children are populated
129                $revision = $this->rootPostLoader->get( $uuid );
130                break;
131
132            default:
133                throw new FlowException( 'Unknown workflow type: ' . $row->workflow_type );
134        }
135
136        if ( !$revision ) {
137            return [];
138        }
139
140        $timestamp = $this->getUpdateTimestamp( $revision )->getTimestamp( TS_MW );
141        if ( $timestamp === wfTimestamp( TS_MW, $row->workflow_last_update_timestamp ) ) {
142            // correct update timestamp already, nothing to update
143            return [];
144        }
145
146        return [ 'workflow_last_update_timestamp' => $this->db->timestamp( $timestamp ) ];
147    }
148
149    /**
150     * @param AbstractRevision $revision
151     * @return MWTimestamp
152     * @throws Exception
153     * @throws TimestampException
154     * @throws DataModelException
155     */
156    protected function getUpdateTimestamp( AbstractRevision $revision ) {
157        $timestamp = $revision->getRevisionId()->getTimestampObj();
158
159        if ( !$revision instanceof PostRevision ) {
160            return $timestamp;
161        }
162
163        foreach ( $revision->getChildren() as $child ) {
164            // go recursive, find timestamp of most recent child post
165            $comparison = $this->getUpdateTimestamp( $child );
166            $diff = $comparison->diff( $timestamp );
167
168            // invert will be 1 if the diff is a negative time period from
169            // child timestamp ($comparison) to $timestamp, which means that
170            // $comparison is more recent than our current $timestamp
171            if ( $diff->invert ) {
172                $timestamp = $comparison;
173            }
174        }
175
176        return $timestamp;
177    }
178}
179
180class UpdateWorkflowLastUpdateTimestampWriter extends BatchRowWriter {
181    /**
182     * @var ManagerGroup
183     */
184    protected $storage;
185
186    /**
187     * @param ManagerGroup $storage
188     * @param string|false $clusterName
189     */
190    public function __construct( ManagerGroup $storage, $clusterName = false ) {
191        $this->storage = $storage;
192        $this->clusterName = $clusterName;
193    }
194
195    /**
196     * Overwriting default writer because I want to use Flow storage methods so
197     * the updates also affect cache, not just DB.
198     *
199     * @param array[] $updates
200     */
201    public function write( array $updates ) {
202        /*
203         * from:
204         * [
205         *     'primaryKey' => [ 'workflow_id' => $id ],
206         *     'updates' => [ 'workflow_last_update_timestamp' => $timestamp ],
207         * ]
208         * to:
209         * [ $id => $timestamp ]
210         */
211        $timestamps = array_combine(
212            $this->arrayColumn( $this->arrayColumn( $updates, 'primaryKey' ), 'workflow_id' ),
213            $this->arrayColumn( $this->arrayColumn( $updates, 'changes' ), 'workflow_last_update_timestamp' )
214        );
215
216        /** @var UUID[] $uuids */
217        $uuids = array_map( [ UUID::class, 'create' ], array_keys( $timestamps ) );
218
219        /** @var Workflow[] $workflows */
220        $workflows = $this->storage->getMulti( 'Workflow', $uuids );
221        foreach ( $workflows as $workflow ) {
222            $timestamp = $timestamps[$workflow->getId()->getBinary()->__toString()];
223            $workflow->updateLastUpdated( UUID::getComparisonUUID( $timestamp ) );
224        }
225
226        $this->storage->multiPut( $workflows );
227
228        // prevent memory from filling up
229        $this->storage->clear();
230
231        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
232        $lbFactory->waitForReplication( [ 'cluster' => $this->clusterName ] );
233    }
234
235    /**
236     * PHP<5.5-compatible array_column alternative.
237     *
238     * @param array $array
239     * @param string $key
240     * @return array
241     */
242    protected function arrayColumn( array $array, $key ) {
243        return array_map( static function ( $item ) use ( $key ) {
244            return $item[$key];
245        }, $array );
246    }
247}
248
249$maintClass = FlowFixWorkflowLastUpdateTimestamp::class;
250require_once RUN_MAINTENANCE_IF_MAIN;