Passed
Push — bufferfix ( 9ef1b7...96911a )
by Simon
07:44
created

SolrIndexTask::runChildProcess()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
c 1
b 0
f 0
nc 9
nop 5
dl 0
loc 21
ccs 0
cts 0
cp 0
crap 42
rs 9.2222
1
<?php
2
3
4
namespace Firesphere\SolrSearch\Tasks;
5
6
use Exception;
7
use Firesphere\SolrSearch\Factories\DocumentFactory;
8
use Firesphere\SolrSearch\Helpers\SolrLogger;
9
use Firesphere\SolrSearch\Indexes\BaseIndex;
10
use Firesphere\SolrSearch\Services\SolrCoreService;
11
use Firesphere\SolrSearch\States\SiteState;
12
use Firesphere\SolrSearch\Traits\LoggerTrait;
13
use GuzzleHttp\Exception\GuzzleException;
14
use Psr\Log\LoggerInterface;
15
use ReflectionException;
16
use SilverStripe\Control\Director;
17
use SilverStripe\Control\HTTPRequest;
18
use SilverStripe\Core\Injector\Injector;
19
use SilverStripe\Dev\BuildTask;
20
use SilverStripe\ORM\ArrayList;
21
use SilverStripe\ORM\DataList;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\DB;
24
use SilverStripe\ORM\ValidationException;
25
use SilverStripe\Versioned\Versioned;
26
27
/**
28
 * Class SolrIndexTask
29
 *
30
 * @description Index items to Solr through a tasks
31
 * @package Firesphere\SolrSearch\Tasks
32
 */
