Passed
Push — master ( 62ccc4...3821b3 )
by Roman
01:54
created

JobMonitor::afterExec()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 22
rs 8.4444
c 0
b 0
f 0
cc 8
nc 6
nop 1
1
<?php
2
/**
3
 * @link https://github.com/zhuravljov/yii2-queue-monitor
4
 * @copyright Copyright (c) 2017 Roman Zhuravlev
5
 * @license http://opensource.org/licenses/BSD-3-Clause
6
 */
7
8
namespace zhuravljov\yii\queue\monitor;
9
10
use Yii;
11
use yii\base\Behavior;
12
use yii\base\InvalidConfigException;
13
use yii\helpers\ArrayHelper;
14
use yii\helpers\VarDumper;
15
use yii\queue\ExecEvent;
16
use yii\queue\JobEvent;
17
use yii\queue\JobInterface;
18
use yii\queue\PushEvent;
19
use yii\queue\Queue;
20
use zhuravljov\yii\queue\monitor\records\ExecRecord;
21
use zhuravljov\yii\queue\monitor\records\PushRecord;
22
use zhuravljov\yii\queue\monitor\records\WorkerRecord;
23
24
/**
25
 * Queue Job Monitor
26
 *
27
 * @author Roman Zhuravlev <[email protected]>
28
 */
