├── 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 |

title);?>

16 |

17 | 18 | "\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 |

title);?>

16 |

17 | "\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 |

title);?>

15 |

16 | "\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

16 |

17 | "\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 |

title);?>

16 |

17 | 18 | "\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 |

title);?>

13 |

14 | 15 |
 
16 |
17 |
18 |
19 | History 20 | 7], ['class' => 'history-graph '. ($days == 7 ? 'active' : '') ]);?> 21 | 'history-graph '. ($days == 30 ? 'active' : '') ]);?> 22 | 90], ['class' => 'history-graph '. ($days == 90 ? 'active' : '') ]);?> 23 | 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 | 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 |
46 | 80 | 81 |
82 | 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}); --------------------------------------------------------------------------------