33
class SolrIndexTask extends BuildTask
34
{
35
    use LoggerTrait;
36
    /**
37
     * URLSegment of this task
38
     *
39
     * @var string
40
     */
41
    private static $segment = 'SolrIndexTask';
42
    /**
43
     * Store the current states for all instances of SiteState
44
     *
45
     * @var array
46
     */
47
    public $currentStates;
48
    /**
49
     * My name
50
     *
51
     * @var string
52
     */
53
    protected $title = 'Solr Index update';
54
    /**
55
     * What do I do?
56
     *
57
     * @var string
58
     */
59
    protected $description = 'Add or update documents to an existing Solr core.';
60
    /**
61
     * Debug mode enabled, default false
62
     *
63
     * @var bool
64
     */
65
    protected $debug = false;
66
    /**
67
     * Singleton of {@link SolrCoreService}
68
     *
69
     * @var SolrCoreService
70
     */
71
    protected $service;
72
73
    /**
74
     * SolrIndexTask constructor. Sets up the document factory
75
     *
76
     * @throws ReflectionException
77 14
     */
78
    public function __construct()
79 14
    {
80
        parent::__construct();
81
        // Only index live items.
82 14
        // The old FTS module also indexed Draft items. This is unnecessary
83 14
        Versioned::set_reading_mode(Versioned::DEFAULT_MODE);
84 14
        // If versioned is needed, a separate Versioned Search module is required
85 14
        $this->setService(Injector::inst()->get(SolrCoreService::class));
86 14
        $this->setLogger(Injector::inst()->get(LoggerInterface::class));
87 14
        $this->setDebug(Director::isDev() || Director::is_cli());
88 14
        $currentStates = SiteState::currentStates();
89
        SiteState::setDefaultStates($currentStates);
90
    }
91
92
    /**
93
     * Set the {@link SolrCoreService}
94
     *
95
     * @param SolrCoreService $service
96 14
     * @return SolrIndexTask
97
     */
98 14
    public function setService(SolrCoreService $service): SolrIndexTask
99
    {
100 14
        $this->service = $service;
101
102
        return $this;
103
    }
104
105
    /**
106
     * Set the debug mode
107
     *
108
     * @param bool $debug
109 14
     * @return SolrIndexTask
110
     */
111 14
    public function setDebug(bool $debug): SolrIndexTask
112
    {
113 14
        $this->debug = $debug;
114
115
        return $this;
116
    }
117
118
    /**
119
     * Implement this method in the task subclass to
120
     * execute via the TaskRunner
121
     *
122
     * @param HTTPRequest $request
123
     * @return int|bool
124
     * @throws Exception
125
     * @throws GuzzleException
126 13
     * @todo defer to background because it may run out of memory
127
     */
128 13
    public function run($request)
129 13
    {
130 13
        $startTime = time();
131 13
        $this->getLogger()->info(date('Y-m-d H:i:s'));
132
        list($vars, $group, $isGroup) = $this->taskSetup($request);
133 13
        $groups = 0;
134
        $indexes = $this->service->getValidIndexes($request->getVar('index'));
135 13
136
        foreach ($indexes as $indexName) {
137 13
            /** @var BaseIndex $index */
138 13
            $index = Injector::inst()->get($indexName, false);
139 13
140 10
            $indexClasses = $index->getClasses();
141
            $classes = $this->getClasses($vars, $indexClasses);
142
            if (!count($classes)) {
143 13
                continue;
144
            }
145 13
146
            $this->clearIndex($vars, $index);
147 13
148 13
            $groups = $this->indexClassForIndex($classes, $isGroup, $index, $group);
149
        }
150
151 13
        $this->getLogger()->info(sprintf('Finished in %s', date('H:i:s', time() - $startTime)));
152 13
153
        return $groups;
154 13
    }
155
156
    /**
157
     * Set up the requirements for this task
158
     *
159
     * @param HTTPRequest $request
160
     * @return array
161
     */
162
    protected function taskSetup($request): array
163 13
    {
164
        $vars = $request->getVars();
165 13
        $this->debug = $this->debug || isset($vars['debug']);
166 13
        $group = $vars['group'] ?? 0;
167 13
        $start = $vars['start'] ?? 0;
168 13
        $group = ($start > $group) ? $start : $group;
169 13
        $isGroup = isset($vars['group']);
170
171 13
        return [$vars, $group, $isGroup];
172
    }
173
174
    /**
175
     * get the classes to run for this task execution
176
     *
177
     * @param $vars
178
     * @param array $classes
179
     * @return bool|array
180
     */
181 13
    protected function getClasses($vars, array $classes): array
182
    {
183 13
        if (isset($vars['class'])) {
184 1
            return array_intersect($classes, [$vars['class']]);
185
        }
186
187 12
        return $classes;
188
    }
189
190
    /**
191
     * Clear the given index if a full re-index is needed
192
     *
193
     * @param $vars
194
     * @param BaseIndex $index
195
     * @throws Exception
196
     */
197 13
    public function clearIndex($vars, BaseIndex $index)
198
    {
199 13
        if (!empty($vars['clear'])) {
200 1
            $this->getLogger()->info(sprintf('Clearing index %s', $index->getIndexName()));
201 1
            $this->service->doManipulate(ArrayList::create([]), SolrCoreService::DELETE_TYPE_ALL, $index);
202
        }
203 13
    }
204
205
    /**
206
     * Index the classes for a specific index
207
     *
208
     * @param $classes
209
     * @param $isGroup
210
     * @param BaseIndex $index
211
     * @param $group
212
     * @return int
213
     * @throws Exception
214
     * @throws GuzzleException
215 13
     */
216
    protected function indexClassForIndex($classes, $isGroup, BaseIndex $index, $group): int
217 13
    {
218 13
        $groups = 0;
219 13
        foreach ($classes as $class) {
220
            $groups = $this->indexClass($isGroup, $class, $index, $group);
221
        }
222 13
223
        return $groups;
224
    }
225
226
    /**
227
     * Index a single class for a given index. {@link static::indexClassForIndex()}
228
     *
229
     * @param bool $isGroup
230
     * @param string $class
231
     * @param BaseIndex $index
232
     * @param int $group
233
     * @return int
234
     * @throws GuzzleException
235 13
     * @throws ValidationException
236
     */
237 13
    private function indexClass($isGroup, $class, BaseIndex $index, int $group): int
238
    {
239
        $this->getLogger()->info(sprintf('Indexing %s for %s', $class, $index->getIndexName()), []);
240 13
241
        $batchLength = DocumentFactory::config()->get('batchLength');
242
        $groups = (int)ceil($class::get()->count() / $batchLength);
243
        $groups = $isGroup ? $group : $groups;
244
        $this->getLogger()->info(sprintf('Total groups %s', $groups));
245 13
        do { // Run from oldest to newest
246
            try {
247
                // Temporary workaround for CircleCI
248
                if (function_exists('pcntl_fork')) {
249
                    $cores = SolrCoreService::config()->get('cores');
250
                    $pids = [];
251
                    // for each core, start a grouped indexing
252
                    for ($i = 0; $i < $cores; $i++) {
253
                        $group += $i;
254
                        $pid = pcntl_fork();
255
                        // PID needs to be pushed before anything else, for some reason
256
                        $pids[$i] = $pid;
257 13
                        if (!$pid && $group < $groups) {
258
                            $this->doReindex($group, $class, $batchLength, $index);
259 13
                        }
260 13
                    }
261
                    // Wait for each child to finish
262
                    foreach ($pids as $pid) {
263 13
                        if ($pid) {
264
                            pcntl_waitpid($pid, $status);
265
                        }
266 13
                    }
267 13
                } else {
268
                    $this->doReindex($group, $class, $batchLength, $index);
269
                }
270
            } catch (Exception $error) {
271
                $this->logException($index->getIndexName(), $group, $error);
272
                $group++;
273
                continue;
274
            }
275
            $group++;
276
        } while ($group <= $groups);
277 13
278
        return $groups;
279
    }
280 13
281
    /**
282 13
     * Reindex the given group, for each state
283 13
     *
284 13
     * @param int $group
285 13
     * @param string $class
286 13
     * @param int $batchLength
287
     * @param BaseIndex $index
288 13
     * @throws Exception
289 1
     */
290
    private function doReindex($group, $class, $batchLength, BaseIndex $index): int
291 13
    {
292
        $config = DB::getConfig();
293
        DB::connect($config);
294
        foreach (SiteState::getStates() as $state) {
295
            if ($state !== 'default' && !empty($state)) {
296
                SiteState::withState($state);
297
            }
298
            $this->stateReindex($group, $class, $batchLength, $index);
299
        }
300 1
301
        SiteState::withState(SiteState::DEFAULT_STATE);
302 1
        $this->getLogger()->info(sprintf('Indexed group %s', $group ));
303 1
304 1
        exit(0);
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return integer. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
305
    }
306
307
    /**
308
     * Index a group of a class for a specific state and index
309
     *
310
     * @param $group
311
     * @param $class
312
     * @param $batchLength
313
     * @param BaseIndex $index
314
     * @throws Exception
315
     */
316
    private function stateReindex($group, $class, $batchLength, BaseIndex $index): void
317
    {
318
        // Generate filtered list of local records
319
        $baseClass = DataObject::getSchema()->baseDataClass($class);
320
        /** @var DataList|DataObject[] $items */
321
        $items = DataObject::get($baseClass)
322
            ->sort('ID ASC')
323
            ->limit($batchLength, ($group * $batchLength));
324
        if ($items->count()) {
325
            $this->updateIndex($index, $items);
326
        }
327
    }
328
329
    /**
330
     * Execute the update on the client
331
     *
332
     * @param BaseIndex $index
333
     * @param $items
334
     * @throws Exception
335
     */
336
    private function updateIndex(BaseIndex $index, $items): void
337
    {
338
        $client = $index->getClient();
339
        $update = $client->createUpdate();
340
        $this->service->setInDebugMode($this->debug);
341
        $this->service->updateIndex($index, $items, $update);
342
        $update->addCommit();
343
        $client->update($update);
344
    }
345
346
    /**
347
     * Log an exception if it happens. Most are catched, these logs are for the developers
348
     * to identify problems and fix them.
349
     *
350
     * @param string $index
351
     * @param int $group
352
     * @param Exception $exception
353
     * @throws GuzzleException
354
     * @throws ValidationException
355
     */
356
    private function logException($index, int $group, Exception $exception): void
357
    {
358
        $this->getLogger()->error($exception->getMessage());
359
        $msg = sprintf(
360
            'Error indexing core %s on group %s,' . PHP_EOL .
361
            'Please log in to the CMS to find out more about Indexing errors' . PHP_EOL,
362
            $index,
363
            $group
364
        );
365
        SolrLogger::logMessage('ERROR', $msg, $index);
366
    }
367
}
368