├── LICENSE ├── README.md ├── composer.json └── src ├── Env.php ├── JobMonitor.php ├── Module.php ├── WorkerMonitor.php ├── assets ├── JobItemAsset.php └── MainAsset.php ├── base ├── FlashTrait.php └── Migration.php ├── console └── GcController.php ├── controllers ├── JobController.php └── WorkerController.php ├── filters ├── BaseFilter.php ├── JobFilter.php └── WorkerFilter.php ├── messages └── ru │ ├── main.php │ └── notice.php ├── migrations ├── M180807000000Schema.php └── M190420000000ExecResult.php ├── records ├── ExecQuery.php ├── ExecRecord.php ├── PushQuery.php ├── PushRecord.php ├── WorkerQuery.php └── WorkerRecord.php ├── views ├── job │ ├── _data-list.php │ ├── _index-item.php │ ├── _job-filter.php │ ├── _table.php │ ├── _view-nav.php │ ├── index.php │ ├── view-attempts.php │ ├── view-context.php │ ├── view-data.php │ └── view-details.php ├── layouts │ ├── _alerts.php │ └── main.php └── worker │ └── index.php ├── web ├── job-item.css ├── logo.png ├── main.css └── stat-index.js └── widgets └── FilterBar.php /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Roman Zhuravlev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Yii2 Queue Analytics Module 2 | =========================== 3 | 4 | The module collects statistics about working of queues of an application, and provides web interface 5 | for research. Also the module allows to stop and replay any jobs manually. 6 | 7 | [![Latest Stable Version](https://poser.pugx.org/zhuravljov/yii2-queue-monitor/v/stable.svg)](https://packagist.org/packages/zhuravljov/yii2-queue-monitor) 8 | [![Total Downloads](https://poser.pugx.org/zhuravljov/yii2-queue-monitor/downloads.svg)](https://packagist.org/packages/zhuravljov/yii2-queue-monitor) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zhuravljov/yii2-queue-monitor/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zhuravljov/yii2-queue-monitor/?branch=master) 10 | 11 | Installation 12 | ------------ 13 | 14 | The preferred way to install the extension is through [composer](http://getcomposer.org/download/). 15 | Add to the require section of your `composer.json` file: 16 | 17 | ``` 18 | "zhuravljov/yii2-queue-monitor": "~0.1" 19 | ``` 20 | 21 | Usage 22 | ----- 23 | 24 | To configure the statistics collector, you need to add monitor behavior for each queue component. 25 | Update common config file: 26 | 27 | ```php 28 | return [ 29 | 'components' => [ 30 | 'queue' => [ 31 | // ... 32 | 'as jobMonitor' => \zhuravljov\yii\queue\monitor\JobMonitor::class, 33 | 'as workerMonitor' => \zhuravljov\yii\queue\monitor\WorkerMonitor::class, 34 | ], 35 | ], 36 | ]; 37 | ``` 38 | 39 | There are storage options that you can configure by common config file: 40 | 41 | ```php 42 | return [ 43 | 'container' => [ 44 | 'singletons' => [ 45 | \zhuravljov\yii\queue\monitor\Env::class => [ 46 | 'cache' => 'cache', 47 | 'db' => 'db', 48 | 'pushTableName' => '{{%queue_push}}', 49 | 'execTableName' => '{{%queue_exec}}', 50 | 'workerTableName' => '{{%queue_worker}}', 51 | ], 52 | ], 53 | ], 54 | ]; 55 | ``` 56 | 57 | If you want use migrations of the extension, configure migration command in console config: 58 | 59 | ```php 60 | 'controllerMap' => [ 61 | 'migrate' => [ 62 | 'class' => \yii\console\controllers\MigrateController::class, 63 | 'migrationNamespaces' => [ 64 | //... 65 | 'zhuravljov\yii\queue\monitor\migrations', 66 | ], 67 | ], 68 | ], 69 | ``` 70 | 71 | And apply migrations. 72 | 73 | 74 | ### Web 75 | 76 | Finally, modify your web config file to turn on web interface: 77 | 78 | ```php 79 | return [ 80 | 'bootstrap' => [ 81 | 'monitor', 82 | ], 83 | 'modules' => [ 84 | 'monitor' => [ 85 | 'class' => \zhuravljov\yii\queue\monitor\Module::class, 86 | ], 87 | ], 88 | ]; 89 | ``` 90 | 91 | It will be available by URL `http://yourhost.com/monitor`. 92 | 93 | 94 | ### Console 95 | 96 | There is console garbage collector: 97 | 98 | ```php 99 | 'controllerMap' => [ 100 | 'monitor' => [ 101 | 'class' => \zhuravljov\yii\queue\monitor\console\GcController::class, 102 | ], 103 | ], 104 | ``` 105 | 106 | It can be executed as: 107 | 108 | ```sh 109 | php yii monitor/clear-deprecated P1D 110 | ``` 111 | 112 | Where `P1D` is [interval spec] that specifies to delete all records one day older. 113 | 114 | [interval spec]: https://www.php.net/manual/en/dateinterval.construct.php -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zhuravljov/yii2-queue-monitor", 3 | "description": "Yii2 Queue Analytics Module", 4 | "type": "yii2-extension", 5 | "keywords": ["yii", "queue", "analytics", "module"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Roman Zhuravlev", 10 | "email": "zhuravljov@gmail.com" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/zhuravljov/yii2-queue-monitor/issues", 15 | "source": "https://github.com/zhuravljov/yii2-queue-monitor" 16 | }, 17 | "require": { 18 | "php": ">=5.5.0", 19 | "ext-pdo": "*", 20 | "yiisoft/yii2": "~2.0.14", 21 | "yiisoft/yii2-queue": ">=2.2.0", 22 | "yiisoft/yii2-bootstrap": "~2.0.0" 23 | }, 24 | "require-dev": { 25 | "yiisoft/yii2-debug": "~2.0.0", 26 | "zhuravljov/yii2-pagination": "~1.0" 27 | }, 28 | "suggest": { 29 | "zhuravljov/yii2-pagination": "Makes pagination more responsive" 30 | }, 31 | "repositories": [ 32 | { 33 | "type": "composer", 34 | "url": "https://asset-packagist.org" 35 | } 36 | ], 37 | "autoload": { 38 | "psr-4": { 39 | "zhuravljov\\yii\\queue\\monitor\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "tests\\": "tests" 45 | } 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "1.x-dev" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Env.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Env extends BaseObject 22 | { 23 | /** 24 | * @var Cache|array|string 25 | */ 26 | public $cache = 'cache'; 27 | /** 28 | * @var Connection|array|string 29 | */ 30 | public $db = 'db'; 31 | /** 32 | * @var string 33 | */ 34 | public $pushTableName = '{{%queue_push}}'; 35 | /** 36 | * @var string 37 | */ 38 | public $execTableName = '{{%queue_exec}}'; 39 | /** 40 | * @var string 41 | */ 42 | public $workerTableName = '{{%queue_worker}}'; 43 | /** 44 | * @var int 45 | */ 46 | public $workerPingInterval = 15; 47 | 48 | /** 49 | * @return static 50 | */ 51 | public static function ensure() 52 | { 53 | return Yii::$container->get(static::class); 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function init() 60 | { 61 | parent::init(); 62 | $this->cache = Instance::ensure($this->cache, Cache::class); 63 | $this->db = Instance::ensure($this->db, Connection::class); 64 | } 65 | 66 | /** 67 | * @return bool 68 | */ 69 | public function canListenWorkerLoop() 70 | { 71 | return !!$this->workerPingInterval; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function getHost() 78 | { 79 | if ($this->db->driverName === 'mysql') { 80 | $host = $this->db 81 | ->createCommand('SELECT `HOST` FROM `information_schema`.`PROCESSLIST` WHERE `ID` = CONNECTION_ID()') 82 | ->queryScalar(); 83 | return preg_replace('/:\d+$/', '', $host); 84 | } 85 | 86 | if ($this->db->driverName === 'pgsql') { 87 | return $this->db 88 | ->createCommand('SELECT inet_client_addr()') 89 | ->queryScalar(); 90 | } 91 | 92 | return '127.0.0.1'; // By default 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/JobMonitor.php: -------------------------------------------------------------------------------- 1 | 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)) { 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)) { 123 | return; 124 | } 125 | static::$startedPush = $push = $this->getPushRecord($event); 126 | if (!$push) { 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); 136 | 137 | $exec = new ExecRecord(); 138 | $exec->push_id = $push->id; 139 | if ($worker) { 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) { 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)) { 163 | return; 164 | } 165 | $push = static::$startedPush ?: $this->getPushRecord($event); 166 | if (!$push) { 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) { 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) { 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) { 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 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Module extends \yii\base\Module implements BootstrapInterface 25 | { 26 | /** 27 | * @var bool 28 | */ 29 | public $canPushAgain = false; 30 | /** 31 | * @var bool 32 | */ 33 | public $canExecStop = false; 34 | /** 35 | * @var bool 36 | */ 37 | public $canWorkerStop = false; 38 | /** 39 | * @inheritdoc 40 | */ 41 | public $layout = 'main'; 42 | /** 43 | * @inheritdoc 44 | */ 45 | public $controllerNamespace = 'zhuravljov\yii\queue\monitor\controllers'; 46 | /** 47 | * @inheritdoc 48 | */ 49 | public $defaultRoute = 'job/index'; 50 | 51 | public function init() 52 | { 53 | parent::init(); 54 | $this->registerTranslations(); 55 | } 56 | 57 | /** 58 | * @inheritdoc 59 | */ 60 | public function bootstrap($app) 61 | { 62 | if ($app instanceof WebApplication) { 63 | $app->urlManager->addRules([[ 64 | 'class' => GroupUrlRule::class, 65 | 'prefix' => $this->id, 66 | 'rules' => [ 67 | 'jobs' => 'job/index', 68 | 'job//' => 'job/view-', 69 | 'workers' => 'worker/index', 70 | '/' => '/view', 71 | '//' => '/', 72 | '/' => '/', 73 | ], 74 | ]], false); 75 | } else { 76 | throw new InvalidConfigException('The module must be used for web application only.'); 77 | } 78 | } 79 | 80 | private function registerTranslations() 81 | { 82 | if (!isset(Yii::$app->i18n->translations['queue-monitor/*'])) { 83 | Yii::$app->i18n->translations['queue-monitor/*'] = [ 84 | 'class' => PhpMessageSource::class, 85 | 'sourceLanguage' => 'en-US', 86 | 'basePath' => '@zhuravljov/yii/queue/monitor/messages', 87 | 'fileMap' => [ 88 | 'queue-monitor/main' => 'main.php', 89 | 'queue-monitor/notice' => 'notice.php', 90 | ], 91 | ]; 92 | } 93 | } 94 | 95 | /** 96 | * Module translator. 97 | * 98 | * @param string $category 99 | * @param string $message 100 | * @param array $params 101 | * @param string $language 102 | * @return string 103 | */ 104 | public static function t($category, $message, $params = [], $language = null) 105 | { 106 | return Yii::t('queue-monitor/' . $category, $message, $params, $language); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/WorkerMonitor.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class WorkerMonitor extends Behavior 24 | { 25 | /** 26 | * @var Queue 27 | * @inheritdoc 28 | */ 29 | public $owner; 30 | /** 31 | * @var Env 32 | */ 33 | protected $env; 34 | /** 35 | * @var WorkerRecord 36 | */ 37 | private $record; 38 | 39 | /** 40 | * @param Env $env 41 | * @param array $config 42 | */ 43 | public function __construct(Env $env, $config = []) 44 | { 45 | $this->env = $env; 46 | parent::__construct($config); 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function events() 53 | { 54 | $events = [ 55 | Queue::EVENT_WORKER_START => 'workerStart', 56 | Queue::EVENT_WORKER_STOP => 'workerStop', 57 | ]; 58 | if ($this->env->canListenWorkerLoop()) { 59 | $events[Queue::EVENT_WORKER_LOOP] = 'workerLoop'; 60 | } 61 | return $events; 62 | } 63 | 64 | /** 65 | * @param WorkerEvent $event 66 | */ 67 | public function workerStart(WorkerEvent $event) 68 | { 69 | $this->record = new WorkerRecord(); 70 | $this->record->sender_name = $this->getSenderName($event); 71 | $this->record->host = $this->env->getHost(); 72 | $this->record->pid = $event->sender->getWorkerPid(); 73 | $this->record->started_at = time(); 74 | $this->record->pinged_at = time(); 75 | $this->record->save(false); 76 | } 77 | 78 | /** 79 | * @param WorkerEvent $event 80 | */ 81 | public function workerLoop(WorkerEvent $event) 82 | { 83 | if ($this->record->pinged_at + $this->env->workerPingInterval > time()) { 84 | return; 85 | } 86 | if (!$this->record->refresh()) { 87 | $this->record->setIsNewRecord(true); 88 | } 89 | $this->record->pinged_at = time(); 90 | $this->record->save(false); 91 | 92 | if ($this->record->isStopped()) { 93 | $event->exitCode = ExitCode::OK; 94 | } 95 | } 96 | 97 | /** 98 | * @param WorkerEvent $event 99 | */ 100 | public function workerStop(WorkerEvent $event) 101 | { 102 | if (!$this->env->canListenWorkerLoop()) { 103 | $this->env->db->close(); // To reopen a lost connection 104 | } 105 | $this->record->finished_at = time(); 106 | $this->record->save(false); 107 | } 108 | 109 | /** 110 | * @param WorkerEvent $event 111 | * @throws 112 | * @return string 113 | */ 114 | protected function getSenderName(WorkerEvent $event) 115 | { 116 | foreach (Yii::$app->getComponents(false) as $id => $component) { 117 | if ($component === $event->sender) { 118 | return $id; 119 | } 120 | } 121 | throw new InvalidConfigException('Queue must be an application component.'); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/assets/JobItemAsset.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class JobItemAsset extends AssetBundle 19 | { 20 | public $sourcePath = '@zhuravljov/yii/queue/monitor/web'; 21 | public $css = [ 22 | 'job-item.css', 23 | ]; 24 | public $depends = [ 25 | BootstrapAsset::class, 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/MainAsset.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class MainAsset extends AssetBundle 22 | { 23 | public $sourcePath = '@zhuravljov/yii/queue/monitor/web'; 24 | public $css = [ 25 | 'main.css', 26 | ]; 27 | public $js = [ 28 | ]; 29 | public $depends = [ 30 | YiiAsset::class, 31 | BootstrapAsset::class, 32 | BootstrapPluginAsset::class, 33 | BootstrapThemeAsset::class, 34 | ]; 35 | } 36 | -------------------------------------------------------------------------------- /src/base/FlashTrait.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | trait FlashTrait 18 | { 19 | /** 20 | * @param string $message 21 | * @return $this 22 | */ 23 | protected function success($message) 24 | { 25 | return $this->flash('success', $message); 26 | } 27 | 28 | /** 29 | * @param string $message 30 | * @return $this 31 | */ 32 | protected function info($message) 33 | { 34 | return $this->flash('info', $message); 35 | } 36 | 37 | /** 38 | * @param string $message 39 | * @return $this 40 | */ 41 | protected function warning($message) 42 | { 43 | return $this->flash('warning', $message); 44 | } 45 | 46 | /** 47 | * @param string $message 48 | * @return $this 49 | */ 50 | protected function error($message) 51 | { 52 | return $this->flash('error', $message); 53 | } 54 | 55 | /** 56 | * @param string $type 57 | * @param string $message 58 | * @return $this 59 | */ 60 | protected function flash($type, $message) 61 | { 62 | Yii::$app->session->setFlash($type, $message); 63 | return $this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/base/Migration.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class Migration extends \yii\db\Migration 18 | { 19 | /** 20 | * @var Env 21 | */ 22 | protected $env; 23 | 24 | /** 25 | * @param Env $env 26 | * @inheritdoc 27 | */ 28 | public function __construct(Env $env, $config = []) 29 | { 30 | $this->env = $env; 31 | parent::__construct($config); 32 | } 33 | 34 | /** 35 | * @inheritdoc 36 | */ 37 | public function binary($length = null) 38 | { 39 | if ($this->db->driverName === 'mysql') { 40 | return $this->db->schema->createColumnSchemaBuilder('longblob'); 41 | } 42 | return parent::binary($length); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function text() 49 | { 50 | if ($this->db->driverName === 'mysql') { 51 | return $this->db->schema->createColumnSchemaBuilder('longtext'); 52 | } 53 | return parent::text(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/console/GcController.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class GcController extends Controller 21 | { 22 | /** 23 | * @var bool verbose mode. 24 | */ 25 | public $silent = false; 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function options($actionID) 31 | { 32 | return array_merge(parent::options($actionID), [ 33 | 'silent', 34 | ]); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function optionAliases() 41 | { 42 | return array_merge(parent::optionAliases(), [ 43 | 's' => 'silent', 44 | ]); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function beforeAction($action) 51 | { 52 | if ($this->silent) { 53 | $this->interactive = false; 54 | } 55 | return parent::beforeAction($action); 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function stdout($string) 62 | { 63 | if ($this->silent) { 64 | return false; 65 | } 66 | return parent::stdout($string); 67 | } 68 | 69 | /** 70 | * Clear deprecated records. 71 | * 72 | * @param string $interval 73 | * @link https://www.php.net/manual/en/dateinterval.construct.php 74 | */ 75 | public function actionClearDeprecated($interval) 76 | { 77 | $ids = PushRecord::find() 78 | ->deprecated($interval) 79 | ->done() 80 | ->select('push.id') 81 | ->asArray()->column(); 82 | $count = count($ids); 83 | if ($count && $this->confirm("Do you want to delete $count records?")) { 84 | $count = PushRecord::getDb()->transaction(function () use ($ids) { 85 | ExecRecord::deleteAll(['push_id' => $ids]); 86 | return PushRecord::deleteAll(['id' => $ids]); 87 | }); 88 | $this->stdout("$count records deleted.\n"); 89 | } 90 | } 91 | 92 | /** 93 | * Clear all records. 94 | */ 95 | public function actionClearAll() 96 | { 97 | if ($this->confirm('Are you sure?')) { 98 | $count = PushRecord::getDb()->transaction(function () { 99 | WorkerRecord::deleteAll(); 100 | ExecRecord::deleteAll(); 101 | return PushRecord::deleteAll(); 102 | }); 103 | $this->stdout("$count records deleted.\n"); 104 | } 105 | } 106 | 107 | /** 108 | * Clear lost worker records. 109 | */ 110 | public function actionClearWorkers() 111 | { 112 | $count = WorkerRecord::deleteAll(); 113 | $this->stdout("$count records deleted.\n"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/controllers/JobController.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class JobController extends Controller 25 | { 26 | use FlashTrait; 27 | 28 | /** 29 | * @var Module 30 | */ 31 | public $module; 32 | 33 | /** 34 | * @inheritdoc 35 | */ 36 | public function behaviors() 37 | { 38 | return [ 39 | 'verbs' => [ 40 | 'class' => VerbFilter::class, 41 | 'actions' => [ 42 | 'push' => ['post'], 43 | 'stop' => ['post'], 44 | ], 45 | ], 46 | ]; 47 | } 48 | 49 | /** 50 | * Pushed jobs 51 | * 52 | * @return mixed 53 | */ 54 | public function actionIndex() 55 | { 56 | return $this->render('index', [ 57 | 'filter' => JobFilter::ensure(), 58 | ]); 59 | } 60 | 61 | /** 62 | * Job view 63 | * 64 | * @param int $id 65 | * @return mixed 66 | */ 67 | public function actionView($id) 68 | { 69 | $record = $this->findRecord($id); 70 | if ($record->lastExec && $record->lastExec->isFailed()) { 71 | return $this->redirect(['view-attempts', 'id' => $record->id]); 72 | } 73 | return $this->redirect(['view-details', 'id' => $record->id]); 74 | } 75 | 76 | /** 77 | * Push details 78 | * 79 | * @param int $id 80 | * @return mixed 81 | */ 82 | public function actionViewDetails($id) 83 | { 84 | return $this->render('view-details', [ 85 | 'record' => $this->findRecord($id), 86 | ]); 87 | } 88 | 89 | /** 90 | * Push environment 91 | * 92 | * @param int $id 93 | * @return mixed 94 | */ 95 | public function actionViewContext($id) 96 | { 97 | return $this->render('view-context', [ 98 | 'record' => $this->findRecord($id), 99 | ]); 100 | } 101 | 102 | /** 103 | * Job object data 104 | * 105 | * @param int $id 106 | * @return mixed 107 | */ 108 | public function actionViewData($id) 109 | { 110 | return $this->render('view-data', [ 111 | 'record' => $this->findRecord($id), 112 | ]); 113 | } 114 | 115 | /** 116 | * Attempts 117 | * 118 | * @param int $id 119 | * @return mixed 120 | */ 121 | public function actionViewAttempts($id) 122 | { 123 | return $this->render('view-attempts', [ 124 | 'record' => $this->findRecord($id), 125 | ]); 126 | } 127 | 128 | /** 129 | * Pushes a job again 130 | * 131 | * @param int $id 132 | * @throws 133 | * @return mixed 134 | */ 135 | public function actionPush($id) 136 | { 137 | if (!$this->module->canPushAgain) { 138 | throw new ForbiddenHttpException(Module::t('notice', 'Push is forbidden.')); 139 | } 140 | 141 | $record = $this->findRecord($id); 142 | 143 | if (!$record->isSenderValid()) { 144 | $error = Module::t( 145 | 'notice', 146 | 'The job isn\'t pushed because {sender} component isn\'t found.', 147 | ['sender'=>$record->sender_name] 148 | ); 149 | return $this 150 | ->error($error) 151 | ->redirect(['view-details', 'id' => $record->id]); 152 | } 153 | 154 | if (!$record->isJobValid()) { 155 | $error = Module::t( 156 | 'notice', 157 | 'The job isn\'t pushed because it must be JobInterface instance.' 158 | ); 159 | return $this 160 | ->error($error) 161 | ->redirect(['view-data', 'id' => $record->id]); 162 | } 163 | 164 | $uid = $record->getSender()->push($record->createJob()); 165 | $newRecord = PushRecord::find()->byJob($record->sender_name, $uid)->one(); 166 | 167 | return $this 168 | ->success(Module::t('notice', 'The job is pushed again.')) 169 | ->redirect(['view', 'id' => $newRecord->id]); 170 | } 171 | 172 | /** 173 | * Stop a job 174 | * 175 | * @param int $id 176 | * @throws 177 | * @return mixed 178 | */ 179 | public function actionStop($id) 180 | { 181 | if (!$this->module->canExecStop) { 182 | throw new ForbiddenHttpException(Module::t('notice', 'Stop is forbidden.')); 183 | } 184 | 185 | $record = $this->findRecord($id); 186 | 187 | if ($record->isStopped()) { 188 | return $this 189 | ->error(Module::t('notice', 'The job is already stopped.')) 190 | ->redirect(['view-details', 'id' => $record->id]); 191 | } 192 | 193 | if (!$record->canStop()) { 194 | return $this 195 | ->error(Module::t('notice', 'The job is already done.')) 196 | ->redirect(['view-attempts', 'id' => $record->id]); 197 | } 198 | 199 | $record->stop(); 200 | 201 | return $this 202 | ->success(Module::t('notice', 'The job will be stopped.')) 203 | ->redirect(['view-details', 'id' => $record->id]); 204 | } 205 | 206 | /** 207 | * @param int $id 208 | * @throws NotFoundHttpException 209 | * @return PushRecord 210 | */ 211 | protected function findRecord($id) 212 | { 213 | if ($record = PushRecord::find()->byId($id)->one()) { 214 | return $record; 215 | } 216 | throw new NotFoundHttpException(Module::t('notice', 'Record not found.')); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/controllers/WorkerController.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class WorkerController extends Controller 26 | { 27 | use FlashTrait; 28 | 29 | /** 30 | * @var Module 31 | */ 32 | public $module; 33 | /** 34 | * @var Env 35 | */ 36 | protected $env; 37 | 38 | public function __construct($id, $module, Env $env, array $config = []) 39 | { 40 | $this->env = $env; 41 | parent::__construct($id, $module, $config); 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function behaviors() 48 | { 49 | return [ 50 | 'verb' => [ 51 | 'class' => VerbFilter::class, 52 | 'actions' => [ 53 | 'stop' => ['post'], 54 | ], 55 | ], 56 | ]; 57 | } 58 | 59 | /** 60 | * Worker List 61 | * 62 | * @return string 63 | */ 64 | public function actionIndex() 65 | { 66 | return $this->render('index', [ 67 | 'filter' => WorkerFilter::ensure(), 68 | ]); 69 | } 70 | 71 | /** 72 | * Stops a worker 73 | * 74 | * @param int $id 75 | * @throws ForbiddenHttpException 76 | * @return \yii\web\Response 77 | */ 78 | public function actionStop($id) 79 | { 80 | if (!$this->module->canWorkerStop) { 81 | throw new ForbiddenHttpException(Module::t('notice', 'Stop is forbidden.')); 82 | } 83 | 84 | $record = $this->findRecord($id); 85 | $record->stop(); 86 | return $this 87 | ->success(Module::t('notice', 'The worker will be stopped within {timeout} sec.', [ 88 | 'timeout' => $record->pinged_at + $this->env->workerPingInterval - time(), 89 | ])) 90 | ->redirect(['index']); 91 | } 92 | 93 | /** 94 | * @param int $id 95 | * @throws NotFoundHttpException 96 | * @return WorkerRecord 97 | */ 98 | protected function findRecord($id) 99 | { 100 | if ($record = WorkerRecord::findOne($id)) { 101 | return $record; 102 | } 103 | throw new NotFoundHttpException(Module::t('notice', 'Record not found.')); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/filters/BaseFilter.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class BaseFilter extends Model 20 | { 21 | /** 22 | * @var Env 23 | */ 24 | protected $env; 25 | 26 | /** 27 | * @param Env $env 28 | * @param array $config 29 | */ 30 | public function __construct(Env $env, $config = []) 31 | { 32 | $this->env = $env; 33 | parent::__construct($config); 34 | } 35 | 36 | public static function ensure() 37 | { 38 | /** @var static $filter */ 39 | $filter = Yii::createObject(get_called_class()); 40 | $filter->load(Yii::$app->request->queryParams) && $filter->validate(); 41 | $filter->storeParams(); 42 | return $filter; 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public static function restoreParams() 49 | { 50 | return Yii::$app->session->get(get_called_class(), []); 51 | } 52 | 53 | public function storeParams() 54 | { 55 | $params = []; 56 | foreach ($this->attributes as $attribute => $value) { 57 | if ($value !== null && $value !== '') { 58 | $params[$attribute] = $value; 59 | } 60 | } 61 | Yii::$app->session->set(get_called_class(), $params); 62 | } 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | public function formName() 68 | { 69 | return ''; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/filters/JobFilter.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class JobFilter extends BaseFilter 22 | { 23 | const IS_WAITING = 'waiting'; 24 | const IS_IN_PROGRESS = 'in-progress'; 25 | const IS_DONE = 'done'; 26 | const IS_SUCCESS = 'success'; 27 | const IS_BURIED = 'buried'; 28 | const IS_FAILED = 'failed'; 29 | const IS_STOPPED = 'stopped'; 30 | 31 | public $is; 32 | public $sender; 33 | public $class; 34 | public $pushed_after; 35 | public $pushed_before; 36 | public $contains; 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function rules() 42 | { 43 | return [ 44 | ['is', 'string'], 45 | ['is', 'in', 'range' => array_keys($this->scopeList())], 46 | ['sender', 'string'], 47 | ['sender', 'trim'], 48 | ['class', 'string'], 49 | ['class', 'trim'], 50 | [['pushed_after', 'pushed_before'], 'string'], 51 | [['pushed_after', 'pushed_before'], 'validateDatetime'], 52 | ['contains', 'string'], 53 | ['contains', 'trim'], 54 | ]; 55 | } 56 | 57 | public function validateDatetime($attribute) 58 | { 59 | if ($this->hasErrors($attribute)) { 60 | return; 61 | } 62 | if ($this->parseDatetime($this->$attribute) === null) { 63 | $this->addError($attribute, Yii::t('yii', 'The format of {attribute} is invalid.', [ 64 | 'attribute' => $this->getAttributeLabel($attribute), 65 | ])); 66 | } 67 | } 68 | 69 | /** 70 | * @inheritdoc 71 | */ 72 | public function attributeLabels() 73 | { 74 | return [ 75 | 'is' => Module::t('main', 'Scope'), 76 | 'sender' => Module::t('main', 'Sender'), 77 | 'class' => Module::t('main', 'Job'), 78 | 'pushed_after' => Module::t('main', 'Pushed After'), 79 | 'pushed_before' => Module::t('main', 'Pushed Before'), 80 | 'contains' => Module::t('main', 'Contains'), 81 | ]; 82 | } 83 | 84 | /** 85 | * @return array 86 | */ 87 | public function scopeList() 88 | { 89 | return [ 90 | self::IS_WAITING => Module::t('main', 'Waiting'), 91 | self::IS_IN_PROGRESS => Module::t('main', 'In progress'), 92 | self::IS_DONE => Module::t('main', 'Done'), 93 | self::IS_SUCCESS => Module::t('main', 'Done successfully'), 94 | self::IS_BURIED => Module::t('main', 'Buried'), 95 | self::IS_FAILED => Module::t('main', 'Has failed attempts'), 96 | self::IS_STOPPED => Module::t('main', 'Stopped'), 97 | ]; 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | public function senderList() 104 | { 105 | return $this->env->cache->getOrSet(__METHOD__, function () { 106 | return PushRecord::find() 107 | ->select('push.sender_name') 108 | ->groupBy('push.sender_name') 109 | ->orderBy('push.sender_name') 110 | ->column(); 111 | }, 3600); 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | public function classList() 118 | { 119 | return $this->env->cache->getOrSet(__METHOD__, function () { 120 | return PushRecord::find() 121 | ->select('push.job_class') 122 | ->groupBy('push.job_class') 123 | ->orderBy('push.job_class') 124 | ->column(); 125 | }, 3600); 126 | } 127 | 128 | /** 129 | * @return PushQuery 130 | */ 131 | public function search() 132 | { 133 | $query = PushRecord::find(); 134 | if ($this->hasErrors()) { 135 | return $query->andWhere('1 = 0'); 136 | } 137 | 138 | $query->andFilterWhere(['push.sender_name' => $this->sender]); 139 | $query->andFilterWhere(['like', 'push.job_class', $this->class]); 140 | $query->andFilterWhere(['like', 'push.job_data', $this->contains]); 141 | $query->andFilterWhere(['>=', 'push.pushed_at', $this->parseDatetime($this->pushed_after)]); 142 | $query->andFilterWhere(['<=', 'push.pushed_at', $this->parseDatetime($this->pushed_before, true)]); 143 | 144 | if ($this->is === self::IS_WAITING) { 145 | $query->waiting(); 146 | } elseif ($this->is === self::IS_IN_PROGRESS) { 147 | $query->inProgress(); 148 | } elseif ($this->is === self::IS_DONE) { 149 | $query->done(); 150 | } elseif ($this->is === self::IS_SUCCESS) { 151 | $query->success(); 152 | } elseif ($this->is === self::IS_BURIED) { 153 | $query->buried(); 154 | } elseif ($this->is === self::IS_FAILED) { 155 | $query->hasFails(); 156 | } elseif ($this->is === self::IS_STOPPED) { 157 | $query->stopped(); 158 | } 159 | 160 | return $query; 161 | } 162 | 163 | /** 164 | * @return array 165 | */ 166 | public function searchClasses() 167 | { 168 | return $this->search() 169 | ->select(['name' => 'push.job_class', 'count' => 'COUNT(*)']) 170 | ->groupBy(['name']) 171 | ->orderBy(['name' => SORT_ASC]) 172 | ->asArray() 173 | ->all(); 174 | } 175 | 176 | /** 177 | * @return array 178 | */ 179 | public function searchSenders() 180 | { 181 | return $this->search() 182 | ->select(['name' => 'push.sender_name', 'count' => 'COUNT(*)']) 183 | ->groupBy(['name']) 184 | ->orderBy(['name' => SORT_ASC]) 185 | ->asArray() 186 | ->all(); 187 | } 188 | 189 | /** 190 | * @param string $value 191 | * @param bool $isEnd 192 | * @return int|null 193 | */ 194 | private function parseDatetime($value, $isEnd = false) 195 | { 196 | $dt = DateTime::createFromFormat('Y-m-d\TH:i', $value); 197 | if (!$dt) { 198 | return null; 199 | } 200 | $time = $dt->getTimestamp(); 201 | $time = $time - $time % 60; 202 | if ($isEnd) { 203 | $time += 59; 204 | } 205 | return $time; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/filters/WorkerFilter.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class WorkerFilter extends BaseFilter 19 | { 20 | /** 21 | * @return WorkerQuery 22 | */ 23 | public function search() 24 | { 25 | return WorkerRecord::find(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/messages/ru/main.php: -------------------------------------------------------------------------------- 1 | '{sender}: №{jobId}', 10 | 'Application' => 'Приложение', 11 | 'Attempt' => 'Попытка', 12 | 'Attempts' => 'Попытки', 13 | 'Attempts ({attempts})' => 'Попытки ({attempts})', 14 | 'Buried' => 'Провалено', 15 | 'Busy since {time}.' => 'Активен с {time}.', 16 | 'Class' => 'Класс', 17 | 'Contains' => 'Содержит', 18 | 'Context' => 'Контекст', 19 | 'Data' => 'Параметры', 20 | 'Delay' => 'Задержка', 21 | 'Details' => 'Подробности', 22 | 'Done' => 'Выполнено', 23 | 'Done successfully' => 'Успешно выполнено', 24 | 'Duration' => 'Длительность', 25 | 'Empty' => 'Ничего не найдено', 26 | 'Environment' => 'Окружение', 27 | 'Error' => 'Ошибка', 28 | 'Exec' => 'Выполнение', 29 | 'Failed' => 'Ошибка', 30 | 'Filtered' => 'Отфильтровано', 31 | 'Finished' => 'Завершено', 32 | 'Has failed attempts' => 'С неудачными попытками', 33 | 'Host' => 'Хост', 34 | 'ID' => 'ID', 35 | 'Idle after a job since {time}.' => 'Простаивает после выполнения задачи с {time}.', 36 | 'Idle since {time}.' => 'Простаивает с {time}.', 37 | 'In progress' => 'В процессе', 38 | 'Is retry?' => 'Повтор?', 39 | 'Job' => 'Задача', 40 | 'Job UID' => 'ID Задачи', 41 | 'Jobs' => 'Задачи', 42 | 'Last execute time and memory usage.' => 'Время последнего выполнения и использование памяти', 43 | 'Mark as stopped.' => 'Остановить', 44 | 'Memory Usage' => 'Использование памяти', 45 | 'Name' => 'Название', 46 | 'No jobs found.' => 'Задач не найдено.', 47 | 'No workers found.' => 'Исполнителей не найдено.', 48 | 'Number of attempts.' => 'Кол-во попыток', 49 | 'PID' => 'PID', 50 | 'Push Again' => 'Перезапустить', 51 | 'Push Context' => 'Контекст запуска', 52 | 'Push TTR' => 'TTR', 53 | 'Push Trace' => 'Стек запуска', 54 | 'Push again.' => 'Повторный запуск задачи', 55 | 'Pushed' => 'Запущено', 56 | 'Pushed After' => 'Запущено после', 57 | 'Pushed Before' => 'Запущено перед', 58 | 'Queue Monitor' => 'Менеджер очередей', 59 | 'Reset' => 'Сброс', 60 | 'Restarted' => 'Перезапущено', 61 | 'Scope' => 'Статус', 62 | 'Search' => 'Найти', 63 | 'Sender' => 'Инициатор', 64 | 'Sender: {name} {class}' => 'Инициатор: {name} {class}', 65 | 'Started' => 'Запущено', 66 | 'Started At' => 'Запущено в', 67 | 'Status' => 'Статус', 68 | 'Stop' => 'Остановить', 69 | 'Stop the worker.' => 'Остановить процесс', 70 | 'Stopped' => 'Остановлено', 71 | 'Sub Jobs' => 'Подзадачи', 72 | 'TTR' => 'TTR', 73 | 'Time to reserve of the job.' => 'Время резервирования задачи', 74 | 'Total Done' => 'Всего исполнено', 75 | 'Total Started' => 'Всего запущено', 76 | 'Value' => 'Значение', 77 | 'Wait' => 'Ожидание', 78 | 'Wait Time' => 'Время ожидания', 79 | 'Waiting' => 'В ожидании', 80 | 'Waiting time from push till first execute.' => 'Время ожидания с момента постановки до первого выполнения', 81 | 'Workers' => 'Исполнители', 82 | 'empty lead' => 'ничего не найдено', 83 | 'from' => 'из', 84 | ]; 85 | -------------------------------------------------------------------------------- /src/messages/ru/notice.php: -------------------------------------------------------------------------------- 1 | 'Перезапуск задачи запрещен', 10 | 'Record not found.' => 'Запись не найдена', 11 | 'Stop is forbidden.' => 'Прерывание задачи запрещено', 12 | 'The job is already done.' => 'Задача уже выполнена', 13 | 'The job is already stopped.' => 'Задача уже остановлена', 14 | 'The job is pushed again.' => 'Задача отправлена в очередь повторно', 15 | 'The job isn\'t pushed because it must be JobInterface instance.' => 'Задача не может быть добавлена, задача должна реализовывать интерфейс JobInterface ', 16 | 'The job isn\'t pushed because {sender} component isn\'t found.' => 'Задача не может быть добавлена, компонент - отправитель не найден', 17 | 'The job will be stopped.' => 'Задача будет остановлена', 18 | 'The worker will be stopped within {timeout} sec.' => 'Исполнитель будет остановлен в течение {timeout} сек.', 19 | ]; 20 | -------------------------------------------------------------------------------- /src/migrations/M180807000000Schema.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M180807000000Schema extends Migration 18 | { 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function safeUp() 23 | { 24 | $this->createTable($this->env->pushTableName, [ 25 | 'id' => $this->bigPrimaryKey(), 26 | 'parent_id' => $this->bigInteger(), 27 | 'sender_name' => $this->string(32)->notNull(), 28 | 'job_uid' => $this->string(32)->notNull(), 29 | 'job_class' => $this->string()->notNull(), 30 | 'job_data' => $this->binary()->notNull(), 31 | 'ttr' => $this->integer()->unsigned()->notNull(), 32 | 'delay' => $this->integer()->unsigned()->notNull(), 33 | 'trace' => $this->text(), 34 | 'context' => $this->text(), 35 | 'pushed_at' => $this->integer()->unsigned()->notNull(), 36 | 'stopped_at' => $this->integer()->unsigned(), 37 | 'first_exec_id' => $this->bigInteger(), 38 | 'last_exec_id' => $this->bigInteger(), 39 | ]); 40 | $this->createIndex('ind_qp_parent_id', $this->env->pushTableName, 'parent_id'); 41 | $this->createIndex('ind_qp_job_uid', $this->env->pushTableName, ['sender_name', 'job_uid']); 42 | $this->createIndex('ind_qp_job_class', $this->env->pushTableName, 'job_class'); 43 | $this->createIndex('ind_qp_first_exec_id', $this->env->pushTableName, 'first_exec_id'); 44 | $this->createIndex('ind_qp_last_exec_id', $this->env->pushTableName, 'last_exec_id'); 45 | 46 | $this->createTable($this->env->execTableName, [ 47 | 'id' => $this->bigPrimaryKey(), 48 | 'push_id' => $this->bigInteger()->notNull(), 49 | 'worker_id' => $this->bigInteger(), 50 | 'attempt' => $this->integer()->unsigned()->notNull(), 51 | 'started_at' => $this->integer()->unsigned()->notNull(), 52 | 'finished_at' => $this->integer()->unsigned(), 53 | 'memory_usage' => $this->bigInteger()->unsigned(), 54 | 'error' => $this->text(), 55 | 'retry' => $this->boolean(), 56 | ]); 57 | $this->createIndex('ind_qe_push_id', $this->env->execTableName, 'push_id'); 58 | $this->createIndex('ind_qe_worker_id', $this->env->execTableName, 'worker_id'); 59 | 60 | $this->createTable($this->env->workerTableName, [ 61 | 'id' => $this->bigPrimaryKey(), 62 | 'sender_name' => $this->string(32)->notNull(), 63 | 'host' => $this->string(64), 64 | 'pid' => $this->integer()->unsigned(), 65 | 'started_at' => $this->integer()->unsigned()->notNull(), 66 | 'pinged_at' => $this->integer()->unsigned()->notNull(), 67 | 'stopped_at' => $this->integer()->unsigned(), 68 | 'finished_at' => $this->integer()->unsigned(), 69 | 'last_exec_id' => $this->bigInteger(), 70 | ]); 71 | $this->createIndex('ind_qw_finished_at', $this->env->workerTableName, 'finished_at'); 72 | $this->createIndex('ind_qw_last_exec_id', $this->env->workerTableName, 'last_exec_id'); 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function safeDown() 79 | { 80 | $this->dropTable($this->env->workerTableName); 81 | $this->dropTable($this->env->execTableName); 82 | $this->dropTable($this->env->pushTableName); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/migrations/M190420000000ExecResult.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class M190420000000ExecResult extends Migration 18 | { 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function safeUp() 23 | { 24 | $this->addColumn($this->env->execTableName, 'result_data', $this->binary()->after('error')); 25 | } 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function safeDown() 31 | { 32 | $this->dropColumn($this->env->execTableName, 'result_data'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/records/ExecQuery.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ExecQuery extends ActiveQuery 18 | { 19 | /** 20 | * @inheritdoc 21 | */ 22 | public function init() 23 | { 24 | parent::init(); 25 | $this->alias('exec'); 26 | } 27 | 28 | /** 29 | * @inheritdoc 30 | * @return ExecRecord[]|array 31 | */ 32 | public function all($db = null) 33 | { 34 | return parent::all($db); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | * @return ExecRecord|array|null 40 | */ 41 | public function one($db = null) 42 | { 43 | return parent::one($db); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/records/ExecRecord.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | class ExecRecord extends ActiveRecord 37 | { 38 | private $_errorMessage; 39 | 40 | /** 41 | * @inheritdoc 42 | * @return ExecQuery the active query used by this AR class. 43 | */ 44 | public static function find() 45 | { 46 | return Yii::createObject(ExecQuery::class, [get_called_class()]); 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public static function getDb() 53 | { 54 | return Env::ensure()->db; 55 | } 56 | 57 | /** 58 | * @inheritdoc 59 | */ 60 | public static function tableName() 61 | { 62 | return Env::ensure()->execTableName; 63 | } 64 | 65 | /** 66 | * @return PushQuery 67 | */ 68 | public function getPush() 69 | { 70 | return $this->hasOne(PushRecord::class, ['id' => 'push_id']); 71 | } 72 | 73 | /** 74 | * @return WorkerQuery 75 | */ 76 | public function getWorker() 77 | { 78 | return $this->hasOne(WorkerRecord::class, ['id' => 'worker_id']); 79 | } 80 | 81 | /** 82 | * @return int 83 | */ 84 | public function getDuration() 85 | { 86 | if ($this->finished_at) { 87 | return $this->finished_at - $this->started_at; 88 | } 89 | return time() - $this->started_at; 90 | } 91 | 92 | /** 93 | * @return bool 94 | */ 95 | public function isDone() 96 | { 97 | return $this->finished_at !== null; 98 | } 99 | 100 | /** 101 | * @return bool 102 | */ 103 | public function isFailed() 104 | { 105 | return $this->error !== null; 106 | } 107 | 108 | /** 109 | * @return false|string first error line 110 | */ 111 | public function getErrorMessage() 112 | { 113 | if ($this->_errorMessage === null) { 114 | $this->_errorMessage = false; 115 | if ($this->error !== null) { 116 | $this->_errorMessage = trim(explode("\n", $this->error, 2)[0]); 117 | } 118 | } 119 | return $this->_errorMessage; 120 | } 121 | 122 | public function getResult() 123 | { 124 | if (is_resource($this->result_data)) { 125 | $this->result_data = stream_get_contents($this->result_data); 126 | } 127 | if ($this->result_data) { 128 | return unserialize($this->result_data); 129 | } 130 | return null; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/records/PushQuery.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class PushQuery extends ActiveQuery 21 | { 22 | /** 23 | * @inheritdoc 24 | */ 25 | public function init() 26 | { 27 | parent::init(); 28 | $this->alias('push'); 29 | } 30 | 31 | /** 32 | * @param int $id 33 | * @return $this 34 | */ 35 | public function byId($id) 36 | { 37 | return $this->andWhere(['push.id' => $id]); 38 | } 39 | 40 | /** 41 | * @param string $senderName 42 | * @param string $jobUid 43 | * @return $this 44 | */ 45 | public function byJob($senderName, $jobUid) 46 | { 47 | return $this 48 | ->andWhere(['push.sender_name' => $senderName]) 49 | ->andWhere(['push.job_uid' => $jobUid]) 50 | ->orderBy(['push.id' => SORT_DESC]) 51 | ->limit(1); 52 | } 53 | 54 | /** 55 | * @return $this 56 | */ 57 | public function waiting() 58 | { 59 | return $this 60 | ->joinLastExec() 61 | ->andWhere(['or', ['push.last_exec_id' => null], ['last_exec.retry' => true]]) 62 | ->andWhere(['push.stopped_at' => null]); 63 | } 64 | 65 | /** 66 | * @return $this 67 | */ 68 | public function inProgress() 69 | { 70 | return $this 71 | ->andWhere(['is not', 'push.last_exec_id', null]) 72 | ->joinLastExec() 73 | ->andWhere(['last_exec.finished_at' => null]); 74 | } 75 | 76 | /** 77 | * @return $this 78 | */ 79 | public function done() 80 | { 81 | return $this 82 | ->joinLastExec() 83 | ->andWhere(['is not', 'last_exec.finished_at', null]) 84 | ->andWhere(['last_exec.retry' => false]); 85 | } 86 | 87 | /** 88 | * @return $this 89 | */ 90 | public function success() 91 | { 92 | return $this 93 | ->done() 94 | ->andWhere(['last_exec.error' => null]); 95 | } 96 | 97 | /** 98 | * @return $this 99 | */ 100 | public function buried() 101 | { 102 | return $this 103 | ->done() 104 | ->andWhere(['is not', 'last_exec.error', null]); 105 | } 106 | 107 | /** 108 | * @return $this 109 | */ 110 | public function hasFails() 111 | { 112 | return $this 113 | ->andWhere(['exists', new Query([ 114 | 'from' => ['exec' => ExecRecord::tableName()], 115 | 'where' => '{{exec}}.[[push_id]] = {{push}}.[[id]] AND {{exec}}.[[error]] IS NOT NULL', 116 | ])]); 117 | } 118 | 119 | /** 120 | * @return $this 121 | */ 122 | public function stopped() 123 | { 124 | return $this->andWhere(['is not', 'push.stopped_at', null]); 125 | } 126 | 127 | /** 128 | * @return $this 129 | */ 130 | public function joinFirstExec() 131 | { 132 | return $this->leftJoin( 133 | ['first_exec' => ExecRecord::tableName()], 134 | '{{first_exec}}.[[id]] = {{push}}.[[first_exec_id]]' 135 | ); 136 | } 137 | 138 | /** 139 | * @return $this 140 | */ 141 | public function joinLastExec() 142 | { 143 | return $this->leftJoin( 144 | ['last_exec' => ExecRecord::tableName()], 145 | '{{last_exec}}.[[id]] = {{push}}.[[last_exec_id]]' 146 | ); 147 | } 148 | 149 | /** 150 | * @param string $interval 151 | * @link https://www.php.net/manual/en/dateinterval.construct.php 152 | * @return $this 153 | */ 154 | public function deprecated($interval) 155 | { 156 | $min = (new DateTime())->sub(new DateInterval($interval))->getTimestamp(); 157 | return $this->andWhere(['<', 'push.pushed_at', $min]); 158 | } 159 | 160 | /** 161 | * @inheritdoc 162 | * @return PushRecord[]|array 163 | */ 164 | public function all($db = null) 165 | { 166 | return parent::all($db); 167 | } 168 | 169 | /** 170 | * @inheritdoc 171 | * @return PushRecord|array|null 172 | */ 173 | public function one($db = null) 174 | { 175 | return parent::one($db); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/records/PushRecord.php: -------------------------------------------------------------------------------- 1 | 53 | */ 54 | class PushRecord extends ActiveRecord 55 | { 56 | const STATUS_STOPPED = 'stopped'; 57 | const STATUS_WAITING = 'waiting'; 58 | const STATUS_STARTED = 'started'; 59 | const STATUS_DONE = 'done'; 60 | const STATUS_FAILED = 'failed'; 61 | const STATUS_RESTARTED = 'restarted'; 62 | const STATUS_BURIED = 'buried'; 63 | 64 | /** 65 | * @inheritdoc 66 | * @return PushQuery the active query used by this AR class. 67 | */ 68 | public static function find() 69 | { 70 | return Yii::createObject(PushQuery::class, [get_called_class()]); 71 | } 72 | 73 | /** 74 | * @inheritdoc 75 | */ 76 | public static function getDb() 77 | { 78 | return Env::ensure()->db; 79 | } 80 | 81 | /** 82 | * @inheritdoc 83 | */ 84 | public static function tableName() 85 | { 86 | return Env::ensure()->pushTableName; 87 | } 88 | 89 | /** 90 | * @return PushQuery 91 | */ 92 | public function getParent() 93 | { 94 | return $this->hasOne(static::class, ['id' => 'parent_id']); 95 | } 96 | 97 | /** 98 | * @return PushQuery 99 | */ 100 | public function getChildren() 101 | { 102 | return $this->hasMany(static::class, ['parent_id' => 'id']); 103 | } 104 | 105 | /** 106 | * @return ExecQuery 107 | */ 108 | public function getExecs() 109 | { 110 | return $this->hasMany(ExecRecord::class, ['push_id' => 'id']); 111 | } 112 | 113 | /** 114 | * @return ExecQuery 115 | */ 116 | public function getFirstExec() 117 | { 118 | return $this->hasOne(ExecRecord::class, ['id' => 'first_exec_id']); 119 | } 120 | 121 | /** 122 | * @return ExecQuery 123 | */ 124 | public function getLastExec() 125 | { 126 | return $this->hasOne(ExecRecord::class, ['id' => 'last_exec_id']); 127 | } 128 | 129 | /** 130 | * @return ExecQuery 131 | */ 132 | public function getExecTotal() 133 | { 134 | return $this->hasOne(ExecRecord::class, ['push_id' => 'id']) 135 | ->select([ 136 | 'exec.push_id', 137 | 'attempts' => 'COUNT(*)', 138 | 'errors' => 'COUNT(exec.error)', 139 | ]) 140 | ->groupBy('exec.push_id') 141 | ->asArray(); 142 | } 143 | 144 | /** 145 | * @return int number of attempts 146 | */ 147 | public function getAttemptCount() 148 | { 149 | return ArrayHelper::getValue($this->execTotal, 'attempts', 0); 150 | } 151 | 152 | /** 153 | * @return int waiting time from push till first execute 154 | */ 155 | public function getWaitTime() 156 | { 157 | if ($this->firstExec) { 158 | return $this->firstExec->started_at - $this->pushed_at - $this->delay; 159 | } 160 | return time() - $this->pushed_at - $this->delay; 161 | } 162 | 163 | /** 164 | * @return string 165 | */ 166 | public function getStatus() 167 | { 168 | if ($this->isStopped()) { 169 | return self::STATUS_STOPPED; 170 | } 171 | if (!$this->lastExec) { 172 | return self::STATUS_WAITING; 173 | } 174 | if (!$this->lastExec->isDone() && $this->lastExec->attempt == 1) { 175 | return self::STATUS_STARTED; 176 | } 177 | if ($this->lastExec->isDone() && !$this->lastExec->isFailed()) { 178 | return self::STATUS_DONE; 179 | } 180 | if ($this->lastExec->isDone() && $this->lastExec->retry) { 181 | return self::STATUS_FAILED; 182 | } 183 | if (!$this->lastExec->isDone()) { 184 | return self::STATUS_RESTARTED; 185 | } 186 | if ($this->lastExec->isDone() && !$this->lastExec->retry) { 187 | return self::STATUS_BURIED; 188 | } 189 | return null; 190 | } 191 | 192 | public function getStatusLabel($label) 193 | { 194 | $labels = [ 195 | self::STATUS_STOPPED => Module::t('main', 'Stopped'), 196 | self::STATUS_BURIED => Module::t('main', 'Buried'), 197 | self::STATUS_DONE => Module::t('main', 'Done'), 198 | self::STATUS_FAILED => Module::t('main', 'Failed'), 199 | self::STATUS_RESTARTED => Module::t('main', 'Restarted'), 200 | self::STATUS_STARTED => Module::t('main', 'Started'), 201 | self::STATUS_WAITING => Module::t('main', 'Waiting'), 202 | ]; 203 | if (!isset($labels[$label])) { 204 | throw new InvalidArgumentException('label not found'); 205 | } 206 | return $labels[$label]; 207 | } 208 | /** 209 | * @return Queue|null 210 | */ 211 | public function getSender() 212 | { 213 | return Yii::$app->get($this->sender_name, false); 214 | } 215 | 216 | /** 217 | * @return bool 218 | */ 219 | public function isSenderValid() 220 | { 221 | return $this->getSender() instanceof Queue; 222 | } 223 | 224 | /** 225 | * @param JobInterface|mixed $job 226 | */ 227 | public function setJob($job) 228 | { 229 | $this->job_class = get_class($job); 230 | $data = []; 231 | foreach (get_object_vars($job) as $name => $value) { 232 | $data[$name] = $this->serializeData($value); 233 | } 234 | $this->job_data = Json::encode($data); 235 | } 236 | 237 | /** 238 | * @return array of job properties 239 | */ 240 | public function getJobParams() 241 | { 242 | if (is_resource($this->job_data)) { 243 | $this->job_data = stream_get_contents($this->job_data); 244 | } 245 | $params = []; 246 | foreach (Json::decode($this->job_data) as $name => $value) { 247 | $params[$name] = $this->unserializeData($value); 248 | } 249 | return $params; 250 | } 251 | 252 | /** 253 | * @return bool 254 | */ 255 | public function isJobValid() 256 | { 257 | return is_subclass_of($this->job_class, JobInterface::class); 258 | } 259 | 260 | /** 261 | * @return JobInterface|mixed 262 | */ 263 | public function createJob() 264 | { 265 | return Yii::createObject(['class' => $this->job_class] + $this->getJobParams()); 266 | } 267 | 268 | /** 269 | * @return bool 270 | */ 271 | public function canPushAgain() 272 | { 273 | return $this->isSenderValid() && $this->isJobValid(); 274 | } 275 | 276 | /** 277 | * @return bool marked as stopped 278 | */ 279 | public function isStopped() 280 | { 281 | return !!$this->stopped_at; 282 | } 283 | 284 | /** 285 | * @return bool ability to mark as stopped 286 | */ 287 | public function canStop() 288 | { 289 | if ($this->isStopped()) { 290 | return false; 291 | } 292 | if ($this->lastExec && $this->lastExec->isDone() && !$this->lastExec->retry) { 293 | return false; 294 | } 295 | return true; 296 | } 297 | 298 | /** 299 | * Marks as stopped 300 | */ 301 | public function stop() 302 | { 303 | $this->stopped_at = time(); 304 | $this->save(false); 305 | } 306 | 307 | /** 308 | * @param mixed $data 309 | * @return mixed 310 | */ 311 | private function serializeData($data) 312 | { 313 | if (is_object($data)) { 314 | $result = ['=class=' => get_class($data)]; 315 | foreach (get_object_vars($data) as $name => $value) { 316 | $result[$name] = $this->serializeData($value); 317 | } 318 | return $result; 319 | } 320 | 321 | if (is_array($data)) { 322 | $result = []; 323 | foreach ($data as $name => $value) { 324 | $result[$name] = $this->serializeData($value); 325 | } 326 | } 327 | 328 | return $data; 329 | } 330 | 331 | /** 332 | * @param mixed $data 333 | * @return mixed 334 | */ 335 | private function unserializeData($data) 336 | { 337 | if (!is_array($data)) { 338 | return $data; 339 | } 340 | 341 | if (!isset($data['=class='])) { 342 | $result = []; 343 | foreach ($data as $key => $value) { 344 | $result[$key] = $this->unserializeData($value); 345 | } 346 | return $result; 347 | } 348 | 349 | $config = ['class' => $data['=class=']]; 350 | unset($data['=class=']); 351 | foreach ($data as $property => $value) { 352 | $config[$property] = $this->unserializeData($value); 353 | } 354 | return Yii::createObject($config); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/records/WorkerQuery.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class WorkerQuery extends ActiveQuery 19 | { 20 | /** 21 | * @var Env 22 | */ 23 | private $env; 24 | 25 | /** 26 | * @param string $modelClass 27 | * @param Env $env 28 | * @param array $config 29 | * @inheritdoc 30 | */ 31 | public function __construct($modelClass, Env $env, array $config = []) 32 | { 33 | $this->env = $env; 34 | parent::__construct($modelClass, $config); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function init() 41 | { 42 | parent::init(); 43 | $this->alias('worker'); 44 | } 45 | 46 | /** 47 | * @param string $host 48 | * @param int $pid 49 | * @return $this 50 | */ 51 | public function byEvent($host, $pid) 52 | { 53 | return $this->andWhere([ 54 | 'worker.host' => $host, 55 | 'worker.pid' => $pid, 56 | ]); 57 | } 58 | 59 | /** 60 | * @return $this 61 | */ 62 | public function active() 63 | { 64 | return $this 65 | ->andWhere(['worker.finished_at' => null]) 66 | ->leftJoin(['exec' => ExecRecord::tableName()], '{{exec}}.[[id]] = {{worker}}.[[last_exec_id]]') 67 | ->leftJoin(['push' => PushRecord::tableName()], '{{push}}.[[id]] = {{exec}}.[[push_id]]') 68 | ->andWhere([ 69 | 'or', 70 | ['>', 'worker.pinged_at', time() - $this->env->workerPingInterval - 5], 71 | [ 72 | 'and', 73 | ['is not', 'worker.last_exec_id', null], 74 | ['exec.finished_at' => null], 75 | ], 76 | ]); 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | * @return WorkerRecord[]|array 82 | */ 83 | public function all($db = null) 84 | { 85 | return parent::all($db); 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | * @return WorkerRecord|array|null 91 | */ 92 | public function one($db = null) 93 | { 94 | return parent::one($db); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/records/WorkerRecord.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | class WorkerRecord extends ActiveRecord 41 | { 42 | /** 43 | * @inheritdoc 44 | * @return WorkerQuery|object the active query used by this AR class. 45 | */ 46 | public static function find() 47 | { 48 | return Yii::createObject(WorkerQuery::class, [get_called_class()]); 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public static function getDb() 55 | { 56 | return Env::ensure()->db; 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public static function tableName() 63 | { 64 | return Env::ensure()->workerTableName; 65 | } 66 | 67 | public function attributeLabels() 68 | { 69 | return [ 70 | 'id' => Module::t('main', 'ID'), 71 | 'sender_name' => Module::t('main', 'Sender'), 72 | 'host' => Module::t('main', 'Host'), 73 | 'pid' => Module::t('main', 'PID'), 74 | 'status' => Module::t('main', 'Status'), 75 | 'started_at' => Module::t('main', 'Started At'), 76 | 'execTotalStarted' => Module::t('main', 'Total Started'), 77 | 'execTotalDone' => Module::t('main', 'Total Done'), 78 | ]; 79 | } 80 | 81 | /** 82 | * @return ExecQuery|\yii\db\ActiveQuery 83 | */ 84 | public function getLastExec() 85 | { 86 | return $this->hasOne(ExecRecord::class, ['id' => 'last_exec_id']); 87 | } 88 | 89 | /** 90 | * @return ExecQuery|\yii\db\ActiveQuery 91 | */ 92 | public function getExecs() 93 | { 94 | return $this->hasMany(ExecRecord::class, ['worker_id' => 'id']); 95 | } 96 | 97 | /** 98 | * @return ExecQuery|\yii\db\ActiveQuery 99 | */ 100 | public function getExecTotal() 101 | { 102 | return $this->hasOne(ExecRecord::class, ['worker_id' => 'id']) 103 | ->select([ 104 | 'exec.worker_id', 105 | 'started' => 'COUNT(*)', 106 | 'done' => 'COUNT(exec.finished_at)', 107 | ]) 108 | ->groupBy('worker_id') 109 | ->asArray(); 110 | } 111 | 112 | /** 113 | * @return int 114 | */ 115 | public function getExecTotalStarted() 116 | { 117 | return ArrayHelper::getValue($this->execTotal, 'started', 0); 118 | } 119 | 120 | /** 121 | * @return int 122 | */ 123 | public function getExecTotalDone() 124 | { 125 | return ArrayHelper::getValue($this->execTotal, 'done', 0); 126 | } 127 | 128 | /** 129 | * @return int 130 | */ 131 | public function getDuration() 132 | { 133 | if ($this->finished_at) { 134 | return $this->finished_at - $this->started_at; 135 | } 136 | return time() - $this->started_at; 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function getStatus() 143 | { 144 | $format = Module::getInstance()->formatter; 145 | if (!$this->lastExec) { 146 | return Module::t('main', 'Idle since {time}.', [ 147 | 'time' => $format->asRelativeTime($this->started_at), 148 | ]); 149 | } 150 | if ($this->lastExec->finished_at) { 151 | return Module::t('main', 'Idle after a job since {time}.', [ 152 | 'time' => $format->asRelativeTime($this->lastExec->finished_at), 153 | ]); 154 | } 155 | return Module::t('main', 'Busy since {time}.', [ 156 | 'time' => $format->asRelativeTime($this->lastExec->started_at), 157 | ]); 158 | } 159 | 160 | /** 161 | * @return bool 162 | */ 163 | public function isIdle() 164 | { 165 | return !$this->lastExec || $this->lastExec->finished_at; 166 | } 167 | 168 | /** 169 | * @return bool marked as stopped 170 | */ 171 | public function isStopped() 172 | { 173 | return !!$this->stopped_at; 174 | } 175 | 176 | /** 177 | * Marks as stopped 178 | */ 179 | public function stop() 180 | { 181 | $this->stopped_at = time(); 182 | $this->save(false); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/views/job/_data-list.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/job/_index-item.php: -------------------------------------------------------------------------------- 1 | formatter; 13 | $status = $model->getStatus(); 14 | switch ($status) { 15 | case PushRecord::STATUS_STOPPED: 16 | $statusClass = 'bg-info'; 17 | break; 18 | case PushRecord::STATUS_WAITING: 19 | case PushRecord::STATUS_STARTED: 20 | $statusClass = 'bg-success'; 21 | break; 22 | case PushRecord::STATUS_FAILED: 23 | case PushRecord::STATUS_RESTARTED: 24 | $statusClass = 'bg-warning'; 25 | break; 26 | case PushRecord::STATUS_BURIED: 27 | $statusClass = 'bg-danger'; 28 | break; 29 | default: 30 | $statusClass = 'bg-default'; 31 | } 32 | ?> 33 |
34 |
asText($model->getStatusLabel($status)) ?>
35 |
36 | 50 |
51 | : asDatetime($model->pushed_at) ?> 52 |
53 |
54 | : asInteger($model->ttr) ?>s 55 |
56 |
57 | : asInteger($model->delay) ?>s 58 |
59 |
60 | : asInteger($model->getAttemptCount()) ?> 61 |
62 |
63 | : asInteger($model->getWaitTime()) ?>s 64 |
65 | lastExec): ?> 66 |
67 | : asInteger($model->lastExec->getDuration()) ?>s 68 | lastExec->memory_usage): ?> 69 | / asShortSize($model->lastExec->memory_usage, 0) ?> 70 | 71 |
72 | 73 |
74 |
75 | asText($model->job_class) ?> 76 |
77 |
78 | getJobParams() as $property => $value): ?> 79 | 80 | asText($property) ?> = 81 | charset, true) ?> 82 | 83 | 84 |
85 | lastExec && $model->lastExec->isFailed()): ?> 86 |
87 | : 88 | asText($model->lastExec->getErrorMessage()) ?> 89 |
90 | 91 |
92 |
93 | -------------------------------------------------------------------------------- /src/views/job/_job-filter.php: -------------------------------------------------------------------------------- 1 | 13 |
14 | 'job-filter', 16 | 'method' => 'get', 17 | 'action' => ['/' . Yii::$app->controller->route], 18 | 'enableClientValidation' => false, 19 | ]) ?> 20 |
21 |
22 | field($filter, 'is')->dropDownList($filter->scopeList(), ['prompt' => '']) ?> 23 |
24 |
25 | field($filter, 'sender')->textInput(['list' => 'job-sender']) ?> 26 | render('_data-list', ['id' => 'job-sender', 'values' => $filter->senderList()]) ?> 27 |
28 |
29 | field($filter, 'class')->textInput(['list' => 'job-class']) ?> 30 | render('_data-list', ['id' => 'job-class', 'values' => $filter->classList()]) ?> 31 |
32 |
33 | field($filter, 'contains') ?> 34 |
35 |
36 | field($filter, 'pushed_after')->input('datetime-local', [ 37 | 'placeholder' => 'YYYY-MM-DDTHH:MM', 38 | ]) ?> 39 |
40 |
41 | field($filter, 'pushed_before')->input('datetime-local', [ 42 | 'placeholder' => 'YYYY-MM-DDTHH:MM', 43 | ]) ?> 44 |
45 |
46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | -------------------------------------------------------------------------------- /src/views/job/_table.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |

.

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | $value): ?> 24 | 25 | 26 | 27 | 28 | 29 | 30 |
charset, true) ?>
31 | 32 | registerCss( 34 | <<params['breadcrumbs'][] = [ 13 | 'label' => Module::t('main', 'Jobs'), 14 | 'url' => ['index'], 15 | ]; 16 | if ($filtered = JobFilter::restoreParams()) { 17 | $this->params['breadcrumbs'][] = [ 18 | 'label' => Module::t('main', 'Filtered'), 19 | 'url' => ['index'] + $filtered, 20 | ]; 21 | } 22 | if ($parent = $record->parent) { 23 | $this->params['breadcrumbs'][] = [ 24 | 'label' => "#$parent->job_uid", 25 | 'url' => [Yii::$app->requestedAction->id, 'id' => $parent->id], 26 | ]; 27 | } 28 | $this->params['breadcrumbs'][] = [ 29 | 'label' => "#$record->job_uid", 30 | 'url' => [Yii::$app->requestedAction->id, 'id' => $record->id], 31 | ]; 32 | 33 | $module = Module::getInstance(); 34 | ?> 35 |
36 | canExecStop): ?> 37 | $record->id], [ 38 | 'title' => Module::t('main', 'Mark as stopped.'), 39 | 'data' => [ 40 | 'method' => 'post', 41 | 'confirm' => Yii::t('yii', 'Are you sure?'), 42 | ], 43 | 'disabled' => !$record->canStop(), 44 | 'class' => 'btn btn-' . ($record->canStop() ? 'danger' : 'default'), 45 | ]) ?> 46 | 47 | canPushAgain): ?> 48 | $record->id], [ 49 | 'title' => Module::t('main', 'Push again.'), 50 | 'data' => [ 51 | 'method' => 'post', 52 | 'confirm' => Yii::t('yii', 'Are you sure?'), 53 | ], 54 | 'disabled' => !$record->canPushAgain(), 55 | 'class' => 'btn btn-' . ($record->canPushAgain() ? 'primary' : 'default'), 56 | ]) ?> 57 | 58 |
59 | ['class' =>'nav nav-tabs'], 61 | 'items' => [ 62 | [ 63 | 'label' => Module::t('main', 'Details'), 64 | 'url' => ['job/view-details', 'id' => $record->id], 65 | ], 66 | [ 67 | 'label' => Module::t('main', 'Context'), 68 | 'url' => ['job/view-context', 'id' => $record->id], 69 | ], 70 | [ 71 | 'label' => Module::t('main', 'Data'), 72 | 'url' => ['job/view-data', 'id' => $record->id], 73 | ], 74 | [ 75 | 'label' => Module::t('main', 'Attempts ({attempts})', [ 76 | 'attempts'=>$record->attemptCount 77 | ]), 78 | 'url' => ['job/view-attempts', 'id' => $record->id], 79 | ], 80 | ], 81 | ]) ?> 82 | -------------------------------------------------------------------------------- /src/views/job/index.php: -------------------------------------------------------------------------------- 1 | params['breadcrumbs'][] = ['label' => Module::t('main', 'Jobs'), 'url' => ['index']]; 17 | $this->params['breadcrumbs'][] = Module::t('main', 'Filtered'); 18 | } else { 19 | $this->params['breadcrumbs'][] = Module::t('main', 'Jobs'); 20 | } 21 | 22 | JobItemAsset::register($this); 23 | ?> 24 |
25 |
26 |
27 | 28 | render('_job-filter', compact('filter')) ?> 29 | 30 |
31 |
32 | 33 | new ActiveDataProvider([ 35 | 'query' => $filter->search() 36 | ->with(['parent', 'firstExec', 'lastExec', 'execTotal']), 37 | 'sort' => [ 38 | 'defaultOrder' => [ 39 | 'id' => SORT_DESC, 40 | ], 41 | ], 42 | ]), 43 | 'emptyText' => Module::t('main', 'No jobs found.'), 44 | 'emptyTextOptions' => ['class' => Module::t('main', 'empty lead')], 45 | 'itemView' => '_index-item', 46 | 'itemOptions' => ['tag' => null], 47 | ]) ?> 48 | 49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /src/views/job/view-attempts.php: -------------------------------------------------------------------------------- 1 | render('_view-nav', ['record' => $record]); 15 | 16 | $this->params['breadcrumbs'][] = Module::t('main', 'Attempts'); 17 | 18 | $format = Module::getInstance()->formatter; 19 | ?> 20 |
21 | new ActiveDataProvider([ 23 | 'query' => $record->getExecs(), 24 | 'sort' => [ 25 | 'attributes' => [ 26 | 'attempt', 27 | ], 28 | 'defaultOrder' => [ 29 | 'attempt' => SORT_DESC, 30 | ], 31 | ], 32 | ]), 33 | 'layout' => "{items}\n{pager}", 34 | 'emptyText' => Module::t('main', 'No workers found.'), 35 | 'tableOptions' => ['class' => 'table table-hover'], 36 | 'formatter' => $format, 37 | 'columns' => [ 38 | [ 39 | 'attribute' => 'attempt', 40 | 'format' => 'integer', 41 | 'label' => Module::t('main', 'Attempt') 42 | ], 43 | [ 44 | 'attribute' => 'started_at', 45 | 'format' => 'datetime', 46 | 'label' => Module::t('main', 'Started') 47 | ], 48 | [ 49 | 'attribute' => 'finished_at', 50 | 'format' => 'time', 51 | 'label' => Module::t('main', 'Finished') 52 | ], 53 | [ 54 | 'attribute' => 'duration', 55 | 'format' => 'duration', 56 | 'label' => Module::t('main', 'Duration') 57 | ], 58 | [ 59 | 'attribute' => 'memory_usage', 60 | 'format' => 'shortSize', 61 | 'label' => Module::t('main', 'Memory Usage') 62 | ], 63 | [ 64 | 'attribute' => 'retry', 65 | 'format' => 'boolean', 66 | 'label' => Module::t('main', 'Is retry?') 67 | ], 68 | ], 69 | 'rowOptions' => function (ExecRecord $record) { 70 | $options = []; 71 | if ($record->isFailed()) { 72 | Html::addCssClass($options, 'danger'); 73 | } 74 | return $options; 75 | }, 76 | 'afterRow' => function (ExecRecord $record) use ($format) { 77 | if ($record->isFailed()) { 78 | return strtr('{error}', [ 79 | '{error}' => $format->asNtext($record->error), 80 | ]); 81 | } 82 | if ($result = $record->getResult()) { 83 | return strtr('{result}', [ 84 | '{result}' => VarDumper::dumpAsString($result), 85 | ]); 86 | } 87 | return ''; 88 | }, 89 | ]) ?> 90 |
91 | registerCss( 93 | << td { 95 | white-space: normal; 96 | word-break: break-all; 97 | } 98 | tr.result-line > td { 99 | white-space: pre; 100 | word-break: break-all; 101 | } 102 | CSS 103 | ); 104 | -------------------------------------------------------------------------------- /src/views/job/view-context.php: -------------------------------------------------------------------------------- 1 | render('_view-nav', ['record' => $record]); 10 | 11 | $this->params['breadcrumbs'][] = Module::t('main', 'Environment'); 12 | 13 | $format = Module::getInstance()->formatter; 14 | ?> 15 |
16 |