29
class JobMonitor extends Behavior
30
{
31
    /**
32
     * @var array of job class names that this behavior should tracks.
33
     * @since 0.3.2
34
     */
35
    public $only = [];
36
    /**
37
     * @var array of job class names that this behavior should not tracks.
38
     * @since 0.3.2
39
     */
40
    public $except = [];
41
    /**
42
     * @var array
43
     */
44
    public $contextVars = [
45
        '_SERVER.argv',
46
        '_SERVER.REQUEST_METHOD',
47
        '_SERVER.REQUEST_URI',
48
        '_SERVER.HTTP_REFERER',
49
        '_SERVER.HTTP_USER_AGENT',
50
        '_POST',
51
    ];
52
    /**
53
     * @var Queue
54
     * @inheritdoc
55
     */
56
    public $owner;
57
    /**
58
     * @var Env
59
     */
60
    protected $env;
61
    /**
62
     * @var null|PushRecord
63
     */
64
    protected static $startedPush;
65
66
    /**
67
     * @param Env $env
68
     * @param array $config
69
     */
70
    public function __construct(Env $env, $config = [])
71
    {
72
        $this->env = $env;
73
        parent::__construct($config);
74
    }
75
76
    /**
77
     * @inheritdoc
78
     */
79
    public function events()
80
    {
81
        return [
82
            Queue::EVENT_AFTER_PUSH => 'afterPush',
83
            Queue::EVENT_BEFORE_EXEC => 'beforeExec',
84
            Queue::EVENT_AFTER_EXEC => 'afterExec',
85
            Queue::EVENT_AFTER_ERROR => 'afterExec',
86
        ];
87
    }
88
89
    /**
90
     * @param PushEvent $event
91
     */
92
    public function afterPush(PushEvent $event)
93
    {
94
        if (!$this->isActive($event->job)) {
0 ignored issues
show
Bug introduced by
It seems like $event->job can also be of type null; however, parameter $job of zhuravljov\yii\queue\mon...\JobMonitor::isActive() does only seem to accept yii\queue\JobInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

94
        if (!$this->isActive(/** @scrutinizer ignore-type */ $event->job)) {
Loading history...
95
            return;
96
        }
97
98
        if ($this->env->db->getTransaction()) {
99
            // create new database connection, if there is an open transaction
100
            // to ensure insert statement is not affected by a rollback
101
            $this->env->db = clone $this->env->db;
102
        }
103
104
        $push = new PushRecord();
105
        $push->parent_id = static::$startedPush ? static::$startedPush->id : null;
106
        $push->sender_name = $this->getSenderName($event);
107
        $push->job_uid = $event->id;
108
        $push->setJob($event->job);
109
        $push->ttr = $event->ttr;
110
        $push->delay = $event->delay;
111
        $push->trace = (new \Exception())->getTraceAsString();
112
        $push->context = $this->getContext();
113
        $push->pushed_at = time();
114
        $push->save(false);
115
    }
116
117
    /**
118
     * @param ExecEvent $event
119
     */
120
    public function beforeExec(ExecEvent $event)
121
    {
122
        if (!$this->isActive($event->job)) {
0 ignored issues
show
Bug introduced by
It seems like $event->job can also be of type null; however, parameter $job of zhuravljov\yii\queue\mon...\JobMonitor::isActive() does only seem to accept yii\queue\JobInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
        if (!$this->isActive(/** @scrutinizer ignore-type */ $event->job)) {
Loading history...
123
            return;
124
        }
125
        static::$startedPush = $push = $this->getPushRecord($event);
126
        if (!$push) {
0 ignored issues
show
introduced by
$push is of type zhuravljov\yii\queue\monitor\records\PushRecord, thus it always evaluated to true.
Loading history...
127
            return;
128
        }
129
        if ($push->isStopped()) {
130
            // Rejects job execution in case is stopped
131
            $event->handled = true;
132
            return;
133
        }
134
        $this->env->db->transaction(function () use ($event, $push) {
135
            $worker = $this->getWorkerRecord($event);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $worker is correct as $this->getWorkerRecord($event) targeting zhuravljov\yii\queue\mon...itor::getWorkerRecord() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
136
137
            $exec = new ExecRecord();
138
            $exec->push_id = $push->id;
139
            if ($worker) {
0 ignored issues
show
introduced by
$worker is of type null, thus it always evaluated to false.
Loading history...
140
                $exec->worker_id = $worker->id;
141
            }
142
            $exec->attempt = $event->attempt;
143
            $exec->started_at = time();
144
            $exec->save(false);
145
146
            $push->first_exec_id = $push->first_exec_id ?: $exec->id;
147
            $push->last_exec_id = $exec->id;
148
            $push->save(false);
149
150
            if ($worker) {
0 ignored issues
show
introduced by
$worker is of type null, thus it always evaluated to false.
Loading history...
151
                $worker->last_exec_id = $exec->id;
152
                $worker->save(false);
153
            }
154
        });
155
    }
156
157
    /**
158
     * @param ExecEvent $event
159
     */
160
    public function afterExec(ExecEvent $event)
161
    {
162
        if (!$this->isActive($event->job)) {
0 ignored issues
show
Bug introduced by
It seems like $event->job can also be of type null; however, parameter $job of zhuravljov\yii\queue\mon...\JobMonitor::isActive() does only seem to accept yii\queue\JobInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
        if (!$this->isActive(/** @scrutinizer ignore-type */ $event->job)) {
Loading history...
163
            return;
164
        }
165
        $push = static::$startedPush ?: $this->getPushRecord($event);
166
        if (!$push) {
0 ignored issues
show
introduced by
$push is of type zhuravljov\yii\queue\monitor\records\PushRecord, thus it always evaluated to true.
Loading history...
167
            return;
168
        }
169
        if ($push->isStopped()) {
170
            // Breaks retry in case is stopped
171
            $event->retry = false;
172
        }
173
        if ($push->last_exec_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $push->last_exec_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
174
            ExecRecord::updateAll([
175
                'finished_at' => time(),
176
                'memory_usage' => static::$startedPush ? memory_get_peak_usage() : null,
177
                'error' => $event->error,
178
                'result_data' => $event->result !== null ? serialize($event->result) : null,
179
                'retry' => $event->retry,
180
            ], [
181
                'id' => $push->last_exec_id
182
            ]);
183
        }
184
    }
185
186
    /**
187
     * @param JobInterface $job
188
     * @return bool
189
     * @since 0.3.2
190
     */
191
    protected function isActive(JobInterface $job)
192
    {
193
        $onlyMatch = true;
194
        if ($this->only) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->only of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
195
            $onlyMatch = false;
196
            foreach ($this->only as $className) {
197
                if (is_a($job, $className)) {
198
                    $onlyMatch = true;
199
                    break;
200
                }
201
            }
202
        }
203
204
        $exceptMatch = false;
205
        foreach ($this->except as $className) {
206
            if (is_a($job, $className)) {
207
                $exceptMatch = true;
208
                break;
209
            }
210
        }
211
212
        return !$exceptMatch && $onlyMatch;
213
    }
214
215
    /**
216
     * @param JobEvent $event
217
     * @throws
218
     * @return string
219
     */
220
    protected function getSenderName($event)
221
    {
222
        foreach (Yii::$app->getComponents(false) as $id => $component) {
223
            if ($component === $event->sender) {
224
                return $id;
225
            }
226
        }
227
        throw new InvalidConfigException('Queue must be an application component.');
228
    }
229
230
    /**
231
     * @return string
232
     */
233
    protected function getContext()
234
    {
235
        $context = ArrayHelper::filter($GLOBALS, $this->contextVars);
236
        $result = [];
237
        foreach ($context as $key => $value) {
238
            $result[] = "\${$key} = " . VarDumper::dumpAsString($value);
239
        }
240
241
        return implode("\n\n", $result);
242
    }
243
244
    /**
245
     * @param JobEvent $event
246
     * @return PushRecord
247
     */
248
    protected function getPushRecord(JobEvent $event)
249
    {
250
        if ($event->id !== null) {
251
            return $this->env->db->useMaster(function () use ($event) {
252
                return PushRecord::find()
253
                    ->byJob($this->getSenderName($event), $event->id)
254
                    ->one();
255
            });
256
        } else {
257
            return null;
258
        }
259
    }
260
261
    /**
262
     * @param ExecEvent $event
263
     * @return WorkerRecord|null
264
     */
265
    protected function getWorkerRecord(ExecEvent $event)
266
    {
267
        if ($event->sender->getWorkerPid() === null) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $event->sender->getWorkerPid() targeting yii\queue\Queue::getWorkerPid() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
introduced by
The condition $event->sender->getWorkerPid() === null is always true.
Loading history...
268
            return null;
269
        }
270
        if (!$this->isWorkerMonitored()) {
271
            return null;
272
        }
273
274
        return $this->env->db->useMaster(function () use ($event) {
275
            return WorkerRecord::find()
276
                ->byEvent($this->env->getHost(), $event->sender->getWorkerPid())
277
                ->active()
278
                ->one();
279
        });
280
    }
281
282
    /**
283
     * @return bool whether workers are monitored.
284
     */
285
    private function isWorkerMonitored()
286
    {
287
        foreach ($this->owner->getBehaviors() as $behavior) {
288
            if ($behavior instanceof WorkerMonitor) {
289
                return true;
290
            }
291
        }
292
        return false;
293
    }
294
}
295