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

SolrIndexTask::indexClass()   B

Complexity

Conditions 10
Paths 26

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 11.2294

Importance

Changes 15
Bugs 1 Features 0
Metric Value
cc 10
eloc 28
c 15
b 1
f 0
nc 26
nop 4
dl 0
loc 42
ccs 10
cts 13
cp 0.7692
crap 11.2294
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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