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\IDatabase;
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 IDatabase
102     */
103    protected $db;
104
105    /**
106     * @param ManagerGroup $storage
107     * @param RootPostLoader $rootPostLoader
108     * @param IDatabase $db
109     */
110    public function __construct( ManagerGroup $storage, RootPostLoader $rootPostLoader, IDatabase $db ) {
111        $this->storage = $storage;
112        $this->rootPostLoader = $rootPostLoader;
113        $this->db = $db;
114    }
115
116    /**
117     * @param stdClass $row
118     * @return array
119     * @throws TimestampException
120     * @throws FlowException
121     * @throws InvalidInputException
122     */
123    public function update( $row ) {
124        $uuid = UUID::create( $row->workflow_id );
125
126        switch ( $row->workflow_type ) {
127            case 'discussion':
128                $revision = $this->storage->get( 'Header', $uuid );
129                break;
130
131            case 'topic':
132                // fetch topic (has same id as workflow) via RootPostLoader so
133                // all children are populated
134                $revision = $this->rootPostLoader->get( $uuid );
135                break;
136
137            default:
138                throw new FlowException( 'Unknown workflow type: ' . $row->workflow_type );
139        }
140
141        if ( !$revision ) {
142            return [];
143        }
144
145        $timestamp = $this->getUpdateTimestamp( $revision )->getTimestamp( TS_MW );
146        if ( $timestamp === wfTimestamp( TS_MW, $row->workflow_last_update_timestamp ) ) {
147            // correct update timestamp already, nothing to update
148            return [];
149        }
150
151        return [ 'workflow_last_update_timestamp' => $this->db->timestamp( $timestamp ) ];
152    }
153
154    /**
155     * @param AbstractRevision $revision
156     * @return MWTimestamp
157     * @throws Exception
158     * @throws TimestampException
159     * @throws DataModelException
160     */
161    protected function getUpdateTimestamp( AbstractRevision $revision ) {
162        $timestamp = $revision->getRevisionId()->getTimestampObj();
163
164        if ( !$revision instanceof PostRevision ) {
165            return $timestamp;
166        }
167
168        foreach ( $revision->getChildren() as $child ) {
169            // go recursive, find timestamp of most recent child post
170            $comparison = $this->getUpdateTimestamp( $child );
171            $diff = $comparison->diff( $timestamp );
172
173            // invert will be 1 if the diff is a negative time period from
174            // child timestamp ($comparison) to $timestamp, which means that
175            // $comparison is more recent than our current $timestamp
176            if ( $diff->invert ) {
177                $timestamp = $comparison;
178            }
179        }
180
181        return $timestamp;
182    }
183}
184
185class UpdateWorkflowLastUpdateTimestampWriter extends BatchRowWriter {
186    /**
187     * @var ManagerGroup
188     */
189    protected $storage;
190
191    /**
192     * @param ManagerGroup $storage
193     * @param bool $clusterName
194     */
195    public function __construct( ManagerGroup $storage, $clusterName = false ) {
196        $this->storage = $storage;
197        $this->clusterName = $clusterName;
198    }
199
200    /**
201     * Overwriting default writer because I want to use Flow storage methods so
202     * the updates also affect cache, not just DB.
203     *
204     * @param array[] $updates
205     */
206    public function write( array $updates ) {
207        /*
208         * from:
209         * [
210         *     'primaryKey' => [ 'workflow_id' => $id ],
211         *     'updates' => [ 'workflow_last_update_timestamp' => $timestamp ],
212         * ]
213         * to:
214         * [ $id => $timestamp ]
215         */
216        $timestamps = array_combine(
217            $this->arrayColumn( $this->arrayColumn( $updates, 'primaryKey' ), 'workflow_id' ),
218            $this->arrayColumn( $this->arrayColumn( $updates, 'changes' ), 'workflow_last_update_timestamp' )
219        );
220
221        /** @var UUID[] $uuids */
222        $uuids = array_map( [ UUID::class, 'create' ], array_keys( $timestamps ) );
223
224        /** @var Workflow[] $workflows */
225        $workflows = $this->storage->getMulti( 'Workflow', $uuids );
226        foreach ( $workflows as $workflow ) {
227            $timestamp = $timestamps[$workflow->getId()->getBinary()->__toString()];
228            $workflow->updateLastUpdated( UUID::getComparisonUUID( $timestamp ) );
229        }
230
231        $this->storage->multiPut( $workflows );
232
233        // prevent memory from filling up
234        $this->storage->clear();
235
236        $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
237        $lbFactory->waitForReplication( [ 'cluster' => $this->clusterName ] );
238    }
239
240    /**
241     * PHP<5.5-compatible array_column alternative.
242     *
243     * @param array $array
244     * @param string $key
245     * @return array
246     */
247    protected function arrayColumn( array $array, $key ) {
248        return array_map( static function ( $item ) use ( $key ) {
249            return $item[$key];
250        }, $array );
251    }
252}
253
254$maintClass = FlowFixWorkflowLastUpdateTimestamp::class;
255require_once RUN_MAINTENANCE_IF_MAIN;