├── assets
├── logo.png
├── rickshaw.min.css
├── main.css
└── rickshaw.min.js
├── composer.json
├── base
└── Controller.php
├── controllers
├── DefaultController.php
├── ScheduleController.php
├── RetryController.php
├── QueueController.php
└── WorkerController.php
├── AsynctaskAsset.php
├── console
├── AsyncTaskQuickController.php
└── AsyncTaskController.php
├── views
├── schedule
│ └── index.php
├── queue
│ ├── index.php
│ └── view.php
├── retry
│ └── index.php
├── worker
│ └── index.php
├── default
│ └── index.php
└── layouts
│ └── main.php
├── Module.php
├── README.md
├── Worker.php
└── Queue.php
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wayhood/yii2-asynctask/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wayhood/yii2-asynctask",
3 | "description": "a queue extension using redis for yii2",
4 | "type": "yii2-extension",
5 | "keywords": ["queue","worker","job","yii2","redis"],
6 | "license": "Apache-2.0",
7 | "authors": [
8 | {
9 | "name": "Song Yeung",
10 | "email": "netyum@163.com"
11 | }
12 | ],
13 | "require": {
14 | "yiisoft/yii2": "*",
15 | "yiisoft/yii2-redis": "*",
16 | "jeremeamia/superclosure": "~1.0.1"
17 | },
18 | "autoload": {
19 | "psr-4": {
20 | "wh\\asynctask\\": ""
21 | }
22 | },
23 | "extra": {
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/base/Controller.php:
--------------------------------------------------------------------------------
1 |
15 | * @date 12/20/14
16 | */
17 | class Controller extends \yii\web\Controller
18 | {
19 | public $layout = 'main';
20 |
21 | public $queue = null;
22 |
23 | public function init()
24 | {
25 | parent::init();
26 |
27 | $this->queue = Yii::createObject([
28 | 'class' => 'wh\asynctask\Queue',
29 | 'redis' => $this->module->redis
30 | ]);
31 | }
32 | }
--------------------------------------------------------------------------------
/controllers/DefaultController.php:
--------------------------------------------------------------------------------
1 |
14 | * @date 12/20/14
15 | */
16 | class DefaultController extends \wh\asynctask\base\Controller
17 | {
18 | public function init()
19 | {
20 | parent::init();
21 | }
22 |
23 | public function actionIndex($days = 30)
24 | {
25 | $data = $this->queue->getShowStat($days);
26 | return $this->render('index', [
27 | 'days' => $days,
28 | 'data' => $data
29 | ]);
30 | }
31 | }
--------------------------------------------------------------------------------
/AsynctaskAsset.php:
--------------------------------------------------------------------------------
1 |
12 | * @date 12/20/14
13 | */
14 | class AsynctaskAsset extends \yii\web\AssetBundle
15 | {
16 | public $sourcePath = '@wh/asynctask/assets';
17 | public $css = [
18 | 'main.css',
19 | 'rickshaw.min.css'
20 | ];
21 | public $js = [
22 | 'd3.v3.js',
23 | 'rickshaw.min.js'
24 | ];
25 | public $depends = [
26 | 'yii\web\YiiAsset',
27 | 'yii\bootstrap\BootstrapAsset',
28 | 'yii\bootstrap\BootstrapPluginAsset',
29 | //'yii\gii\TypeAheadAsset',
30 | ];
31 | }
32 |
--------------------------------------------------------------------------------
/console/AsyncTaskQuickController.php:
--------------------------------------------------------------------------------
1 |
15 | * @date 12/21/14
16 | */
17 | class AsyncTaskQuickController extends \yii\console\Controller
18 | {
19 | /**
20 | * 处理一个Worker
21 | * @param string $q queue name
22 | */
23 | public function actionWorker($q="default")
24 | {
25 | $retried_at = date('Y-m-d H:i:s');
26 |
27 | $queue = Yii::createObject([
28 | 'class' => 'wh\asynctask\Queue',
29 | 'redis' => $this->module->redis
30 | ]);
31 |
32 | while(1) {
33 | $data = $queue->pop($q);
34 | if (!is_null($data)) {
35 | forward_static_call_array([$data['class'], 'run'], $data['args']);
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/views/schedule/index.php:
--------------------------------------------------------------------------------
1 | title = 'Scheduled Jobs ';
11 | ?>
12 |
13 |
14 |
15 |
=Html::encode($this->title);?>
16 |
17 |
18 | = GridView::widget([
19 | 'layout' => "\n{items}",
20 | 'dataProvider' => $dataProvider,
21 | //'filterModel' => $searchModel,
22 | 'columns' => [
23 | [
24 | 'class' => 'yii\grid\CheckboxColumn',
25 | ],
26 | [
27 | 'header' => 'When',
28 | 'value' => function($data) {
29 | return date('Y-m-d H:i:s', intval($data['when']));
30 | }
31 | ],
32 | 'queue',
33 | 'worker',
34 | [
35 | 'class' => 'yii\grid\DataColumn',
36 | 'header' => 'Arguments',
37 | 'value' => function($data) {
38 | return json_encode($data['arguments']);
39 | }
40 | ]
41 | /*[
42 | 'class' => 'yii\grid\ActionColumn',
43 | ],*/
44 | ],
45 | ]); ?>
46 |
--------------------------------------------------------------------------------
/controllers/ScheduleController.php:
--------------------------------------------------------------------------------
1 |
14 | * @date 12/20/14
15 | */
16 | class ScheduleController extends \wh\asynctask\base\Controller
17 | {
18 | public function actionIndex()
19 | {
20 | $schedule = $this->queue->getAllSchedule();
21 |
22 | $newSchedule = [];
23 |
24 | foreach($schedule as $key => $row) {
25 | if ($key % 2 != 0) {
26 | $newSchedule[$key-1]['when'] = $row;
27 | } else {
28 | $data = json_decode($row, true);
29 | $newSchedule[$key] = [
30 | 'job_id' => $data['job_id'],
31 | 'queue' => $data['queue'],
32 | 'worker' => $data['class'],
33 | 'arguments' => $data['args']
34 | ];
35 | }
36 | }
37 |
38 |
39 | $dataProvider = new ArrayDataProvider([
40 | 'key' => 'job_id',
41 | 'allModels' => $newSchedule
42 | ]);
43 |
44 | return $this->render('index', ['dataProvider' => $dataProvider]);
45 | }
46 | }
--------------------------------------------------------------------------------
/controllers/RetryController.php:
--------------------------------------------------------------------------------
1 |
14 | * @date 12/20/14
15 | */
16 | class RetryController extends \wh\asynctask\base\Controller
17 | {
18 | public function actionIndex()
19 | {
20 | $retries = $this->queue->getAllRetries();
21 |
22 | $newRetries = [];
23 |
24 | foreach($retries as $key => $row) {
25 | if ($key % 2 != 0) {
26 | $newRetries[$key-1]['next_retry'] = $row;
27 | } else {
28 | $data = json_decode($row, true);
29 | $newRetries[$key] = [
30 | 'job_id' => $data['job_id'],
31 | 'retry_count' => $data['retry_count'],
32 | 'queue' => $data['queue'],
33 | 'worker' => $data['class'],
34 | 'arguments' => $data['args'],
35 | 'error' => $data['error_message']
36 | ];
37 | }
38 | }
39 |
40 | $dataProvider = new ArrayDataProvider([
41 | 'key' => 'job_id',
42 | 'allModels' => $newRetries
43 | ]);
44 | return $this->render('index', ['dataProvider' => $dataProvider]);
45 | }
46 |
47 | public function actionView($queue)
48 | {
49 |
50 | }
51 | }
--------------------------------------------------------------------------------
/views/queue/index.php:
--------------------------------------------------------------------------------
1 | title = 'Queues';
10 |
11 | ?>
12 |
13 |
14 |
15 |
=Html::encode($this->title);?>
16 |
17 | = GridView::widget([
18 | 'layout' => "\n{items}",
19 | 'dataProvider' => $dataProvider,
20 |
21 | 'columns' => [
22 | [
23 | 'header' => 'Queue',
24 | 'format' => 'raw',
25 | 'value' => function($data) {
26 | return \yii\helpers\Html::a($data['queue'], ['view', 'queue' => $data['queue']]);
27 | }
28 | ],
29 | 'size',
30 | [
31 | 'class' => 'yii\grid\ActionColumn',
32 | 'header' => 'Actions',
33 | 'template' => '{delete-queue}',
34 | 'buttons' => [
35 | 'delete-queue' => function ($url, $model) {
36 | return Html::a('
', $url, [
37 | 'title' => Yii::t('yii', 'Delete'),
38 | 'data-confirm' => Yii::t('yii', 'Are you sure you want to delete this item?'),
39 | 'data-method' => 'post',
40 | 'data-pjax' => '0',
41 | ]);
42 | }
43 | ]
44 | ]
45 | ],
46 | ]); ?>
47 |
--------------------------------------------------------------------------------
/views/retry/index.php:
--------------------------------------------------------------------------------
1 | title = 'Retries';
10 | ?>
11 |
12 |
13 |
14 |
=Html::encode($this->title);?>
15 |
16 | = GridView::widget([
17 | 'layout' => "\n{items}",
18 | 'dataProvider' => $dataProvider,
19 | 'columns' => [
20 | [
21 | 'class' => 'yii\grid\CheckboxColumn',
22 | ],
23 | [
24 | 'header' => 'Next Retry',
25 | 'value' => function($data) {
26 | return date('Y-m-d H:i:s', intval($data['next_retry']));
27 | }
28 | ],
29 | [
30 | 'header' => 'Retry Count',
31 | 'value' => function($data) {
32 | return intval($data['retry_count']);
33 | }
34 | ],
35 | 'queue',
36 | 'worker',
37 | [
38 | 'class' => 'yii\grid\DataColumn',
39 | 'header' => 'Arguments',
40 | 'value' => function($data) {
41 | return json_encode($data['arguments']);
42 | }
43 | ],
44 | [
45 | 'class' => 'yii\grid\DataColumn',
46 | 'header' => 'Arguments',
47 | 'value' => function($data) {
48 | return \yii\helpers\Html::encode($data['error']);
49 | }
50 | ]
51 | /*[
52 | 'class' => 'yii\grid\ActionColumn',
53 | ],*/
54 | ],
55 | ]); ?>
56 |
--------------------------------------------------------------------------------
/Module.php:
--------------------------------------------------------------------------------
1 |
15 | * @date 12/20/14
16 | */
17 | class Module extends \yii\base\Module implements BootstrapInterface
18 | {
19 | public $redis = 'redis';
20 |
21 | private $_workerLogPath = null;
22 |
23 | public function bootstrap($app)
24 | {
25 | if ($app instanceof \yii\web\Application) {
26 | $app->getUrlManager()->addRules([
27 | $this->id => $this->id .'/default/index',
28 | $this->id .'/' => $this->id .'/default/view',
29 | $this->id .'//' => $this->id .'//',
30 | ], false);
31 | } elseif ($app instanceof \yii\console\Application) {
32 | $app->controllerMap[$this->id] = [
33 | 'class' => 'wh\asynctask\console\AsyncTaskController',
34 | 'module' => $this
35 | ];
36 | $app->controllerMap[$this->id.'-quick'] = [
37 | 'class' => 'wh\asynctask\console\AsyncTaskQuickController',
38 | 'module' => $this
39 | ];
40 | }
41 | }
42 |
43 | public function setWorkerLogPath($path)
44 | {
45 | $this->_workerLogPath = Yii::getAlias($path);
46 | }
47 |
48 | public function getWorkerLogPath()
49 | {
50 | if (is_null($this->_workerLogPath)) {
51 | $this->_workerLogPath = Yii::$app->getRuntimePath();
52 | }
53 | return $this->_workerLogPath;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/views/queue/view.php:
--------------------------------------------------------------------------------
1 | title = 'Queues';
10 |
11 | ?>
12 |
13 |
14 |
15 |
Current messages in =$queue;?>
16 |
17 | = GridView::widget([
18 | 'layout' => "\n{items}",
19 | 'dataProvider' => $dataProvider,
20 |
21 | 'columns' => [
22 | 'class',
23 | [
24 | 'class' => 'yii\grid\DataColumn',
25 | 'header' => 'Arguments',
26 | 'value' => function($data) {
27 | return json_encode($data['arguments']);
28 | }
29 | ],
30 | [
31 | 'class' => 'yii\grid\ActionColumn',
32 | 'header' => 'Actions',
33 | 'template' => '{delete-item}',
34 | 'buttons' => [
35 | 'delete-item' => function ($url, $model) {
36 | $url = \yii\helpers\Url::to([
37 | 'delete-item',
38 | 'queue' => $model['queue'],
39 | 'id' => base64_encode($model['content'])
40 | ]);
41 | return Html::a('
', $url, [
42 | 'title' => Yii::t('yii', 'Delete'),
43 | 'data-confirm' => Yii::t('yii', 'Are you sure you want to delete this item?'),
44 | 'data-method' => 'post',
45 | 'data-pjax' => '0',
46 | ]);
47 | }
48 | ]
49 | ]
50 | ],
51 | ]); ?>
52 |
--------------------------------------------------------------------------------
/views/worker/index.php:
--------------------------------------------------------------------------------
1 | title = 'Workers';
11 | ?>
12 |
13 |
14 |
15 |
=Html::encode($this->title);?>
16 |
17 |
18 | = GridView::widget([
19 | 'layout' => "\n{items}",
20 | 'dataProvider' => $dataProvider,
21 | //'filterModel' => $searchModel,
22 | 'columns' => [
23 | [
24 | 'header' => 'Worker',
25 | 'value' => function($data) {
26 | return $data['worker'];
27 | }
28 | ],
29 | 'queue',
30 | 'class',
31 | [
32 | 'class' => 'yii\grid\DataColumn',
33 | 'header' => 'Arguments',
34 | 'value' => function($data) {
35 | return json_encode($data['arguments']);
36 | }
37 | ],
38 | [
39 | 'header' => 'Started',
40 | 'value' => function($data) {
41 | return date('Y-m-d H:i:s', $data['started']);
42 | }
43 | ],
44 | [
45 | 'header' => '操作',
46 | 'class' => 'yii\grid\ActionColumn',
47 | 'template' => '{retry} {delete}',
48 | 'buttons' => [
49 | 'retry' => function($url, $model, $key) {
50 | return Html::a('重试', $url, [
51 | 'title' => '重试',
52 | ]);
53 | },
54 | 'delete' => function($url, $model, $key) {
55 | return Html::a('删除', $url, [
56 | 'title' => '删除'
57 | ]);
58 | }
59 | ]
60 | ]
61 | ],
62 | ]); ?>
63 |
--------------------------------------------------------------------------------
/controllers/QueueController.php:
--------------------------------------------------------------------------------
1 |
14 | * @date 12/20/14
15 | */
16 | class QueueController extends \wh\asynctask\base\Controller
17 | {
18 | public function actionIndex()
19 | {
20 | $queues = $this->queue->getQueues();
21 |
22 | $newQueues = [];
23 |
24 | foreach($queues as $queue) {
25 | $newQueues[] = [
26 | 'queue' => $queue,
27 | 'size' => $this->queue->getQueueSize($queue)
28 | ];
29 | }
30 |
31 | $dataProvider = new ArrayDataProvider([
32 | 'key' => 'queue',
33 | 'allModels' => $newQueues
34 | ]);
35 |
36 | return $this->render('index', ['dataProvider' => $dataProvider]);
37 | }
38 |
39 | public function actionView($queue)
40 | {
41 | $list = $this->queue->getQueueList($queue, 0, 20);
42 | $newList = [];
43 | foreach($list as $row) {
44 | $data = json_decode($row, true);
45 | $newList[] = [
46 | 'job_id' => $data['job_id'],
47 | 'class' => $data['class'],
48 | 'arguments' => $data['args'],
49 | 'queue' => $data['queue'],
50 | 'content' => $row
51 | ];
52 | }
53 |
54 | $dataProvider = new ArrayDataProvider([
55 | 'key' => 'job_id',
56 | 'allModels' => $newList
57 | ]);
58 |
59 | return $this->render('view', ['queue' => $queue, 'dataProvider' => $dataProvider]);
60 | }
61 |
62 | public function actionDeleteQueue($id)
63 | {
64 | $this->queue->removeQueue($id);
65 | return $this->redirect(['index']);
66 | }
67 |
68 | public function actionDeleteItem($id, $queue)
69 | {
70 | $data = base64_decode($id);
71 | $this->queue->removeQueueItem($queue, $data);
72 | return $this->redirect(['view', 'queue' => $queue]);
73 |
74 | }
75 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Async Task module for Yii2
2 | =========
3 | a async task process module using redis for yii2
4 |
5 | Installation
6 | ------------
7 |
8 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
9 |
10 | Either run
11 |
12 | ```
13 | php composer.phar require --prefer-dist wayhood/yii2-asynctask "*"
14 | ```
15 |
16 | or add
17 |
18 | ```
19 | "wayhood/yii2-asynctask": "*"
20 | ```
21 |
22 | to the require section of your `composer.json` file.
23 |
24 |
25 | Usage
26 | -----
27 |
28 | To use this module in a web application. simply add the following code in your application configuration:
29 |
30 | ```php
31 | reutrn [
32 | 'bootstrap' => [..., 'asynctask'],
33 | 'modules' => [
34 | 'asynctask' => [
35 | 'class' => 'wh\asynctask\Module',
36 | 'redis' => 'redis' /* or [
37 | 'class' => 'yii\redis\Connection',
38 | 'hostname' => 'localhost',
39 | 'port' => 6379,
40 | 'database' => 0,
41 | ]*/
42 | ]
43 | ],
44 | ...
45 | ];
46 | ```
47 |
48 | http://path/to/index.php?r=asynctask
49 |
50 | To use this module in a console application. like to web application
51 |
52 | run
53 |
54 | ```php
55 | ./yii asynctask "a, b, c"
56 | ````
57 |
58 | a, b and c was queue name.
59 |
60 |
61 | CREATE WORKER FILE
62 |
63 | ```php
64 |
75 | * @date 12/20/14
76 | */
77 | class TestWorker extends \wh\asynctask\Worker
78 | {
79 | protected static $queue = 'abc'; //a queue name
80 |
81 | protected static $redis = 'reids' //or a configure array.
82 |
83 | public static function run($a, $b)
84 | {
85 | var_dump($a); //real process code
86 | var_dump($b);
87 | }
88 | }
89 | ```
90 |
91 |
92 | CALL THE WORKER IN CONTROLLER OR MODEL AND ANYWHERE.
93 |
94 | ```
95 | // run one time
96 | \frontend\workers\TestWorker::runAysnc('a', 'b');
97 | // run after 10 sec
98 | \frontend\workers\TestWorker::runIn('10s', 'a', 'b');
99 | // run each 10 min
100 | \frontend\workers\TestWorker::runEach('10m', 'a', 'b');
101 | ```
102 |
--------------------------------------------------------------------------------
/controllers/WorkerController.php:
--------------------------------------------------------------------------------
1 |
14 | * @date 12/20/14
15 | */
16 | class WorkerController extends \wh\asynctask\base\Controller
17 | {
18 | public function actionIndex()
19 | {
20 | $workers = $this->queue->getWorkerList();
21 |
22 | $newWorkers = [];
23 |
24 | foreach($workers as $identity) {
25 | $info = $this->queue->getWorkerInfo($identity);
26 | if (is_null($info)) {
27 | break;
28 | }
29 | $hash = json_decode($info, true);
30 |
31 | $newWorkers[] = [
32 | //'job_id' => $hash['playload']['job_id'],
33 | 'worker' => $identity,
34 | 'started' => $this->queue->getWorkerStarted($identity),
35 | 'arguments' => $hash['playload']['args'],
36 | 'queue' => $hash['queue'],
37 | 'class' => $hash['playload']['class'],
38 | ];
39 | }
40 | $dataProvider = new ArrayDataProvider([
41 | 'key' => 'worker',
42 | 'allModels' => $newWorkers
43 | ]);
44 |
45 | return $this->render('index', ['dataProvider' => $dataProvider]);
46 | }
47 |
48 |
49 | public function actionRetry($id)
50 | {
51 | //echo $id;exit;
52 | $info = @json_decode($this->queue->getWorkerInfo($id), true);
53 | if (isset($info['payload'])) {
54 | $data = $info['payload'];
55 | $this->queue->setRetry([
56 | 'retry' => $data['retry'],
57 | 'class' => $data['class'],
58 | 'args' => $data['args'],
59 | 'enqueued_at' => $data['enqueued_at'],
60 | 'error_message' => 'worker faild',
61 | 'failed_at' => date('Y-m-d H:i:s'),
62 | 'job_id' => $data['job_id'],
63 | 'queue' => $data['queue'],
64 | 'retried_at' => isset($data['retried_at']) ? $retried_at : date('Y-m-d H:i:s'),
65 | 'retry_count' => isset($data['retry_count']) ? intval($data['retry_count'])+1 : 0,
66 | ]);
67 | $this->queue->setWorkerEnd($id);
68 | }
69 | $this->redirect(['index']);
70 | }
71 |
72 | public function actionDelete($id)
73 | {
74 | $this->queue->setWorkerEnd($id);
75 | $this->redirect(['index']);
76 | }
77 | }
--------------------------------------------------------------------------------
/views/default/index.php:
--------------------------------------------------------------------------------
1 | title = 'Dashboard ';
8 | ?>
9 |
10 |
11 |
12 |
=Html::encode($this->title);?>
13 |
14 |
15 |
16 |
17 |
18 |
19 | History
20 | = Html::a('1 week', ['index', 'days' => 7], ['class' => 'history-graph '. ($days == 7 ? 'active' : '') ]);?>
21 | = Html::a('1 month', ['index'], ['class' => 'history-graph '. ($days == 30 ? 'active' : '') ]);?>
22 | = Html::a('3 months', ['index', 'days' => 90], ['class' => 'history-graph '. ($days == 90 ? 'active' : '') ]);?>
23 | = Html::a('6 months', ['index', 'days' => 180], ['class' => 'history-graph '. ($days == 180 ? 'active' : '') ]);?>
24 |
25 |
26 |
27 |
28 |
29 | registerJs($js);
99 |
--------------------------------------------------------------------------------
/views/layouts/main.php:
--------------------------------------------------------------------------------
1 |
12 | beginPage() ?>
13 |
14 |
15 |
16 |
17 |
18 | = Html::csrfMetaTags() ?>
19 | = Html::encode($this->title) ?>
20 | head() ?>
21 |
22 |
23 | beginBody() ?>
24 | Html::img($asset->baseUrl . '/logo.png'),
27 | 'brandUrl' => ['default/index'],
28 | 'options' => ['class' => 'navbar-inverse navbar-fixed-top'],
29 | ]);
30 | echo Nav::widget([
31 | 'options' => ['class' => 'nav navbar-nav navbar-right'],
32 | 'route' => Yii::$app->controller->getUniqueId().'/index',
33 | 'items' => [
34 | ['label' => 'Dashboard', 'url' => ['default/index']],
35 | ['label' => 'Workers', 'url' => ['worker/index']],
36 | ['label' => 'Queues', 'url' => ['queue/index']],
37 | ['label' => 'Retries', 'url' => ['retry/index']],
38 | ['label' => 'Scheduled', 'url' => ['schedule/index']],
39 | ],
40 | ]);
41 | NavBar::end();
42 | ?>
43 |
44 |
45 |
82 | = $content ?>
83 |
84 |
85 |
91 |
92 | endBody() ?>
93 |
94 |
95 | endPage() ?>
96 |
--------------------------------------------------------------------------------
/Worker.php:
--------------------------------------------------------------------------------
1 |
14 | * @date 12/20/14
15 | */
16 | abstract class Worker extends \yii\base\Component
17 | {
18 | // a queue name
19 | protected static $queue = 'default';
20 |
21 | // a yii2-redis application component name or configure array
22 | protected static $redis = 'redis';
23 |
24 | protected static $retry = true;
25 |
26 | /**
27 | * async process. please feature in subclass.
28 | * @return mixed
29 | */
30 | //public static function run() {}
31 |
32 | /**
33 | * async push queue
34 | */
35 | public static function runAsync()
36 | {
37 | $payload = [
38 | 'retry' => static::$retry,
39 | 'queue' => static::$queue,
40 | 'class' => static::className(),
41 | 'args' => func_get_args(),
42 | 'job_id' => self::getRandomId(),
43 | 'enqueued_at' => microtime(true)
44 | ];
45 |
46 | $queue = Yii::createObject([
47 | 'class' => 'wh\asynctask\Queue',
48 | 'redis' => static::$redis
49 | ]);
50 |
51 | $queue->push(static::$queue, $payload);
52 | return $payload['job_id'];
53 | }
54 |
55 | /**
56 | * interval d day, h hour, m minute, s second
57 | * schedule
58 | */
59 | public static function runIn()
60 | {
61 | $args = func_get_args();
62 | $time = static::getTime(array_shift($args));
63 |
64 | $payload = [
65 | 'retry' => static::$retry,
66 | 'queue' => static::$queue,
67 | 'class' => static::className(),
68 | 'args' => $args,
69 | 'job_id' => static::getRandomId(),
70 | 'enqueued_at' => microtime(true)
71 | ];
72 |
73 | $score = microtime(true) + $time;
74 |
75 | $queue = Yii::createObject([
76 | 'class' => 'wh\asynctask\Queue',
77 | 'redis' => static::$redis
78 | ]);
79 |
80 | $queue->setSchedule($payload, $score);
81 | return $payload['job_id'];
82 | }
83 |
84 | public static function runEach()
85 | {
86 | $args = func_get_args();
87 | $time = static::getTime(array_shift($args));
88 |
89 | $payload = [
90 | 'retry' => static::$retry,
91 | 'each' => $time,
92 | 'queue' => static::$queue,
93 | 'class' => static::className(),
94 | 'args' => $args,
95 | 'job_id' => static::getRandomId(),
96 | 'enqueued_at' => microtime(true)
97 | ];
98 | $score = microtime(true) + $time;
99 |
100 | $queue = Yii::createObject([
101 | 'class' => 'wh\asynctask\Queue',
102 | 'redis' => static::$redis
103 | ]);
104 |
105 | $queue->setSchedule($payload, $score);
106 | return $payload['job_id'];
107 | }
108 |
109 | protected static function getTime($time)
110 | {
111 | preg_match('/^([0-9]+)[ ]*([hdms]?)$/', $time, $match);
112 | $num = (int) $match[1];
113 | $interval = $match[2];
114 | $second = 0;
115 | switch($interval) {
116 | case 'h':
117 | $second = 60 * 60 * $num;
118 | break;
119 | case 'd':
120 | $second = 60 * 60 * 24 * $num;
121 | break;
122 | case 'm':
123 | $second = 60 * $num;
124 | break;
125 | case 's':
126 | default:
127 | $second = $num;
128 | }
129 |
130 | return $second;
131 | }
132 |
133 | protected static function getRandomId()
134 | {
135 | $queue = Yii::createObject([
136 | 'class' => 'wh\asynctask\Queue',
137 | 'redis' => static::$redis
138 | ]);
139 |
140 | return $queue->redis->incr('queue:'.static::$queue.':counter');
141 | }
142 |
143 | public static function delete($jobIds)
144 | {
145 | $queue = Yii::createObject([
146 | 'class' => 'wh\asynctask\Queue',
147 | 'redis' => static::$redis
148 | ]);
149 | if(!is_array($jobIds)) {
150 | $jobIds = [$jobIds];
151 | }
152 |
153 | $list = $queue->getQueueList(static::$queue, 0, PHP_INT_MAX);
154 | foreach($list as $item) {
155 | $data = json_decode($item, true);
156 | if($data['class'] == get_called_class()
157 | && in_array($data['job_id'], $jobIds)
158 | ) {
159 | $queue->removeQueueItem(static::$queue, $item);
160 | }
161 | }
162 |
163 | $list = $queue->getAllSchedule(static::$queue);
164 | foreach($list as $item) {
165 | $data = json_decode($item, true);
166 | if($data['class'] == get_called_class()
167 | && in_array($data['job_id'], $jobIds)
168 | ) {
169 | $queue->removeScheduleItem($item);
170 | }
171 | }
172 | }
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/assets/rickshaw.min.css:
--------------------------------------------------------------------------------
1 | .rickshaw_graph .detail{pointer-events:none;position:absolute;top:0;z-index:2;background:rgba(0,0,0,.1);bottom:0;width:1px;transition:opacity .25s linear;-moz-transition:opacity .25s linear;-o-transition:opacity .25s linear;-webkit-transition:opacity .25s linear}.rickshaw_graph .detail.inactive{opacity:0}.rickshaw_graph .detail .item.active{opacity:1}.rickshaw_graph .detail .x_label{font-family:Arial,sans-serif;border-radius:3px;padding:6px;opacity:.5;border:1px solid #e0e0e0;font-size:12px;position:absolute;background:#fff;white-space:nowrap}.rickshaw_graph .detail .x_label.left{left:0}.rickshaw_graph .detail .x_label.right{right:0}.rickshaw_graph .detail .item{position:absolute;z-index:2;border-radius:3px;padding:.25em;font-size:12px;font-family:Arial,sans-serif;opacity:0;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(0,0,0,.4);margin-left:1em;margin-right:1em;margin-top:-1em;white-space:nowrap}.rickshaw_graph .detail .item.left{left:0}.rickshaw_graph .detail .item.right{right:0}.rickshaw_graph .detail .item.active{opacity:1;background:rgba(0,0,0,.8)}.rickshaw_graph .detail .item:after{position:absolute;display:block;width:0;height:0;content:"";border:5px solid transparent}.rickshaw_graph .detail .item.left:after{top:1em;left:-5px;margin-top:-5px;border-right-color:rgba(0,0,0,.8);border-left-width:0}.rickshaw_graph .detail .item.right:after{top:1em;right:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,.8);border-right-width:0}.rickshaw_graph .detail .dot{width:4px;height:4px;margin-left:-3px;margin-top:-3.5px;border-radius:5px;position:absolute;box-shadow:0 0 2px rgba(0,0,0,.6);box-sizing:content-box;-moz-box-sizing:content-box;background:#fff;border-width:2px;border-style:solid;display:none;background-clip:padding-box}.rickshaw_graph .detail .dot.active{display:block}.rickshaw_graph{position:relative}.rickshaw_graph svg{display:block;overflow:hidden}.rickshaw_graph .x_tick{position:absolute;top:0;bottom:0;width:0;border-left:1px dotted rgba(0,0,0,.2);pointer-events:none}.rickshaw_graph .x_tick .title{position:absolute;font-size:12px;font-family:Arial,sans-serif;opacity:.5;white-space:nowrap;margin-left:3px;bottom:1px}.rickshaw_annotation_timeline{height:1px;border-top:1px solid #e0e0e0;margin-top:10px;position:relative}.rickshaw_annotation_timeline .annotation{position:absolute;height:6px;width:6px;margin-left:-2px;top:-3px;border-radius:5px;background-color:rgba(0,0,0,.25)}.rickshaw_graph .annotation_line{position:absolute;top:0;bottom:-6px;width:0;border-left:2px solid rgba(0,0,0,.3);display:none}.rickshaw_graph .annotation_line.active{display:block}.rickshaw_graph .annotation_range{background:rgba(0,0,0,.1);display:none;position:absolute;top:0;bottom:-6px}.rickshaw_graph .annotation_range.active{display:block}.rickshaw_graph .annotation_range.active.offscreen{display:none}.rickshaw_annotation_timeline .annotation .content{background:#fff;color:#000;opacity:.9;padding:5px;box-shadow:0 0 2px rgba(0,0,0,.8);border-radius:3px;position:relative;z-index:20;font-size:12px;padding:6px 8px 8px;top:18px;left:-11px;width:160px;display:none;cursor:pointer}.rickshaw_annotation_timeline .annotation .content:before{content:"\25b2";position:absolute;top:-11px;color:#fff;text-shadow:0 -1px 1px rgba(0,0,0,.8)}.rickshaw_annotation_timeline .annotation.active,.rickshaw_annotation_timeline .annotation:hover{background-color:rgba(0,0,0,.8);cursor:none}.rickshaw_annotation_timeline .annotation .content:hover{z-index:50}.rickshaw_annotation_timeline .annotation.active .content{display:block}.rickshaw_annotation_timeline .annotation:hover .content{display:block;z-index:50}.rickshaw_graph .y_axis,.rickshaw_graph .x_axis_d3{fill:none}.rickshaw_graph .y_ticks .tick line,.rickshaw_graph .x_ticks_d3 .tick{stroke:rgba(0,0,0,.16);stroke-width:2px;shape-rendering:crisp-edges;pointer-events:none}.rickshaw_graph .y_grid .tick,.rickshaw_graph .x_grid_d3 .tick{z-index:-1;stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:1 1}.rickshaw_graph .y_grid .tick[data-y-value="0"]{stroke-dasharray:1 0}.rickshaw_graph .y_grid path,.rickshaw_graph .x_grid_d3 path{fill:none;stroke:none}.rickshaw_graph .y_ticks path,.rickshaw_graph .x_ticks_d3 path{fill:none;stroke:gray}.rickshaw_graph .y_ticks text,.rickshaw_graph .x_ticks_d3 text{opacity:.5;font-size:12px;pointer-events:none}.rickshaw_graph .x_tick.glow .title,.rickshaw_graph .y_ticks.glow text{fill:#000;color:#000;text-shadow:-1px 1px 0 rgba(255,255,255,.1),1px -1px 0 rgba(255,255,255,.1),1px 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1),0 -1px 0 rgba(255,255,255,.1),1px 0 0 rgba(255,255,255,.1),-1px 0 0 rgba(255,255,255,.1),-1px -1px 0 rgba(255,255,255,.1)}.rickshaw_graph .x_tick.inverse .title,.rickshaw_graph .y_ticks.inverse text{fill:#fff;color:#fff;text-shadow:-1px 1px 0 rgba(0,0,0,.8),1px -1px 0 rgba(0,0,0,.8),1px 1px 0 rgba(0,0,0,.8),0 1px 0 rgba(0,0,0,.8),0 -1px 0 rgba(0,0,0,.8),1px 0 0 rgba(0,0,0,.8),-1px 0 0 rgba(0,0,0,.8),-1px -1px 0 rgba(0,0,0,.8)}.rickshaw_legend{font-family:Arial;font-size:12px;color:#fff;background:#404040;display:inline-block;padding:12px 5px;border-radius:2px;position:relative}.rickshaw_legend:hover{z-index:10}.rickshaw_legend .swatch{width:10px;height:10px;border:1px solid rgba(0,0,0,.2)}.rickshaw_legend .line{clear:both;line-height:140%;padding-right:15px}.rickshaw_legend .line .swatch{display:inline-block;margin-right:3px;border-radius:2px}.rickshaw_legend .label{margin:0;white-space:nowrap;display:inline;font-size:inherit;background-color:transparent;color:inherit;font-weight:400;line-height:normal;padding:0;text-shadow:none}.rickshaw_legend .action:hover{opacity:.6}.rickshaw_legend .action{margin-right:.2em;font-size:10px;opacity:.2;cursor:pointer;font-size:14px}.rickshaw_legend .line.disabled{opacity:.4}.rickshaw_legend ul{list-style-type:none;margin:0;padding:0;margin:2px;cursor:pointer}.rickshaw_legend li{padding:0 0 0 2px;min-width:80px;white-space:nowrap}.rickshaw_legend li:hover{background:rgba(255,255,255,.08);border-radius:3px}.rickshaw_legend li:active{background:rgba(255,255,255,.2);border-radius:3px}
--------------------------------------------------------------------------------
/assets/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 70px;
3 | }
4 |
5 | .footer {
6 | border-top: 1px solid #ddd;
7 | margin-top: 30px;
8 | padding: 15px 0 30px;
9 | }
10 |
11 | .jumbotron {
12 | text-align: center;
13 | background-color: transparent;
14 | }
15 |
16 | .jumbotron .btn {
17 | font-size: 21px;
18 | padding: 14px 24px;
19 | }
20 |
21 | .navbar-brand {
22 | padding: 0;
23 | margin: 0;
24 | }
25 |
26 | .default-index .generator {
27 | min-height: 200px;
28 | margin-bottom: 20px;
29 | }
30 |
31 | .list-group .glyphicon {
32 | float: right;
33 | }
34 |
35 | .popover {
36 | max-width: 400px;
37 | width: 400px;
38 | }
39 |
40 | .hint-block {
41 | display: none;
42 | }
43 |
44 | .error-summary {
45 | color: #a94442;
46 | background: #fdf7f7;
47 | border-left: 3px solid #eed3d7;
48 | padding: 10px 20px;
49 | margin: 0 0 15px 0;
50 | }
51 |
52 | .default-view .sticky-value {
53 | padding: 6px 12px;
54 | background: lightyellow;
55 | white-space: pre;
56 | word-wrap: break-word;
57 | }
58 |
59 | .default-view .form-group label.help {
60 | border-bottom: 1px dashed #888;
61 | cursor: help;
62 | }
63 |
64 | .default-view .modal-dialog {
65 | width: 800px;
66 | }
67 |
68 | .default-view .modal-dialog .error {
69 | color: #d9534f;
70 | }
71 |
72 | .default-view .modal-dialog .content {
73 | background: #fafafa;
74 | border-left: #eee 5px solid;
75 | padding: 5px 10px;
76 | overflow: auto;
77 | }
78 |
79 | .default-view .modal-dialog code {
80 | background: transparent;
81 | }
82 |
83 | .default-view-files table .action {
84 | width: 100px;
85 | }
86 |
87 | .default-view-files table .check {
88 | width: 25px;
89 | text-align: center;
90 | }
91 |
92 | .default-view-results pre {
93 | overflow: auto;
94 | background-color: #333;
95 | max-height: 300px;
96 | color: white;
97 | padding: 10px;
98 | border-radius: 0;
99 | white-space: nowrap;
100 | }
101 |
102 | .default-view-results pre .error {
103 | background: #FFE0E1;
104 | color: black;
105 | padding: 1px;
106 | }
107 |
108 | .default-view-results .alert pre {
109 | background: white;
110 | }
111 |
112 | .default-diff pre {
113 | padding: 0;
114 | margin: 0;
115 | background: transparent;
116 | border: none;
117 | }
118 |
119 | .default-diff pre del {
120 | background: pink;
121 | }
122 |
123 | .default-diff pre ins {
124 | background: lightgreen;
125 | text-decoration: none;
126 | }
127 |
128 | .Differences {
129 | width: 100%;
130 | border-collapse: collapse;
131 | border-spacing: 0;
132 | empty-cells: show;
133 | }
134 |
135 | .Differences thead {
136 | display: none;
137 | }
138 |
139 | .Differences tbody th {
140 | text-align: right;
141 | background: #FAFAFA;
142 | padding: 1px 2px;
143 | border-right: 1px solid #eee;
144 | vertical-align: top;
145 | font-size: 13px;
146 | font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
147 | font-weight: normal;
148 | color: #999;
149 | width: 5px;
150 | }
151 |
152 | .Differences td {
153 | padding: 1px 2px;
154 | font-size: 13px;
155 | font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
156 | }
157 |
158 | .DifferencesSideBySide .ChangeInsert td.Left {
159 | background: #dfd;
160 | }
161 |
162 | .DifferencesSideBySide .ChangeInsert td.Right {
163 | background: #cfc;
164 | }
165 |
166 | .DifferencesSideBySide .ChangeDelete td.Left {
167 | background: #f88;
168 | }
169 |
170 | .DifferencesSideBySide .ChangeDelete td.Right {
171 | background: #faa;
172 | }
173 |
174 | .DifferencesSideBySide .ChangeReplace .Left {
175 | background: #fe9;
176 | }
177 |
178 | .DifferencesSideBySide .ChangeReplace .Right {
179 | background: #fd8;
180 | }
181 |
182 | .Differences ins, .Differences del {
183 | text-decoration: none;
184 | }
185 |
186 | .DifferencesSideBySide .ChangeReplace ins, .DifferencesSideBySide .ChangeReplace del {
187 | background: #fc0;
188 | }
189 |
190 | .Differences .Skipped {
191 | background: #f7f7f7;
192 | }
193 |
194 | .DifferencesInline .ChangeReplace .Left,
195 | .DifferencesInline .ChangeDelete .Left {
196 | background: #fdd;
197 | }
198 |
199 | .DifferencesInline .ChangeReplace .Right,
200 | .DifferencesInline .ChangeInsert .Right {
201 | background: #dfd;
202 | }
203 |
204 | .DifferencesInline .ChangeReplace ins {
205 | background: #9e9;
206 | }
207 |
208 | .DifferencesInline .ChangeReplace del {
209 | background: #e99;
210 | }
211 |
212 | .DifferencesInline th[data-line-number]:before {
213 | content: attr(data-line-number);
214 | }
215 |
216 | /* additional styles for typeahead.js, adapted from http://twitter.github.io/typeahead.js/examples/ */
217 |
218 | .twitter-typeahead {
219 | display: block !important;
220 | }
221 |
222 | .tt-query {
223 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
224 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
225 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
226 | }
227 |
228 | .tt-hint {
229 | color: #999
230 | }
231 |
232 | .tt-dropdown-menu {
233 | width: 422px;
234 | margin-top: 2px;
235 | padding: 8px 0;
236 | background-color: #fff;
237 | border: 1px solid #ccc;
238 | border: 1px solid rgba(0, 0, 0, 0.2);
239 | -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
240 | -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
241 | box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
242 | }
243 |
244 | .tt-suggestion {
245 | padding: 3px 20px;
246 | font-size: 18px;
247 | line-height: 24px;
248 | }
249 |
250 | .tt-suggestion.tt-cursor {
251 | color: #fff;
252 | background-color: #0097cf;
253 |
254 | }
255 |
256 | .tt-suggestion p {
257 | margin: 0;
258 | }
259 |
260 |
261 | .summary_bar .status {
262 | margin-left: 10px;
263 | }
264 | .summary_bar .summary {
265 | margin-top: 12px;
266 | background-color: #fff;
267 | -webkit-box-shadow: 0px 0px 5px rgba(50, 50, 50, 0.25);
268 | -moz-box-shadow: 0px 0px 5px rgba(50, 50, 50, 0.25);
269 | box-shadow: 0px 0px 5px rgba(50, 50, 50, 0.25);
270 | -webkit-border-radius: 4px;
271 | -webkit-border-radius: 4px;
272 | -moz-border-radius: 4px;
273 | -moz-border-radius: 4px;
274 | border-radius: 4px;
275 | border-radius: 4px;
276 | padding: 8px;
277 | margin-bottom: 10px;
278 | border-width: 0px;
279 | }
280 |
281 | .summary_bar ul h3 {
282 | font-size: 1em;
283 | margin: 0;
284 | font-weight: normal;
285 | line-height: 1em;
286 | }
287 | .summary_bar ul li {
288 | padding: 4px 0 2px 0;
289 | text-align: center;
290 | }
291 | @media (max-width: 767px) and (min-width: 400px) {
292 | .summary_bar ul li span {
293 | width: 50% !important;
294 | }
295 | .summary_bar ul .desc {
296 | text-align: left;
297 | }
298 | .summary_bar ul .count {
299 | text-align: right;
300 | }
301 | }
302 | @media (max-width: 979px) and (min-width: 768px) {
303 | .summary_bar ul li.col-sm-2 {
304 | margin: 0px 10px;
305 | width: 96px !important;
306 | }
307 | }
308 | /*@media (max-width: 1199px) and (min-width: 980px) {
309 | .summary_bar ul li.col-sm-2 {
310 | width: 130px !important;
311 | }
312 | }
313 | @media (min-width: 1200px) {
314 | .summary_bar ul li.col-sm-2 {
315 | width: 154px !important;
316 | }
317 | }*/
318 | .summary_bar ul .desc {
319 | display: block;
320 | font-size: 1em;
321 | font-weight: normal;
322 | width: 100%;
323 | }
324 | .summary_bar ul .count {
325 | color: #b1003e;
326 | display: block;
327 | font-size: 1em;
328 | font-weight: bold;
329 | float: right;
330 | padding: 0px 0px 2px 0px;
331 | width: 100%;
332 | }
333 |
334 | .summary_bar ul a {
335 | color: #b1003e;
336 | }
337 | .summary_bar ul a:active, .summary_bar ul a:hover, .summary_bar ul a:visited {
338 | color: #4b001a;
339 | }
340 | .summary_bar ul a.btn {
341 | color: #585454;
342 | }
343 | .summary_bar ul a.btn-primary {
344 | color: white;
345 | }
346 |
347 | .history-graph.active {
348 | background-color: #b1003e;
349 | color: white;
350 | }
351 | .history-graph {
352 | border-radius: 3px;
353 | font-size: 0.8em;
354 | padding: 3px;
355 | }
356 |
--------------------------------------------------------------------------------
/console/AsyncTaskController.php:
--------------------------------------------------------------------------------
1 |
16 | * @date 12/21/14
17 | */
18 | class AsyncTaskController extends \yii\console\Controller
19 | {
20 | /**
21 | * @var \wh\asynctask\Module
22 | */
23 | public $module;
24 |
25 | /**
26 | * php 环境
27 | * @var string
28 | */
29 | public $phpEnv;
30 |
31 | /**
32 | * max process num;
33 | * @var int
34 | */
35 | public $processMaxNum = 10;
36 |
37 | /**
38 | * max process retries num;
39 | * @var int
40 | */
41 | public $processMaxRetries = 5;
42 |
43 | /**
44 | * 选项
45 | * @param string $actionID
46 | * @return array
47 | */
48 | public function options($actionID)
49 | {
50 | if ($actionID == 'index') {
51 | return array_merge(
52 | parent::options($actionID),
53 | ['phpEnv', 'processMaxNum']
54 | );
55 | } else {
56 | return parent::options($actionID);
57 | }
58 | }
59 |
60 | /**
61 | * 处理一个Worker
62 | * @param string $q queue name
63 | */
64 | public function actionWorker($q="default")
65 | {
66 | $retried_at = date('Y-m-d H:i:s');
67 |
68 | $queue = Yii::createObject([
69 | 'class' => 'wh\asynctask\Queue',
70 | 'redis' => $this->module->redis
71 | ]);
72 |
73 | /** @var $identity string */
74 | $identity = $queue->getWorkerIdentity();
75 |
76 | $logPath = $this->module->getWorkerLogPath();
77 | FileHelper::createDirectory($logPath);
78 | $currentDate = date('Y-m-d');
79 | FileHelper::createDirectory($logPath.'/'.$currentDate);
80 | $logStderr = "{$logPath}/{$currentDate}/{$q}.stderr.log";
81 | $data = $queue->pop($q);
82 |
83 | if (!is_null($data)) {
84 | try {
85 | $queue->setWorkerStart($identity, $data);
86 | $this->setProcessTitle($this->module->id.'/worker '.$data['class'].':run('.implode(', ', $data['args']).')');
87 | forward_static_call_array([$data['class'], 'run'], $data['args']);
88 | } catch (\Exception $e) {
89 | if($data['retry']
90 | && (!isset($data['retry_count']) || $data['retry_count'] < $this->processMaxRetries)
91 | ) {
92 | $queue->setStat(false);
93 | $realCommand = sprintf('echo "%s">>%s &', $e->getMessage(), $logStderr);
94 | exec($realCommand);
95 | $queue->setRetry([
96 | 'retry' => $data['retry'],
97 | 'class' => $data['class'],
98 | 'args' => $data['args'],
99 | 'enqueued_at' => $data['enqueued_at'],
100 | 'error_message' => $e->getMessage(),
101 | 'failed_at' => date('Y-m-d H:i:s'),
102 | 'job_id' => $data['job_id'],
103 | 'queue' => $data['queue'],
104 | 'retried_at' => isset($data['retried_at']) ? $retried_at : date('Y-m-d H:i:s'),
105 | 'retry_count' => isset($data['retry_count']) ? intval($data['retry_count'])+1 : 0,
106 | ]);
107 | }
108 | }
109 | $queue->setStat();
110 | $queue->setWorkerEnd($identity);
111 | }
112 | }
113 |
114 | /**
115 | * process worker main loop
116 | * @param string $q queue name. e.g. a, b, c
117 | */
118 | public function actionIndex($q='default')
119 | {
120 | $command = $this->getCommandLine();
121 |
122 | $logPath = $this->module->getWorkerLogPath();
123 | FileHelper::createDirectory($logPath);
124 | $queue = Yii::createObject([
125 | 'class' => 'wh\asynctask\Queue',
126 | 'redis' => $this->module->redis
127 | ]);
128 |
129 | $queueNames = preg_split('/\s*,\s*/', $q, -1, PREG_SPLIT_NO_EMPTY);
130 |
131 | $currentQueues = $queue->getQueues();
132 |
133 | if (!is_array($currentQueues)) {
134 | $currentQueues = [];
135 | }
136 |
137 | foreach($queueNames as $key => $queueName) {
138 | if (!in_array($queueName, $currentQueues)) {
139 | unset($queueNames[$key]);
140 | }
141 | }
142 |
143 | $currentDate = date('Y-m-d');
144 | FileHelper::createDirectory($logPath.'/'.$currentDate);
145 | while(1) {
146 | $ret = $queue->redis->executeCommand('PING');
147 | if (!$ret) {
148 | break;
149 | }
150 |
151 | if (date('d') != date('d', strtotime($currentDate))) {
152 | sleep(5);
153 | $queue->setStatDay($currentDate);
154 | $currentDate = date('Y-m-d');
155 | FileHelper::createDirectory($logPath.'/'.$currentDate);
156 | }
157 |
158 | //process schedule
159 | $scheduleList = $queue->getSchedule();
160 | foreach($scheduleList as $data) {
161 | $data = @json_decode($data, true);
162 | if (!is_null($data)) {
163 | $queue->quickPush($data['queue'], $data);
164 | if(!empty($data['each'])) {
165 | $queue->setSchedule($data, microtime(true) + $data['each']);
166 | }
167 | }
168 | }
169 | unset($scheduleList);
170 |
171 | //process retry
172 | $retryList = $queue->getRetries();
173 | foreach($retryList as $data) {
174 | $data = @json_decode($data, true);
175 | if (!is_null($data)) {
176 | if ($data['retry_count'] >= 20) {
177 | break;
178 | }
179 | //checkout retry time
180 | $second = pow($data['retry_count'], 4) + 15 + rand(0, 30) * ($data['retry_count'] + 1);
181 | if (time() > strtotime($data['retried_at']) + $second) {
182 | $queue->quickPush($data['queue'], $data);
183 | } else { //no process
184 | $queue->setRetry($data, microtime(true)+$second);
185 | }
186 | }
187 | }
188 |
189 | //process queue
190 | $max = $this->processMaxNum;
191 |
192 | foreach($queueNames as $queueName) {
193 | if ($queue->getQueueSize($queueName) > 0) {
194 | // has queue
195 | $currentSubProcessNum = $this->getCurrentSubProcessNum();
196 | $subProcessNum = $max - $currentSubProcessNum;
197 | if ($subProcessNum > 0) {
198 | $logStdout = "{$logPath}/{$currentDate}/{$queueName}.stdout.log";
199 | $logStderr = "{$logPath}/{$currentDate}/{$queueName}.stderr.log";
200 | $realCommand = sprintf('%s/worker "%s" >> %s 2>>%s &', trim($command), $queueName, $logStdout, $logStderr);
201 | exec($realCommand);
202 | }
203 | }
204 | }
205 | }
206 | }
207 |
208 | /**
209 | * get command line
210 | * @return string
211 | */
212 | protected function getCommandLine()
213 | {
214 | $this->phpEnv = is_null($this->phpEnv) ? '/usr/bin/php' : $this->phpEnv;
215 | $yii = $_SERVER['argv'][0];
216 | $command = $this->phpEnv .' '. $yii .' '. $this->module->id;
217 |
218 | return $command;
219 | }
220 |
221 | /**
222 | * get current sub process num
223 | * @return int
224 | */
225 | protected function getCurrentSubProcessNum()
226 | {
227 | $str = $this->module->id.'/worker';
228 | $current = intval(`ps -ef | grep "$str" | grep -v grep | wc -l`);
229 | return $current;
230 | }
231 |
232 | /**
233 | * Set process title to given string.
234 | * Process title is changing by cli_set_process_title (PHP >= 5.5) or
235 | * setproctitle (if proctitle extension is available).
236 | *
237 | * @param string $title
238 | */
239 | protected function setProcessTitle($title)
240 | {
241 | if (function_exists('cli_set_process_title')) {
242 | cli_set_process_title($title);
243 | } elseif (function_exists('setproctitle')) {
244 | setproctitle($title);
245 | }
246 | }
247 |
248 | }
249 |
--------------------------------------------------------------------------------
/Queue.php:
--------------------------------------------------------------------------------
1 |
16 | * @date 12/20/14
17 | */
18 | class Queue extends \yii\base\Component
19 | {
20 | public $redis = 'redis';
21 |
22 | //默认队列名
23 | public $default = 'default';
24 |
25 | public function init()
26 | {
27 | parent::init();
28 | if (is_string($this->redis)) {
29 | $this->redis = Yii::$app->get($this->redis);
30 | } elseif (is_array($this->redis)) {
31 | if (!isset($this->redis['class'])) {
32 | $this->redis['class'] = Connection::className();
33 | }
34 | $this->redis = Yii::createObject($this->redis);
35 | }
36 | if (!$this->redis instanceof Connection) {
37 | throw new InvalidConfigException("Queue::redis must be either a Redis connection instance or the application component ID of a Redis connection.");
38 | }
39 | }
40 |
41 | /**
42 | * write data to queue
43 | * @param $queue
44 | * @param $data
45 | * @return mixed
46 | */
47 | public function push($queue, $data)
48 | {
49 | $this->redis->sadd('queues', $queue);
50 | $queue = 'queue:'.$queue;
51 | return $this->redis->rpush($queue, json_encode($data));
52 | }
53 |
54 | public function quickPush($queue, $data)
55 | {
56 | $this->redis->sadd('queues', $queue);
57 | $queue = 'queue:'.$queue;
58 | return $this->redis->lpush($queue, json_encode($data));
59 | }
60 |
61 | /**
62 | * get a data from queue
63 | * @param $queue
64 | * @return mixed
65 | */
66 | public function pop($queue)
67 | {
68 | $queue = 'queue:'.$queue;
69 | $data = $this->redis->lpop($queue);
70 | $data = @json_decode($data, true);
71 | return $data;
72 | }
73 |
74 | /**
75 | * get all queues
76 | * @return array|null
77 | */
78 | public function getQueues()
79 | {
80 | return $this->redis->smembers('queues');
81 | }
82 |
83 | /**
84 | * write retry
85 | * @param $data
86 | * @return mixed
87 | */
88 | public function setRetry($data, $score=null)
89 | {
90 | $key = 'retry';
91 | if (is_null($score)) {
92 | $score = doubleval(microtime(true));
93 | } else {
94 | $score = doubleval($score);
95 | }
96 | return $this->redis->zadd($key, $score, json_encode($data));
97 | }
98 |
99 | public function setSchedule($data, $score)
100 | {
101 | $key = 'schedule';
102 | return $this->redis->zadd($key, doubleval($score), json_encode($data));
103 | }
104 |
105 | public function getRetries($remove=true)
106 | {
107 | $key = 'retry';
108 | $score = doubleval(microtime(true));
109 | $result = $this->redis->zrangebyscore($key, -1, $score);
110 | if ($remove) {
111 | $this->redis->zremrangebyscore($key, -1, $score);
112 | }
113 | return $result;
114 | }
115 |
116 | public function getSchedule($remove=true)
117 | {
118 | $key = 'schedule';
119 | $score = doubleval(microtime(true));
120 | $result = $this->redis->zrangebyscore($key, -1, $score);
121 | if ($remove) {
122 | $this->redis->zremrangebyscore($key, -1, $score);
123 | }
124 | return $result;
125 | }
126 |
127 | public function setStat($type = true)
128 | {
129 | $currentFailedKey = 'stat:failed';
130 | $currentProcessedKey = 'stat:processed';
131 | if ($type == true) {
132 | $this->redis->incr($currentProcessedKey);
133 | } else {
134 | $this->redis->incr($currentFailedKey);
135 | }
136 | }
137 |
138 | public function setStatDay($currentDate)
139 | {
140 | $currentFailedKey = 'stat:failed';
141 | $currentProcessedKey = 'stat:processed';
142 |
143 | $currentFailedNum = $this->redis->get($currentFailedKey);
144 | $currentProcessedNum = $this->redis->get($currentProcessedKey);
145 |
146 | $this->redis->set($currentFailedKey.':'.$currentDate, intval($currentFailedNum));
147 | $this->redis->set($currentProcessedKey.':'.$currentDate, intval($currentProcessedNum));
148 |
149 | $this->redis->set($currentFailedKey, 0);
150 | $this->redis->set($currentProcessedKey, 0);
151 |
152 | }
153 |
154 | public function getStatDay($currentDate, $type = true)
155 | {
156 | $currentFailedKey = 'stat:failed:'.$currentDate;
157 | $currentProcessedKey = 'stat:processed:'.$currentDate;
158 | if ($type == true) {
159 | $stat = $this->redis->get($currentProcessedKey);
160 | } else {
161 | $stat = $this->redis->get($currentFailedKey);
162 | }
163 | return intval($stat);
164 | }
165 |
166 | public function getStat($type = true)
167 | {
168 | $currentFailedKey = 'stat:failed';
169 | $currentProcessedKey = 'stat:processed';
170 | if ($type == true) {
171 | $stat = $this->redis->get($currentProcessedKey);
172 | } else {
173 | $stat = $this->redis->get($currentFailedKey);
174 | }
175 | return intval($stat);
176 | }
177 |
178 | public function getWorkerCount()
179 | {
180 | $count = $this->redis->executeCommand('SCARD', ['workers']);
181 | return intval($count);
182 | }
183 |
184 | public function getQueueCount()
185 | {
186 | $queueNames = $this->getQueues();
187 |
188 | $count = 0;
189 | foreach($queueNames as $queueName) {
190 | $count += intval($this->redis->llen('queue:'.$queueName));
191 | }
192 | return $count;
193 | }
194 |
195 | public function getRetryCount()
196 | {
197 | $key = 'retry';
198 | $count = $this->redis->executeCommand('ZLEXCOUNT', [$key, '-', '+']);
199 | return intval($count);
200 | }
201 |
202 | public function getScheduleCount()
203 | {
204 | $key = 'schedule';
205 | $count = $this->redis->executeCommand('ZLEXCOUNT', [$key, '-', '+']);
206 | return intval($count);
207 | }
208 |
209 | public function getAllSchedule()
210 | {
211 | $key = 'schedule';
212 | $result = $this->redis->zrange($key, 0, -1, 'WITHSCORES');
213 | return $result;
214 | }
215 |
216 | public function getAllRetries()
217 | {
218 | $key = 'retry';
219 | $result = $this->redis->zrange($key, 0, -1, 'WITHSCORES');
220 | return $result;
221 | }
222 |
223 | public function getQueueSize($queue)
224 | {
225 | $queue = 'queue:'.$queue;
226 | return $this->redis->llen($queue);
227 | }
228 |
229 | public function getQueueList($queue, $start, $stop)
230 | {
231 | $queue = 'queue:'.$queue;
232 | return $this->redis->lrange($queue, $start, $stop);
233 | }
234 |
235 | public function removeQueue($queue)
236 | {
237 | $this->redis->srem('queues', $queue);
238 | $queue = 'queue:'.$queue;
239 | return $this->redis->del($queue);
240 | }
241 |
242 | public function removeQueueItem($queue, $data)
243 | {
244 | $queue = 'queue:'.$queue;
245 | $this->redis->lrem($queue, -1, $data);
246 | }
247 |
248 | public function removeScheduleItem($data)
249 | {
250 | $key = 'schedule';
251 | return $this->redis->zrem($key, -1, $data);
252 | }
253 |
254 | public function getWorkerIdentity()
255 | {
256 | $pid = @getmypid();
257 | $hostname = @gethostname();
258 | $ip = @gethostbyname($hostname);
259 |
260 | if (!$ip) {
261 | $ip = 'unknow';
262 | }
263 | if (!$hostname) {
264 | $hostname = 'unknow';
265 | }
266 | return $hostname. ':'. $ip .':'. $pid;
267 | }
268 |
269 | public function setWorkerStart($identity, $data)
270 | {
271 | $timeout = 180 * 24 * 60 * 60;
272 | $this->redis->sadd('workers', $identity);
273 | $this->redis->setex('worker:'. $identity. ':started', $timeout, microtime(true));
274 |
275 | $hash = [
276 | 'queue' => $data['queue'],
277 | 'playload' => $data,
278 | 'run_at' => microtime(true),
279 | ];
280 | $this->redis->setex('worker:'. $identity, $timeout, json_encode($hash));
281 | }
282 |
283 | public function setWorkerEnd($identity)
284 | {
285 | $this->redis->srem('workers', $identity);
286 | $this->redis->del('worker:'. $identity);
287 | $this->redis->del('worker:'. $identity .':started');
288 | }
289 |
290 | public function getWorkerList()
291 | {
292 | return $this->redis->smembers('workers');
293 | }
294 |
295 | public function getWorkerStarted($identity)
296 | {
297 | return $this->redis->get('worker:'. $identity .':started');
298 | }
299 |
300 | public function getWorkerInfo($identity)
301 | {
302 | return $this->redis->get('worker:'. $identity);
303 | }
304 |
305 | public function getShowStat($days)
306 | {
307 | $currentDate = date('Y-m-d');
308 | $ret = [
309 | 'processed' => [],
310 | 'failed' => []
311 | ];
312 | $cacheKey = 'stat:'.$currentDate.':'.$days;
313 | $data = $this->redis->get($cacheKey);
314 | if ($data == false) {
315 | $ret['processed'][date('Y-m-d')] = $this->getStat(true);
316 | $ret['failed'][date('Y-m-d')] = $this->getStat(false);
317 | for($i=1; $i<$days; $i++) {
318 | $date = date('Y-m-d', time()-3600*24*$i);
319 | $ret['processed'][$date] = $this->getStatDay($date, true);
320 | $ret['failed'][$date] = $this->getStatDay($date, false);
321 | }
322 | $this->redis->set($cacheKey, json_encode($ret));
323 | $this->redis->expire($cacheKey, 3600*24);
324 | } else {
325 | $ret = @json_decode($data, true);
326 | }
327 |
328 | return $ret;
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/assets/rickshaw.min.js:
--------------------------------------------------------------------------------
1 | (function(root,factory){if(typeof define==="function"&&define.amd){define(["d3"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent)
2 | })}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}self.graph.update()}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw});
--------------------------------------------------------------------------------