Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.04% covered (warning)
72.04%
67 / 93
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReindexTask
72.04% covered (warning)
72.04%
67 / 93
25.00% covered (danger)
25.00%
2 / 8
34.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isComplete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 cancel
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 delete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getResponse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatus
66.67% covered (warning)
66.67%
14 / 21
0.00% covered (danger)
0.00%
0 / 1
8.81
 mergeStatusWithChildren
85.96% covered (warning)
85.96%
49 / 57
0.00% covered (danger)
0.00%
0 / 1
8.18
1<?php
2
3namespace CirrusSearch\Elastica;
4
5use Elastica\Client;
6use Elastica\Exception\ResponseException;
7use Elastica\Exception\RuntimeException;
8use Elastica\Request;
9use MediaWiki\Logger\LoggerFactory;
10
11class ReindexTask {
12    /** @var Client */
13    private $client;
14    /** @var string */
15    private $taskId;
16    /** @var ReindexResponse|null */
17    private $response;
18    /** @var \Psr\Log\LoggerInterface */
19    private $log;
20
21    /**
22     * @param Client $client
23     * @param string $taskId
24     */
25    public function __construct( Client $client, $taskId ) {
26        $this->client = $client;
27        $this->taskId = $taskId;
28        $this->log = LoggerFactory::getInstance( 'CirrusSearch' );
29    }
30
31    /**
32     * @return string
33     */
34    public function getId() {
35        return $this->taskId;
36    }
37
38    /**
39     * @param bool $check When true queries the remote
40     *  to see if the task is complete. Otherwise reports
41     *  last requested status.
42     * @return bool True if the reindex task is complete
43     */
44    public function isComplete( $check = false ) {
45        return $this->response !== null;
46    }
47
48    /**
49     * Cancel the in-progress reindex task.
50     * @return bool True if cancel was succesful
51     */
52    public function cancel() {
53        if ( $this->response ) {
54            throw new \Exception( 'Cannot cancel completed task' );
55        }
56
57        $response = $this->client->request( "_tasks/{$this->taskId}/_cancel", Request::POST );
58
59        return $response->isOK();
60    }
61
62    /**
63     * Delete the task
64     * @return bool True if delete was successful, false otherwise.
65     *  Throws Elastica NotFoundException for unknown task (already
66     *  deleted?) or HttpException for communication failures.
67     */
68    public function delete() {
69        if ( !$this->response ) {
70            throw new \Exception( 'Cannot delete in-progress task' );
71        }
72        $response =
73            $this->client->getIndex( '.tasks' )->deleteById( $this->taskId );
74
75        return $response->isOK();
76    }
77
78    /**
79     * Get the final result of the reindexing task.
80     *
81     * @return ReindexResponse|null The result of the reindex, or null
82     *  if the reindex is still running. self::getStatus must be used
83     *  to update the task completion status.
84     */
85    public function getResponse() {
86        return $this->response;
87    }
88
89    /**
90     * @return ReindexStatus|null The status of the reindex, or null
91     *  on failure. Transport may also throw exceptions for network
92     *  failures.
93     */
94    public function getStatus() {
95        if ( $this->response ) {
96            // task complete
97            return $this->response;
98        }
99
100        $response = $this->client->request( "_tasks/{$this->taskId}", Request::GET );
101        if ( !$response->isOK() ) {
102            $lastRequest = $this->client->getLastRequest();
103            if ( $lastRequest !== null ) {
104                throw new ResponseException( $lastRequest, $response );
105            } else {
106                throw new RuntimeException( "Client::getLastRequest() should not be null" );
107            }
108        }
109        $data = $response->getData();
110        $status = $data['task']['status'];
111
112        if ( isset( $data['response'] ) ) {
113            // task complete
114            $this->response = new ReindexResponse( $data['response'] );
115
116            return $this->response;
117        }
118
119        /**
120         * the task.status.slices array contains null for each incomplete child
121         * task. This fetches the children and merges their status in.
122         */
123        if ( isset( $data['task']['status']['slices'] ) ) {
124            $childResponse = $this->client->request( "_tasks", Request::GET, [], [
125                'parent_task_id' => $this->taskId,
126                'detailed' => 'true',
127            ] );
128            if ( $childResponse->isOK() ) {
129                $status = $this->mergeStatusWithChildren( $status, $childResponse->getData() );
130            }
131        }
132
133        return new ReindexStatus( $status );
134    }
135
136    private function mergeStatusWithChildren( array $status, array $childResponse ) {
137        foreach ( $childResponse['nodes'] as $nodeData ) {
138            foreach ( $nodeData['tasks'] as $taskId => $childData ) {
139                $sliceId = $childData['status']['slice_id'];
140                $status['slices'][$sliceId] = $childData['status'];
141            }
142        }
143
144        // Below mimics org.elasticsearch.action.bulk.byscroll.BulkByScrollTask.Status::Status
145        // except that class doesn't have data about in-progress task's.
146        $status['total'] = 0;
147        $status['updated'] = 0;
148        $status['created'] = 0;
149        $status['deleted'] = 0;
150        $status['batches'] = 0;
151        $status['version_conflicts'] = 0;
152        $status['noops'] = 0;
153        $status['bulkRetries'] = 0;
154        $status['searchRetries'] = 0;
155        $status['throttled_millis'] = 0;
156        $status['requests_per_second'] = 0;
157        $status['throttled_until_millis'] = PHP_INT_MAX;
158        $sliceFields = [
159            'total',
160            'updated',
161            'created',
162            'deleted',
163            'batches',
164            'version_conflicts',
165            'noops',
166            'retries',
167            'throttled_millis',
168            'requests_per_second',
169            'throttled_until_millis',
170        ];
171        foreach ( $status['slices'] as $slice ) {
172            if ( $slice === null ) {
173                // slice has failed catastrophically
174                continue;
175            }
176            $missing_status_fields = array_diff_key( array_flip( $sliceFields ), $slice );
177            if ( $missing_status_fields !== [] ) {
178                // slice has missing key fields
179                $slice_to_json = json_encode( $slice );
180                $this->log->warning( 'Missing key field(s) for reindex task status', [
181                    'cirrus_reindex_task_slice' => $slice_to_json,
182                    'exact_missing_fields' => $missing_status_fields,
183                ] );
184                continue;
185            }
186            $status['total'] += $slice['total'];
187            $status['updated'] += $slice['updated'];
188            $status['created'] += $slice['created'];
189            $status['deleted'] += $slice['deleted'];
190            $status['batches'] += $slice['batches'];
191            $status['version_conflicts'] += $slice['version_conflicts'];
192            $status['noops'] += $slice['noops'];
193            $status['retries']['bulk'] += $slice['retries']['bulk'];
194            $status['retries']['search'] += $slice['retries']['search'];
195            $status['throttled_millis'] += $slice['throttled_millis'];
196            $status['requests_per_second'] += $slice['requests_per_second'] === -1 ? INF
197                : $slice['requests_per_second'];
198            $status['throttled_until_millis'] += min( $status['throttled_until_millis'],
199                $slice['throttled_until_millis'] );
200        }
201
202        if ( $status['requests_per_second'] === INF ) {
203            $status['requests_per_second'] = -1;
204        }
205
206        return $status;
207    }
208}