Completed
Push — 2.0 ( 6e1974...7f0480 )
by Marco
13:21
created

Manager::__construct()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 5
1
<?php namespace Comodojo\Extender\Task;
2
3
use \Comodojo\Foundation\Base\Configuration;
4
use \Comodojo\Foundation\Events\Manager as EventsManager;
5
use \Comodojo\Foundation\Logging\LoggerTrait;
6
use \Comodojo\Foundation\Events\EventsTrait;
7
use \Comodojo\Daemon\Utils\ProcessTools;
8
use \Comodojo\Foundation\Base\ConfigurationTrait;
9
use \Comodojo\Extender\Traits\TasksTableTrait;
10
use \Comodojo\Extender\Traits\TaskErrorHandlerTrait;
11
use \Comodojo\Extender\Utils\Validator as ExtenderCommonValidations;
12
use \Comodojo\Extender\Components\Ipc;
13
use \Comodojo\Extender\Task\Table as TasksTable;
14
use \Comodojo\Extender\Components\Database;
15
use \Psr\Log\LoggerInterface;
16
use \DateTime;
17
use \Exception;
18
19
/**
20
* @package     Comodojo Extender
21
* @author      Marco Giovinazzi <[email protected]>
22
* @license     MIT
23
*
24
* LICENSE:
25
*
26
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32
* THE SOFTWARE.
33
 */
