zhuravljov /
yii2-queue-monitor
| 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
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
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
Loading history...
|
|||||
| 123 | return; |
||||
| 124 | } |
||||
| 125 | static::$startedPush = $push = $this->getPushRecord($event); |
||||
| 126 | if (!$push) { |
||||
|
0 ignored issues
–
show
|
|||||
| 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
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 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
|
|||||
| 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
|
|||||
| 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
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
Loading history...
|
|||||
| 163 | return; |
||||
| 164 | } |
||||
| 165 | $push = static::$startedPush ?: $this->getPushRecord($event); |
||||
| 166 | if (!$push) { |
||||
|
0 ignored issues
–
show
|
|||||
| 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
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 For 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' => (bool) $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
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 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
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 The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes. 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 |