17 |
trace ?>
18 |

19 |
context ?>
20 |
21 | -------------------------------------------------------------------------------- /src/views/job/view-data.php: -------------------------------------------------------------------------------- 1 | render('_view-nav', ['record' => $record]); 10 | 11 | $this->params['breadcrumbs'][] = Module::t('main', 'Data'); 12 | ?> 13 |
14 | render('_table', [ 15 | 'values' => $record->getJobParams(), 16 | ]) ?> 17 |
18 | -------------------------------------------------------------------------------- /src/views/job/view-details.php: -------------------------------------------------------------------------------- 1 | render('_view-nav', ['record' => $record]); 15 | 16 | $this->params['breadcrumbs'][] = Module::t('main', 'Details'); 17 | 18 | JobItemAsset::register($this); 19 | ?> 20 |
21 | $record, 23 | 'formatter' => Module::getInstance()->formatter, 24 | 'attributes' => [ 25 | [ 26 | 'attribute' => 'sender_name', 27 | 'format' => 'text', 28 | 'label' => Module::t('main', 'Sender'), 29 | ], 30 | [ 31 | 'attribute' => 'job_uid', 32 | 'format' => 'text', 33 | 'label' => Module::t('main', 'Job UID'), 34 | ], 35 | [ 36 | 'attribute' => 'job_class', 37 | 'format' => 'text', 38 | 'label' => Module::t('main', 'Class'), 39 | ], 40 | [ 41 | 'attribute' => 'ttr', 42 | 'format' => 'integer', 43 | 'label' => Module::t('main', 'Push TTR'), 44 | ], 45 | [ 46 | 'attribute' => 'delay', 47 | 'format' => 'integer', 48 | 'label' => Module::t('main', 'Delay'), 49 | ], 50 | [ 51 | 'attribute' => 'pushed_at', 52 | 'format' => 'relativeTime', 53 | 'label' => Module::t('main', 'Pushed'), 54 | ], 55 | [ 56 | 'attribute' => 'waitTime', 57 | 'format' => 'duration', 58 | 'label' => Module::t('main', 'Wait Time'), 59 | ], 60 | [ 61 | 'attribute' => 'status', 62 | 'format' => 'text', 63 | 'value' => function ($model) { 64 | return $model->getStatusLabel($model->getStatus()); 65 | }, 66 | 'label' => Module::t('main', 'Status'), 67 | ], 68 | ], 69 | 'options' => ['class' => 'table table-hover'], 70 | ]) ?> 71 | 72 | 73 | new ActiveDataProvider([ 75 | 'query' => $record->getChildren() 76 | ->with(['parent', 'firstExec', 'lastExec', 'execTotal']), 77 | 'sort' => [ 78 | 'defaultOrder' => [ 79 | 'id' => SORT_DESC, 80 | ], 81 | ], 82 | ]), 83 | 'layout' => '