34
35
class Manager {
36
37
    use ConfigurationTrait;
38
    use LoggerTrait;
39
    use EventsTrait;
40
    use TasksTableTrait;
41
    use TaskErrorHandlerTrait;
42
43
    /**
44
     * @var int
45
     */
46
    protected $lagger_timeout;
47
48
    /**
49
     * @var bool
50
     */
51
    protected $multithread;
52
53
    /**
54
     * @var int
55
     */
56
    protected $max_runtime;
57
58
    /**
59
     * @var int
60
     */
61
    protected $max_childs;
62
63
    /**
64
     * @var Ipc
65
     */
66
    protected $ipc;
67
68
    /**
69
     * @var Locker
70
     */
71
    protected $locker;
72
73
    /**
74
     * @var Tracker
75
     */
76
    protected $tracker;
77
78
    /**
79
     * Class constructor
80
     *
81
     * @param string $manager_name
0 ignored issues
show
Bug introduced by
There is no parameter named $manager_name. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
82
     * @param Configuration $configuration
83
     * @param LoggerInterface $logger
84
     * @param TasksTable $tasks
85
     * @param EventsManager $events
86
     * @param EntityManager $em
0 ignored issues
show
Bug introduced by
There is no parameter named $em. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
87
     */
88
    public function __construct(
89
        Locker $locker,
90
        Configuration $configuration,
91
        LoggerInterface $logger,
92
        TasksTable $tasks,
93
        EventsManager $events
94
    ) {
95
96
        $this->setConfiguration($configuration);
97
        $this->setLogger($logger);
98
        $this->setTasksTable($tasks);
99
        $this->setEvents($events);
100
101
        $this->locker = $locker;
102
        $this->tracker = new Tracker($configuration, $logger);
103
        $this->ipc = new Ipc($configuration);
104
105
        // retrieve parameters
106
        $this->lagger_timeout = ExtenderCommonValidations::laggerTimeout($this->configuration->get('child-lagger-timeout'));
107
        $this->multithread = ExtenderCommonValidations::multithread($this->configuration->get('multithread'));
108
        $this->max_runtime = ExtenderCommonValidations::maxChildRuntime($this->configuration->get('child-max-runtime'));
109
        $this->max_childs = ExtenderCommonValidations::forkLimit($this->configuration->get('fork-limit'));
110
111
        // $logger->debug("Tasks Manager online", array(
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
112
        //     'lagger_timeout' => $this->lagger_timeout,
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
113
        //     'multithread' => $this->multithread,
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
114
        //     'max_runtime' => $this->max_runtime,
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
115
        //     'max_childs' => $this->max_childs,
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
116
        //     'tasks_count' => count($this->table)
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
117
        // ));
118
119
    }
120
121
    public function add(Request $request) {
122
123
        $this->tracker->setQueued($request);
124
125
        return $this;
126
127
    }
128
129
    public function addBulk(array $requests) {
130
131
        foreach ($requests as $id => $request) {
132
133
            if ($request instanceof \Comodojo\Extender\Task\Request) {
134
                $this->add($request);
135
            } else {
136
                $this->logger->error("Skipping invalid request with local id $id: class mismatch");
137
            }
138
139
        }
140
141
        return $this;
142
143
    }
144
145
    public function run() {
146
147
        $this->updateTrackerSetQueued();
148
149
        while ( $this->tracker->countQueued() > 0 ) {
150
151
            // Start to cycle queued tasks
152
            $this->cycle();
153
154
        }
155
156
        $this->ipc->free();
157
158
        return $this->tracker->getCompleted();
159
160
    }
161
162
    protected function cycle() {
163
164
        $this->installErrorHandler();
165
166
        foreach ($this->tracker->getQueued() as $uid => $request) {
167
168
            if ( $this->multithread === false ) {
169
170
                $this->runSingleThread($uid, $request);
171
172
            } else {
173
174
                try {
175
176
                    $pid = $this->forker($request);
177
178
                } catch (Exception $e) {
179
180
                    $result = self::generateSyntheticResult($uid, $e->getMessage(), $request->getJid(), false);
181
182
                    $this->updateTrackerSetAborted($uid, $result);
183
184
                    if ( $request->isChain() ) $this->evalChain($request, $result);
185
186
                    continue;
187
188
                }
189
190
                $this->updateTrackerSetRunning($uid, $pid);
191
192
                if ( $this->max_childs > 0 && $this->tracker->countRunning() >= $this->max_childs ) {
193
194
                    while( $this->tracker->countRunning() >= $this->max_childs ) {
195
196
                        $this->catcher();
197
198
                    }
199
200
                }
201
202
            }
203
204
        }
205
206
        // spawn the loop if multithread
207
        if ( $this->multithread === true ) {
208
            $this->catcher_loop();
209
        } else {
210
            $this->restoreErrorHandler();
211
        }
212
213
    }
214
215
    protected function runSingleThread($uid, Request $request) {
216
217
        $pid = ProcessTools::getPid();
218
219
        $this->updateTrackerSetRunning($uid, $pid);
220
221
        $result = Runner::fastStart(
222
            $request,
223
            $this->getConfiguration(),
224
            $this->getLogger(),
225
            $this->getTasksTable(),
226
            $this->getEvents()
227
        );
228
229
        if ( $request->isChain() ) $this->evalChain($request, $result);
230
231
        $this->updateTrackerSetCompleted($uid, $result);
232
233
        $success = $result->success === false ? "error" : "success";
0 ignored issues
show
Documentation introduced by
The property success does not exist on object<Comodojo\Extender\Task\Result>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
234
        $this->logger->notice("Task ".$request->getName()."(uid: ".$request->getUid().") ends in $success");
235
236
    }
237
238
    private function forker(Request $request) {
239
240
        $uid = $request->getUid();
241
242
        try {
243
244
            $this->ipc->init($uid);
245
246
        } catch (Exception $e) {
247
248
            $this->logger->error("Aborting task ".$request->getName().": ".$e->getMessage());
249
250
            $this->ipc->hang($uid);
251
252
            throw $e;
253
254
        }
255
256
        $pid = pcntl_fork();
257
258
        if ( $pid == -1 ) {
259
260
            throw new Exception("Unable to fork job, aborting");
261
262
        } elseif ( $pid ) {
263
264
            $niceness = $request->getNiceness();
265
266
            if ( $niceness !== null ) ProcessTools::setNiceness($niceness, $pid);
267
268
        } else {
269
270
            $this->ipc->close($uid, Ipc::READER);
271
272
            $result = Runner::fastStart(
273
                $request,
274
                $this->getConfiguration(),
275
                $this->getLogger(),
276
                $this->getTasksTable(),
277
                $this->getEvents()
278
            );
279
280
            $this->ipc->write($uid, serialize($result));
281
282
            $this->ipc->close($uid, Ipc::WRITER);
283
284
            exit(!$result->success);
285
286
        }
287
288
        return $pid;
289
290
    }
291
292
    private function catcher_loop() {
293
294
        while ( !empty($this->tracker->getRunning()) ) {
295
296
            $this->catcher();
297
298
        }
299
300
        $this->restoreErrorHandler();
301
302
    }
303
304
    /**
305
     * Catch results from completed jobs
306
     *
307
     */
308
    private function catcher() {
309
310
        foreach ( $this->tracker->getRunning() as $uid => $request ) {
311
312
            if ( ProcessTools::isRunning($request->getPid()) === false ) {
313
314
                $this->ipc->close($uid, Ipc::WRITER);
315
316
                try {
317
318
                    $raw_output = $this->ipc->read($uid);
319
320
                    $result = unserialize(rtrim($raw_output));
321
322
                    $this->ipc->close($uid, Ipc::READER);
323
324
                } catch (Exception $e) {
325
326
                    $result = self::generateSyntheticResult($uid, $e->getMessage(), $request->getJid(), false);
327
328
                }
329
330
                if ( $request->isChain() ) $this->evalChain($request, $result);
331
332
                $this->updateTrackerSetCompleted($uid, $result);
333
334
                $success = $result->success === false ? "error" : "success";
335
                $this->logger->notice("Task ".$request->getName()."(uid: ".$request->getUid().") ends in $success");
336
337
            } else {
338
339
                $current_time = microtime(true);
340
341
                $request_max_time = $request->getMaxtime();
342
                $maxtime = $request_max_time === null ? $this->max_runtime : $request_max_time;
343
344
                if ( $current_time > $request->getStartTimestamp() + $maxtime ) {
345
346
                    $pid = $request->getPid();
347
348
                    $this->logger->warning("Killing pid $pid due to maximum exec time reached", [
349
                        "START_TIME"    => $request->getStartTimestamp(),
350
                        "CURRENT_TIME"  => $current_time,
351
                        "MAX_RUNTIME"   => $maxtime
352
                    ]);
353
354
                    $kill = ProcessTools::term($pid, $this->lagger_timeout);
355
356
                    if ( $kill ) {
357
                        $this->logger->warning("Pid $pid killed");
358
                    } else {
359
                        $this->logger->warning("Pid $pid could not be killed");
360
                    }
361
362
                    $this->ipc->hang($uid);
363
364
                    $result = self::generateSyntheticResult($uid, "Job killed due to max runtime reached", $request->getJid(), false);
365
366
                    if ( $request->isChain() ) $this->evalChain($request, $result);
367
368
                    $this->updateTrackerSetCompleted($uid, $result);
369
370
                    $this->logger->notice("Task ".$request->getName()."(uid: $uid) ends in error");
371
372
                }
373
374
            }
375
376
        }
377
378
    }
379
380
    private function evalChain(Request $request, Result $result) {
381
382 View Code Duplication
        if ( $result->success && $request->hasOnDone() ) {
0 ignored issues
show
Documentation introduced by
The property success does not exist on object<Comodojo\Extender\Task\Result>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
383
            $chain_done = $request->getOnDone();
384
            $chain_done->getParameters()->set('parent', $result);
385
            $chain_done->setParentUid($result->uid);
0 ignored issues
show
Documentation introduced by
The property uid does not exist on object<Comodojo\Extender\Task\Result>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
386
            $this->add($chain_done);
387
        }
388
389 View Code Duplication
        if ( $result->success === false && $request->hasOnFail() ) {
0 ignored issues
show
Documentation introduced by
The property success does not exist on object<Comodojo\Extender\Task\Result>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
390
            $chain_fail = $request->getOnFail();
391
            $chain_fail->getParameters()->set('parent', $result);
392
            $chain_fail->setParentUid($result->uid);
0 ignored issues
show
Documentation introduced by
The property uid does not exist on object<Comodojo\Extender\Task\Result>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
393
            $this->add($chain_fail);
394
        }
395
396
        if ( $request->hasPipe() ) {
397
            $chain_pipe = $request->getPipe();
398
            $chain_pipe->getParameters()->set('parent', $result);
399
            $chain_pipe->setParentUid($result->uid);
0 ignored issues
show
Documentation introduced by
The property uid does not exist on object<Comodojo\Extender\Task\Result>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
400
            $this->add($chain_pipe);
401
        }
402
403
    }
404
405
    private function generateSyntheticResult($uid, $message, $jid = null, $success = true) {
406
407
        return new Result([
408
            $uid,
409
            null,
410
            $jid,
411
            null,
412
            $success,
413
            new DateTime(),
414
            null,
415
            $message,
416
            null
417
        ]);
418
419
    }
420
421
    private function updateTrackerSetQueued() {
422
423
        $this->locker->lock([
424
            'QUEUED' => $this->tracker->countQueued()
425
        ]);
426
427
    }
428
429
    private function updateTrackerSetRunning($uid, $pid) {
430
431
        $this->tracker->setRunning($uid, $pid);
432
        $this->locker->lock([
433
            'QUEUED' => $this->tracker->countQueued(),
434
            'RUNNING' => $this->tracker->countRunning()
435
        ]);
436
437
    }
438
439
    private function updateTrackerSetCompleted($uid, $result) {
440
441
        $this->tracker->setCompleted($uid, $result);
442
443
        $lock_data = [
444
            'RUNNING' => $this->tracker->countRunning(),
445
            'COMPLETED' => 1
446
        ];
447
448
        if ( $result->success ) {
449
            $lock_data['SUCCEEDED'] = 1;
450
        } else {
451
            $lock_data['FAILED'] = 1;
452
        }
453
454
        $this->locker->lock($lock_data);
455
456
    }
457
458
    private function updateTrackerSetAborted($uid, $result) {
459
460
        $this->tracker->setAborted($uid, $result);
461
        $lock_data = [
0 ignored issues
show
Unused Code introduced by
$lock_data is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
462
            'RUNNING' => $this->tracker->countRunning(),
463
            'ABORTED' => 1
464
        ];
465
466
    }
467
468
}
469