Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.33% covered (danger)
31.33%
52 / 166
17.86% covered (danger)
17.86%
5 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
TaskLog
31.33% covered (danger)
31.33%
52 / 166
17.86% covered (danger)
17.86%
5 / 28
1997.31
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
3
 getBroker
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isRds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createContainer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setStatus
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
3.71
 getStatus
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
3.69
 setReport
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getReport
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 updateParent
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
132
 search
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTaskExecutionTimesByDateRange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDataTablePayload
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getById
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getByIdAndUser
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 findAvailableByUser
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getStats
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 archive
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 cancel
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 archiveCollection
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 cancelCollection
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 linkTaskToCategory
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 getCategoryForTask
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 getTaskCategories
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 validateStatus
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 checkIfCanArchive
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 checkIfCanCancel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 getFileSystemService
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017-2021 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\tao\model\taskQueue;
23
24use Datetime;
25use common_report_Report as Report;
26use oat\oatbox\event\EventManager;
27use oat\oatbox\filesystem\FileSystemService;
28use oat\oatbox\service\ConfigurableService;
29use oat\oatbox\log\LoggerAwareTrait;
30use oat\tao\model\taskQueue\Event\TaskLogArchivedEvent;
31use oat\tao\model\taskQueue\Event\TaskLogCancelledEvent;
32use oat\tao\model\taskQueue\Task\FilesystemAwareTrait;
33use oat\tao\model\taskQueue\Task\TaskInterface;
34use oat\tao\model\taskQueue\TaskLog\Broker\RdsTaskLogBrokerInterface;
35use oat\tao\model\taskQueue\TaskLog\Broker\TaskLogBrokerInterface;
36use oat\tao\model\taskQueue\TaskLog\CollectionInterface;
37use oat\tao\model\taskQueue\TaskLog\DataTablePayload;
38use oat\tao\model\taskQueue\TaskLog\Entity\EntityInterface;
39use oat\tao\model\taskQueue\TaskLog\TaskLogCollection;
40use oat\tao\model\taskQueue\TaskLog\TaskLogFilter;
41use oat\tao\model\datatable\DatatableRequest as DatatableRequestInterface;
42
43/**
44 * Managing task logs:
45 * - storing every information for a task like dates, status changes, reports etc.
46 * - each task has one record in the container identified by its id
47 *
48 * @author Gyula Szucs <gyula@taotesting.com>
49 */
50class TaskLog extends ConfigurableService implements TaskLogInterface
51{
52    use LoggerAwareTrait;
53    use FilesystemAwareTrait;
54
55    /**
56     * @var TaskLogBrokerInterface
57     */
58    private $broker;
59
60    /**
61     * TaskLog constructor.
62     *
63     * @param array $options
64     */
65    public function __construct(array $options)
66    {
67        parent::__construct($options);
68
69        if (!$this->hasOption(self::OPTION_TASK_LOG_BROKER) || empty($this->getOption(self::OPTION_TASK_LOG_BROKER))) {
70            throw new \InvalidArgumentException("Task Log Broker service needs to be set.");
71        }
72    }
73
74    /**
75     * Gets the task log broker. It will be created if it has not been initialized.
76     *
77     * @return TaskLogBrokerInterface
78     */
79    public function getBroker()
80    {
81        if (is_null($this->broker)) {
82            $this->broker = $this->getOption(self::OPTION_TASK_LOG_BROKER);
83            $this->broker->setServiceLocator($this->getServiceLocator());
84        }
85
86        return $this->broker;
87    }
88
89    /**
90     * @inheritdoc
91     */
92    public function isRds()
93    {
94        return $this->getBroker() instanceof RdsTaskLogBrokerInterface;
95    }
96
97    /**
98     * @inheritdoc
99     */
100    public function createContainer()
101    {
102        $this->getBroker()->createContainer();
103    }
104
105    /**
106     * @inheritdoc
107     */
108    public function add(TaskInterface $task, $status, $label = null)
109    {
110        try {
111            $this->validateStatus($status);
112
113            $this->getBroker()->add($task, $status, $label);
114        } catch (\Exception $e) {
115            $this->logError('Adding result for task ' . $task->getId() . ' failed with MSG: ' . $e->getMessage());
116        }
117
118        return $this;
119    }
120
121    /**
122     * @inheritdoc
123     */
124    public function setStatus($taskId, $newStatus, $prevStatus = null)
125    {
126        try {
127            $this->validateStatus($newStatus);
128
129            if (!is_null($prevStatus)) {
130                $this->validateStatus($prevStatus);
131            }
132
133            return $this->getBroker()->updateStatus($taskId, $newStatus, $prevStatus);
134        } catch (\Exception $e) {
135            $this->logError('Setting the status for task ' . $taskId . ' failed with MSG: ' . $e->getMessage());
136        }
137
138        return 0;
139    }
140
141    /**
142     * @inheritdoc
143     */
144    public function getStatus($taskId)
145    {
146        try {
147            return $this->getBroker()->getStatus($taskId);
148        } catch (\Exception $e) {
149            $this->logError('Getting status for task ' . $taskId . ' failed with MSG: ' . $e->getMessage());
150        }
151
152        return self::STATUS_UNKNOWN;
153    }
154
155    /**
156     * @inheritdoc
157     */
158    public function setReport($taskId, Report $report, $newStatus = null)
159    {
160        try {
161            $this->validateStatus($newStatus);
162
163            if (!$this->getBroker()->addReport($taskId, $report, $newStatus)) {
164                throw new \RuntimeException("Report is not saved.");
165            }
166        } catch (\Exception $e) {
167            $this->logError('Setting report for item ' . $taskId . ' failed with MSG: ' . $e->getMessage());
168        }
169
170        return $this;
171    }
172
173    /**
174     * @inheritdoc
175     */
176    public function getReport($taskId)
177    {
178        try {
179            return $this->getBroker()->getReport($taskId);
180        } catch (\Exception $e) {
181            $this->logError('Getting report for task ' . $taskId . ' failed with MSG: ' . $e->getMessage());
182        }
183
184        return null;
185    }
186
187    /**
188     * @inheritdoc
189     */
190    public function updateParent($parentTaskId)
191    {
192        try {
193            $filter = (new TaskLogFilter())
194                ->eq(TaskLogBrokerInterface::COLUMN_PARENT_ID, $parentTaskId)
195                ->neq(TaskLogBrokerInterface::COLUMN_STATUS, TaskLogInterface::STATUS_ARCHIVED);
196
197            $children = $this->search($filter);
198            if (!$children->isEmpty()) {
199                $processedOnes = 0;
200                $failedOnes = 0;
201                foreach ($children as $child) {
202                    // no need update if any child is still in progress
203                    if ($child->getStatus()->isInProgress() || $child->getStatus()->isCreated()) {
204                        break;
205                    }
206
207                    if ($child->getStatus()->isCompleted() || $child->getStatus()->isFailed()) {
208                        $processedOnes++;
209                    }
210
211                    if ($child->getStatus()->isFailed()) {
212                        $failedOnes++;
213                    }
214                }
215
216                // we can update the parent status if every child has been processed
217                if ($processedOnes == $children->count()) {
218                    $this->setStatus($parentTaskId, $failedOnes > 0 ? self::STATUS_FAILED : self::STATUS_COMPLETED);
219                }
220            }
221        } catch (\Exception $e) {
222            $this->logError('Updating parent task "' . $parentTaskId . '"" failed with MSG: ' . $e->getMessage());
223        }
224
225        return $this;
226    }
227
228    /**
229     * @inheritdoc
230     */
231    public function search(TaskLogFilter $filter)
232    {
233        return $this->getBroker()->search($filter);
234    }
235
236    /**
237     * @inheritdoc
238     */
239    public function getTaskExecutionTimesByDateRange(DateTime $from, DateTime $to): array
240    {
241        return $this->getBroker()->getTaskExecutionTimesByDateRange($from, $to);
242    }
243
244    /**
245     * @inheritdoc
246     */
247    public function getDataTablePayload(TaskLogFilter $filter, DatatableRequestInterface $request)
248    {
249        return new DataTablePayload($filter, $this->getBroker(), $request);
250    }
251
252    /**
253     * @inheritdoc
254     */
255    public function getById($taskId)
256    {
257        $filter = (new TaskLogFilter())
258            ->eq(TaskLogBrokerInterface::COLUMN_ID, $taskId);
259
260        $collection = $this->search($filter);
261
262        if ($collection->isEmpty()) {
263            throw new \common_exception_NotFound('Task log for task "' . $taskId . '" not found');
264        }
265
266        return $collection->first();
267    }
268
269    /**
270     * @inheritdoc
271     */
272    public function getByIdAndUser($taskId, $userId, $archivedAllowed = false)
273    {
274        $filter = (new TaskLogFilter())
275            ->addAvailableFilters($userId, $archivedAllowed)
276            ->eq(TaskLogBrokerInterface::COLUMN_ID, $taskId);
277
278        $collection = $this->search($filter);
279
280        if ($collection->isEmpty()) {
281            throw new \common_exception_NotFound('Task log for task "' . $taskId . '" not found');
282        }
283
284        return $collection->first();
285    }
286
287    /**
288     * @inheritdoc
289     */
290    public function findAvailableByUser($userId, $limit = null, $offset = null)
291    {
292        $filter = (new TaskLogFilter())
293            ->withIgnoredTasks($this->getOption(self::OPTION_TASK_IGNORE_LIST, []))
294            ->addAvailableFilters($userId)
295            ->setLimit(is_null($limit) ? self::DEFAULT_LIMIT : $limit)
296            ->setOffset(is_null($offset) ? 0 : $offset);
297
298        return $this->getBroker()->search($filter);
299    }
300
301    /**
302     * @inheritdoc
303     */
304    public function getStats($userId)
305    {
306        $filter = (new TaskLogFilter())
307            ->addAvailableFilters($userId);
308
309        return $this->getBroker()->getStats($filter);
310    }
311
312    /**
313     * @inheritdoc
314     */
315    public function archive(EntityInterface $entity, $forceArchive = false)
316    {
317        $this->checkIfCanArchive($entity, $forceArchive);
318
319        $isArchived = $this->getBroker()->archive($entity);
320
321        if ($isArchived) {
322            $this->getServiceManager()->get(EventManager::SERVICE_ID)
323                ->trigger(new TaskLogArchivedEvent($entity, $forceArchive));
324        }
325
326        if ($this->getCategoryForTask($entity->getTaskName()) == self::CATEGORY_EXPORT) {
327            $this->deleteQueueStorageFile($entity);
328        }
329
330        return $isArchived;
331    }
332
333    /**
334     * @inheritdoc
335     */
336    public function cancel(EntityInterface $entity, $forceCancel = false)
337    {
338        $this->checkIfCanCancel($entity, $forceCancel);
339
340        $isCancelled = $this->getBroker()->cancel($entity);
341
342        if ($isCancelled) {
343            $this->getServiceManager()
344                ->get(EventManager::SERVICE_ID)
345                ->trigger(new TaskLogCancelledEvent($entity, $forceCancel));
346        }
347
348        return $isCancelled;
349    }
350
351    /**
352     * @inheritdoc
353     */
354    public function archiveCollection(CollectionInterface $collection, $forceArchive = false)
355    {
356        $tasksAbleToArchive = [];
357
358        /** @var EntityInterface $entity */
359        foreach ($collection as $entity) {
360            try {
361                $this->checkIfCanArchive($entity, $forceArchive);
362                $tasksAbleToArchive[] = $entity;
363            } catch (\Exception $exception) {
364                $this->logDebug('Task Log: ' . $entity->getId() . ' cannot be archived.');
365            }
366        }
367
368        $collectionArchived = $this->getBroker()->archiveCollection(new TaskLogCollection($tasksAbleToArchive));
369
370        if ($collectionArchived) {
371            foreach ($tasksAbleToArchive as $entity) {
372                $this->getServiceManager()
373                    ->get(EventManager::SERVICE_ID)
374                    ->trigger(new TaskLogArchivedEvent($entity, $forceArchive));
375
376                if ($this->getCategoryForTask($entity->getTaskName()) == self::CATEGORY_EXPORT) {
377                    $this->deleteQueueStorageFile($entity);
378                }
379            }
380        }
381
382        return count($collection) === count($tasksAbleToArchive) && $collectionArchived;
383    }
384
385    /**
386     * @inheritdoc
387     */
388    public function cancelCollection(CollectionInterface $collection, $forceCancel = false)
389    {
390        $cancellableTasks = [];
391
392        /** @var EntityInterface $entity */
393        foreach ($collection as $entity) {
394            try {
395                $this->checkIfCanCancel($entity, $forceCancel);
396                $cancellableTasks[] = $entity;
397            } catch (\Exception $exception) {
398                $this->logDebug('Task Log: ' . $entity->getId() . ' cannot be cancelled.');
399            }
400        }
401
402        $cancelledCollection = $this->getBroker()->cancelCollection(new TaskLogCollection($cancellableTasks));
403
404        if ($cancelledCollection) {
405            foreach ($cancellableTasks as $entity) {
406                $this->getServiceManager()
407                    ->get(EventManager::SERVICE_ID)
408                    ->trigger(new TaskLogCancelledEvent($entity, $forceCancel));
409            }
410        }
411
412        return count($collection) === count($cancellableTasks) && $cancelledCollection;
413    }
414
415    /**
416     * @inheritdoc
417     */
418    public function linkTaskToCategory($taskName, $category)
419    {
420        if (is_object($taskName)) {
421            $taskName = get_class($taskName);
422        }
423
424        if (!in_array($category, $this->getTaskCategories())) {
425            throw new \InvalidArgumentException('Category "' . $category . '" is not a valid category.');
426        }
427
428        $associations = (array) $this->getOption(self::OPTION_TASK_TO_CATEGORY_ASSOCIATIONS);
429
430        $associations[ (string) $taskName ] = $category;
431
432        $this->setOption(self::OPTION_TASK_TO_CATEGORY_ASSOCIATIONS, $associations);
433    }
434
435    /**
436     * @inheritdoc
437     */
438    public function getCategoryForTask($taskName)
439    {
440        if (is_object($taskName)) {
441            $taskName = get_class($taskName);
442        }
443
444        $associations = (array) $this->getOption(self::OPTION_TASK_TO_CATEGORY_ASSOCIATIONS);
445
446        if (array_key_exists($taskName, $associations)) {
447            return $associations[$taskName];
448        }
449
450        // check by inheritance
451        foreach ($associations as $className => $category) {
452            if (is_subclass_of($taskName, $className)) {
453                return $category;
454            }
455        }
456
457        return self::CATEGORY_UNKNOWN;
458    }
459
460    /**
461     * @return array
462     */
463    public function getTaskCategories()
464    {
465        return [
466            self::CATEGORY_CREATE,
467            self::CATEGORY_UPDATE,
468            self::CATEGORY_DELETE,
469            self::CATEGORY_COPY,
470            self::CATEGORY_IMPORT,
471            self::CATEGORY_EXPORT,
472            self::CATEGORY_DELIVERY_COMPILATION,
473            self::CATEGORY_UNRELATED_RESOURCE,
474        ];
475    }
476
477    /**
478     * @param string $status
479     */
480    protected function validateStatus($status)
481    {
482        $statuses = [
483            self::STATUS_ENQUEUED,
484            self::STATUS_DEQUEUED,
485            self::STATUS_RUNNING,
486            self::STATUS_CHILD_RUNNING,
487            self::STATUS_COMPLETED,
488            self::STATUS_FAILED,
489            self::STATUS_ARCHIVED,
490            self::STATUS_CANCELLED
491        ];
492
493        if (!in_array($status, $statuses)) {
494            throw new \InvalidArgumentException('Status "' . $status . '"" is not a valid task queue status.');
495        }
496    }
497
498    /**
499     * @param EntityInterface $entity
500     * @param $forceArchive
501     * @throws \common_Exception
502     */
503    protected function checkIfCanArchive(EntityInterface $entity, $forceArchive)
504    {
505        if ($entity->getStatus()->isInProgress() && $forceArchive === false) {
506            throw new \common_Exception('Task cannot be archived because it is in progress.');
507        }
508    }
509
510    /**
511     * @param EntityInterface $entity
512     * @param bool $forceCancel
513     * @throws \common_Exception
514     */
515    protected function checkIfCanCancel(EntityInterface $entity, $forceCancel)
516    {
517        if (!$entity->getStatus()->isCreated() && $forceCancel === false) {
518            throw new \common_Exception('Task cannot be cancelled because it is already dequeued.');
519        }
520    }
521
522    /**
523     * @see FilesystemAwareTrait::getFileSystemService()
524     * @return FileSystemService|object
525     */
526    protected function getFileSystemService()
527    {
528        return $this->getServiceLocator()
529            ->get(FileSystemService::SERVICE_ID);
530    }
531}