' . Module::t('main', 'Sub Jobs') . "

\n{items}\n{pager}", 84 | 'itemView' => '_index-item', 85 | 'itemOptions' => ['tag' => null], 86 | 'emptyText' => false, 87 | ]) ?> 88 | 89 |
90 | -------------------------------------------------------------------------------- /src/views/layouts/_alerts.php: -------------------------------------------------------------------------------- 1 | 'alert-info', 11 | 'success' => 'alert-success', 12 | 'warning' => 'alert-warning', 13 | 'error' => 'alert-danger', 14 | ]; 15 | ?> 16 |
17 | session->getAllFlashes(true) as $type => $message): ?> 18 |
19 | [ 21 | 'class' => isset($aliases[$type]) ? $aliases[$type] : $type, 22 | ], 23 | 'body' => Html::encode($message), 24 | ]) ?> 25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /src/views/layouts/main.php: -------------------------------------------------------------------------------- 1 | 18 | beginPage() ?> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <?= Html::encode($this->title) ?> 27 | head() ?> 28 | 29 | 30 | beginBody() ?> 31 |
32 | Module::t('main', 'Queue Monitor'), 35 | 'brandUrl' => ['/' . Module::getInstance()->id], 36 | 'options' => ['class' => 'navbar-inverse navbar-fixed-top'], 37 | ]); 38 | echo Nav::widget([ 39 | 'options' => ['class' => 'nav navbar-nav'], 40 | 'items' => [ 41 | [ 42 | 'label' => Module::t('main', 'Jobs'), 43 | 'url' => ['job/index'] + JobFilter::restoreParams(), 44 | 'active' => Yii::$app->controller->id === 'job', 45 | ], 46 | [ 47 | 'label' => Module::t('main', 'Workers'), 48 | 'url' => ['worker/index'] + WorkerFilter::restoreParams(), 49 | 'active' => Yii::$app->controller->id === 'worker', 50 | ], 51 | ], 52 | ]); 53 | echo Nav::widget([ 54 | 'options' => ['class' => 'nav navbar-nav navbar-right'], 55 | 'items' => [ 56 | [ 57 | 'label' => Module::t('main', 'Application'), 58 | 'url' => Yii::$app->homeUrl, 59 | ], 60 | ], 61 | ]); 62 | NavBar::end(); 63 | ?> 64 |
65 | false, 67 | 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], 68 | ]) ?> 69 | render('_alerts') ?> 70 | 71 |
72 |
73 | 74 | 81 | 82 | endBody() ?> 83 | 84 | 85 | endPage() ?> 86 | -------------------------------------------------------------------------------- /src/views/worker/index.php: -------------------------------------------------------------------------------- 1 | params['breadcrumbs'][] = Module::t('main', 'Workers'); 16 | 17 | $format = Module::getInstance()->formatter; 18 | ?> 19 |
20 | new ActiveDataProvider([ 22 | 'query' => $filter->search() 23 | ->active() 24 | ->with('execTotal') 25 | ->with('lastExec.push'), 26 | 'sort' => [ 27 | 'attributes' => [ 28 | 'host', 29 | 'pid', 30 | 'started_at' => [ 31 | 'asc' => ['sender_name' => SORT_ASC, 'id' => SORT_ASC], 32 | 'desc' => ['sender_name' => SORT_ASC, 'id' => SORT_DESC], 33 | ], 34 | ], 35 | 'defaultOrder' => [ 36 | 'started_at' => SORT_ASC, 37 | ], 38 | ], 39 | ]), 40 | 'emptyText' => Module::t('main', 'No workers found.'), 41 | 'tableOptions' => ['class' => 'table table-hover'], 42 | 'formatter' => $format, 43 | 'columns' => [ 44 | 'host', 45 | 'pid', 46 | 'started_at:datetime', 47 | 'status:raw', 48 | 'execTotalDone:integer', 49 | [ 50 | 'class' => ActionColumn::class, 51 | 'template' => '{stop}', 52 | 'buttons' => [ 53 | 'stop' => function ($url) { 54 | return Html::a(Html::icon('stop'), $url, [ 55 | 'data' => ['method' => 'post', 'confirm' => Yii::t('yii', 'Are you sure?')], 56 | 'title' => Module::t('main', 'Stop the worker.'), 57 | ]); 58 | }, 59 | ], 60 | 'visibleButtons' => [ 61 | 'stop' => function (WorkerRecord $model) { 62 | return Module::getInstance()->canWorkerStop && !$model->isStopped(); 63 | }, 64 | ], 65 | ], 66 | ], 67 | 'rowOptions' => function (WorkerRecord $record) { 68 | if (!$record->isIdle()) { 69 | return ['class' => 'active']; 70 | } 71 | return []; 72 | }, 73 | 'beforeRow' => function (WorkerRecord $record) use ($format) { 74 | static $senderName; 75 | if ($senderName === $record->sender_name) { 76 | return ''; 77 | } 78 | $senderName = $record->sender_name; 79 | $groupTitle = Module::t('main', 'Sender: {name} {class}', [ 80 | 'name' => $record->sender_name, 81 | 'class' => get_class(Yii::$app->get($record->sender_name)), 82 | ]); 83 | return Html::tag('tr', Html::tag('th', $format->asText($groupTitle), ['colspan' => 6])); 84 | }, 85 | ]) ?> 86 |
87 | -------------------------------------------------------------------------------- /src/web/job-item.css: -------------------------------------------------------------------------------- 1 | .job-item { 2 | position: relative; 3 | padding: 10px; 4 | } 5 | .job-status { 6 | float:right; 7 | font-weight: bold; 8 | text-transform: uppercase; 9 | } 10 | .job-status, 11 | .job-details { 12 | font-size: 90%; 13 | } 14 | .job-details > * { 15 | display: inline-block; 16 | margin-right: 15px; 17 | } 18 | .job-push-uid { 19 | font-weight: bold; 20 | } 21 | .job-class { 22 | margin-top: 5px; 23 | font-size: 125%; 24 | font-weight: bold; 25 | } 26 | .job-params { 27 | font-size: 85%; 28 | color: #777; 29 | } 30 | .job-param { 31 | margin-right: 5px; 32 | } 33 | .job-param:last-child { 34 | margin-right: 0; 35 | } 36 | .job-param-name, 37 | .job-param-value:after { 38 | font-weight: bold; 39 | } 40 | .job-param-name { 41 | white-space: nowrap; 42 | } 43 | .job-param-value { 44 | word-break: break-all; 45 | } 46 | .job-param-value:after { 47 | content: ";" 48 | } 49 | .job-param:last-child > .job-param-value:after { 50 | content: "" 51 | } 52 | .job-error { 53 | margin-top: 10px; 54 | font-size: 90%; 55 | word-break: break-all; 56 | } 57 | 58 | .job-item.bg-default { 59 | border-top: 1px solid #ddd; 60 | } 61 | .job-item.bg-default:hover { 62 | background-color: #eee; 63 | } 64 | .job-item:hover, 65 | .job-item.bg-info, 66 | .job-item.bg-success, 67 | .job-item.bg-warning, 68 | .job-item.bg-danger { 69 | border-radius: 4px; 70 | border-top: 1px solid #fff; 71 | } 72 | :hover + .job-item.bg-default, 73 | .bg-info + .job-item.bg-default, 74 | .bg-success + .job-item.bg-default, 75 | .bg-warning + .job-item.bg-default, 76 | .bg-danger + .job-item.bg-default { 77 | border-top-color: #fff; 78 | } 79 | .job-item { 80 | padding-left: 17px; 81 | } 82 | .job-border { 83 | position: absolute; 84 | display: none; 85 | left: 0; 86 | top: 0; 87 | bottom: 0; 88 | width: 7px; 89 | border-radius: 4px 0 0 4px; 90 | border-right: 1px solid #fff; 91 | background-color: #999; 92 | } 93 | .job-item:hover .job-border { 94 | display: block; 95 | } 96 | -------------------------------------------------------------------------------- /src/web/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuravljov/yii2-queue-monitor/fa5246ba653164848fbd27b86a83e1adb8145c2e/src/web/logo.png -------------------------------------------------------------------------------- /src/web/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | .wrap { 6 | min-height: 100%; 7 | height: auto; 8 | margin: 0 auto -60px; 9 | padding: 0 0 60px; 10 | } 11 | 12 | .wrap > .container { 13 | padding: 70px 15px 20px; 14 | } 15 | 16 | .footer { 17 | height: 60px; 18 | background-color: #f5f5f5; 19 | border-top: 1px solid #ddd; 20 | padding-top: 20px; 21 | } 22 | .navbar-brand { 23 | background: url(logo.png) 15px center no-repeat; 24 | padding-left: 50px; 25 | } 26 | .not-set { 27 | color: #c55; 28 | font-style: italic; 29 | } 30 | form, 31 | .alert, 32 | .nav-tabs { 33 | margin-bottom: 20px; 34 | } 35 | -------------------------------------------------------------------------------- /src/web/stat-index.js: -------------------------------------------------------------------------------- 1 | function renderPie(id, data, onclick) { 2 | var pie = new Chart(document.getElementById(id).getContext('2d'), { 3 | type: 'pie', 4 | data: { 5 | datasets: [{ 6 | data: data.map(function(d) { 7 | return parseInt(d.count); 8 | }), 9 | backgroundColor: [ 10 | '#537bc4', 11 | '#acc236', 12 | '#166a8f', 13 | '#00a950', 14 | '#58595b', 15 | '#8549ba' 16 | ], 17 | label: 'Dataset 1' 18 | }], 19 | labels: data.map(function(d) { 20 | return d.name + ': ' + d.count; 21 | }) 22 | }, 23 | options: { 24 | responsive: true, 25 | legend: { 26 | position: 'right' 27 | }, 28 | tooltips: { 29 | callbacks: { 30 | label: function (item) { 31 | return data[item.index].name + ': ' + data[item.index].count; 32 | } 33 | } 34 | } 35 | } 36 | }); 37 | $('#' + id).click(function(e) { 38 | var elements = pie.getElementAtEvent(e); 39 | if (elements.length) { 40 | onclick(data[elements[0]._index]); 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/widgets/FilterBar.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class FilterBar extends Widget 20 | { 21 | /** 22 | * @inheritdoc 23 | */ 24 | public function init() 25 | { 26 | parent::init(); 27 | ob_start(); 28 | ob_implicit_flush(false); 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function run() 35 | { 36 | BootstrapPluginAsset::register($this->view); 37 | $this->view->registerJs( 38 | <<view->registerCss( 43 | << 'queue-filter-bar', 62 | ]); 63 | } 64 | } 65 | --------------------------------------------------------------------------------