Compare commits

...
26 Commits
Author SHA1 Message Date
ouqiang 93bb9f90b1 时间轮启动参数从配置文件中读取 2017-05-16 11:03:28 +08:00
ouqiang a7fd364b7d 时间轮数据结构由环形链表改为数组存储,提高访问效率 2017-05-15 22:52:47 +08:00
ouqiang c9eb4e31af 删除测试文件 2017-05-14 22:23:11 +08:00
ouqiang 0148fd424b 增加延迟任务 2017-05-14 22:09:36 +08:00
ouqiang ce6ba171e7 bugfix#任务日志列表搜索-状态为后台运行-结果显示不正确 2017-05-13 16:15:31 +08:00
ouqiang 6cd09d7f38 更新版本号为0.1 2017-05-12 10:52:15 +08:00
ouqiang 0f5c4c3e2a 邮件服务器配置-增加清空功能 2017-05-12 10:42:03 +08:00
ouqiang 428275f813 任务、主机编辑页面增加取消按钮 2017-05-12 10:13:42 +08:00
ouqiang 0be9269b8c 系统安装成功后, 自动创建2个测试任务 2017-05-12 09:59:22 +08:00
ouqiang f699a72c28 异步任务回调成功-发送通知 2017-05-10 17:58:05 +08:00
ouqiang 70bccfaea0 任务日志列表-当任务状态为已结束(成功或失败)才显示[任务结果]按钮 2017-05-10 17:14:57 +08:00
ouqiang 0bc4070762 增加删除N个月前的任务日志API 2017-05-10 16:18:21 +08:00
ouqiang 0dfa998d11 完善文档 2017-05-10 11:59:47 +08:00
ouqiang 1b4a6e8c62 替换二进制包下载链接 2017-05-10 08:23:18 +08:00
ouqiang 190cae9ae7 替换二进制包下载链接 2017-05-10 08:19:04 +08:00
ouqiang d1ecbd9043 CI构建-排除第三方库测试 2017-05-09 21:08:07 +08:00
ouqiang 55060aca67 CI构建-排除第三方库测试 2017-05-09 21:06:03 +08:00
ouqiang 2b2d4b237d 修复第三库测试失败 2017-05-09 20:40:16 +08:00
ouqiang 5276fb9074 完善文档 2017-05-09 17:34:28 +08:00
ouqiang 98bf5ee058 二进制包上传到七牛后,刷新CDN 2017-05-09 14:37:40 +08:00
ouqiang 95ea60f23c HTTP任务执行时间不能超过300秒 2017-05-09 13:42:49 +08:00
ouqiang 6f76d3b17e 二进制包改由七牛存储 2017-05-09 09:45:50 +08:00
ouqiang 85996077ec 任务编辑页面-添加主机链接在新窗口打开 2017-05-08 17:54:30 +08:00
ouqiang 7c3f6a58d4 任务执行前后添加日志 2017-05-08 14:07:06 +08:00
qiang.ouandGitHub bf07c6e2e2 Create LICENSE 2017-05-08 14:00:10 +08:00
ouqiang b2ad507010 完善文档 2017-05-07 11:38:03 +08:00
35 changed files with 914 additions and 86 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
language: go
go:
- 1.7.x
- 1.8.x
- master
script: go test `go list ./... | grep -v vendor`
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 qiang.ou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+22 -8
View File
@@ -6,9 +6,10 @@
## 功能特性
* 支持任务CURD
* crontab时间表达式,精确到
* crontab时间表达式,精确到秒
* 任务执行失败重试设置
* 任务超时设置
* 延时任务
* 任务执行方式
* 调用本机系统命令
* 通过SSH执行远程命令
@@ -21,28 +22,31 @@
![Slack](https://raw.githubusercontent.com/ouqiang/gocron/master/screenshot_slack.png)
### 支持平台
> Windows、Linux、OSX
> Windows、Linux、Mac OS
### 环境要求
> MySQL
## 下载
* [Linux-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-linux-amd64.tar.gz)
* [Mac OS-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-darwin-amd64.tar.gz)
* [Windows-64位](http://opns468ov.bkt.clouddn.com/gocron/gocron-windows-amd64.zip)
## 安装
### 二进制安装
1. [下载](https://pan.baidu.com/s/1pLNRndx#list/path=%2Fgocron), 解压压缩包
1. 解压压缩包
2. `cd 解压目录`
3. 启动
* Windows: `gocron.exe web`
* Linux、OSX: `./gocron web`
* Linux、Mac OS: `./gocron web`
4. 浏览器访问 http://localhost:5920
### 源码安装
1. `go`语言版本1.7+
2. `go get -d https://github.com/ouqiang/gocron`
2. `go get -d github.com/ouqiang/gocron`
3. 编译 `go build`
4. 启动、访问方式同上
5. 生成压缩包(Windows: gocron.zip, 其他平台: gocron.tar.gz
> ./build.sh -p 平台 -a CPU架构 例 ./build.sh -p darwin -a amd64
### 启动可选参数
@@ -54,3 +58,13 @@
* 使用`https`访问保证数据传输安全, 可在web服务器如nginx中配置https,通过反向代理,访问内部的gocron
* 网站访问设置IP白名单
* SSH登录设置IP白名单
## 程序使用的组件
* web框架 [Macaron](http://go-macaron.com/)
* 定时任务调度 [cron](https://github.com/robfig/cron)
* ORM [Xorm](https://github.com/go-xorm/xorm)
* UI框架 [Semantic UI](https://semantic-ui.com/)
* 依赖管理(所有依赖包放入vendor目录) [govendor](https://github.com/kardianos/govendor)
## 贡献
欢迎提交PR
+1 -1
View File
@@ -4,7 +4,7 @@
# 构建应用, 生成压缩包 gocron.zip或gocron.tar.gz
# ./build.sh -p windows -a amd64
# 参数含义
# -p 指定平台(widows|linux|darwin)
# -p 指定平台(windows|linux|darwin)
# -a 指定体系架构(amd64|386), 默认amd64
+34 -3
View File
@@ -71,6 +71,27 @@ func initModule() {
// 初始化定时任务
serviceTask := new(service.Task)
serviceTask.Initialize()
// 初始化延时任务
delayTaskEnabled, err := config.Key("delay.task.enable").Bool()
if err != nil {
return
}
if !delayTaskEnabled {
return
}
delayTaskSlots, err := config.Key("delay.task.slots").Int()
if err != nil {
return
}
delayTaskTick := config.Key("delay.task.tick").String()
tick, err := time.ParseDuration(delayTaskTick)
if err != nil {
return
}
serviceDelayTask := new(service.DelayTask)
serviceDelayTask.Initialize(tick, delayTaskSlots)
}
// 解析端口
@@ -93,12 +114,12 @@ func setEnvironment(ctx *cli.Context) {
}
switch env {
case "prod":
macaron.Env = macaron.PROD
case "test":
macaron.Env = macaron.TEST
case "dev":
macaron.Env = macaron.DEV
default:
macaron.Env = macaron.PROD
}
}
@@ -124,12 +145,22 @@ func shutdown() {
os.Exit(0)
return
}
logger.Info("应用准备退出\n停止任务调度")
logger.Info("应用准备退出")
serviceTask := new(service.Task)
// 停止所有任务调度
logger.Info("停止定时任务调度")
serviceTask.StopAll()
delayTaskEnable, _ := app.Setting.Key("delay.task.enable").Bool()
if delayTaskEnable {
logger.Info("停止延时任务调度")
serviceDelayTask := new(service.DelayTask)
serviceDelayTask.Stop()
}
taskNumInRunning := service.TaskNum.Num()
logger.Infof("正在运行的任务有%d个", taskNumInRunning)
if taskNumInRunning > 0 {
logger.Info("等待所有任务执行完成后退出")
}
for {
if taskNumInRunning <= 0 {
break
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/ouqiang/gocron/cmd"
)
const AppVersion = "0.0.1"
const AppVersion = "0.2"
func main() {
app := cli.NewApp()
+85
View File
@@ -0,0 +1,85 @@
package models
import (
"time"
"github.com/go-xorm/xorm"
)
// 延迟任务
type DelayTask struct {
Id int64 `xorm:"bigint pk autoincr"`
Url string `xorm:"varchar(128) not null"`
Params string `xorm:"varchar(256) not null default '' "`
Delay int `xorm:"mediumint notnull default 0"` // 延迟时间
Status Status `xorm:"tinyint notnull index(u_status_created) default 5"` // 状态 0:执行失败 1:执行中 2:执行成功 5: 待执行
Created time.Time `xorm:"datetime notnull created index(u_status_created)"`
Updated time.Time `xorm:"datetime updated"`
BaseModel `xorm:"-"`
}
func (task *DelayTask) Create() (insertId int64, err error) {
_, err = Db.Insert(task)
if err == nil {
insertId = task.Id
}
return
}
// 获取所有待执行任务
func (task *DelayTask) ActiveList(endTime time.Time) ([]DelayTask, error) {
list := make([]DelayTask, 0)
fields := "id,url,params,delay,created"
err := Db.Where("status = ? AND created <= ?", Waiting, endTime.Format(DefaultTimeFormat)).Cols(fields).Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
return list, err
}
// 获取待执行任务数量
func (task *DelayTask) ActiveNum(endTime time.Time) (int, error) {
count ,err := Db.Where("status = ? AND created <= ?", Waiting, endTime.Format(DefaultTimeFormat)).Count(task)
return int(count), err
}
func (task *DelayTask) List(params CommonMap) ([]DelayTask, error) {
task.parsePageAndPageSize(params)
list := make([]DelayTask, 0)
session := Db.Desc("id")
task.parseWhere(session, params)
err := session.Limit(task.PageSize, task.pageLimitOffset()).Find(&list)
return list, err
}
// 更新任务状态
func (task *DelayTask) UpdateStatus(id int64, status Status) (int64, error) {
return Db.Table(task).Id(id).Update(CommonMap{
"status": status,
})
}
// 解析where
func (task *DelayTask) parseWhere(session *xorm.Session, params CommonMap) {
if len(params) == 0 {
return
}
status, ok := params["Status"]
if ok && status.(int) > -1 {
session.And("status = ?", status)
}
}
// 删除N个月前的日志
func (task *DelayTask) Remove(id int) (int64, error) {
t := time.Now().AddDate(0, -id, 0)
return Db.Where("created <= ?", t.Format(DefaultTimeFormat)).Delete(task)
}
func (task *DelayTask) Total(params CommonMap) (int64, error) {
session := Db.NewSession()
task.parseWhere(session, params)
return session.Count(task)
}
+3 -1
View File
@@ -13,8 +13,9 @@ func (migration *Migration) Exec(dbName string) error {
return errors.New("数据库不存在")
}
setting := new(Setting)
task := new(Task)
tables := []interface{}{
&User{}, &Task{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
&User{}, task, &DelayTask{}, &TaskLog{}, &Host{}, setting,&LoginLog{},
}
for _, table := range tables {
exist, err:= Db.IsTableExist(table)
@@ -30,6 +31,7 @@ func (migration *Migration) Exec(dbName string) error {
}
}
setting.InitBasicField()
task.CreateTestTask()
return nil
}
+1
View File
@@ -26,6 +26,7 @@ const (
Finish Status = 2 // 完成
Cancel Status = 3 // 取消
Background Status = 4 // 后台运行
Waiting Status = 5 // 等待中
)
const (
+28 -2
View File
@@ -4,6 +4,7 @@ import (
"time"
"github.com/ouqiang/gocron/modules/ssh"
"github.com/go-xorm/xorm"
"github.com/ouqiang/gocron/modules/utils"
)
type TaskProtocol int8
@@ -29,9 +30,9 @@ type Task struct {
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
Status Status `xorm:"tinyint notnull default 0"` // 状态 1:正常 0:停止
Created time.Time `xorm:"datetime notnull created"` // 创建时间
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
Status Status `xorm:"tinyint notnull default 1"` // 状态 1:正常 0:停止
BaseModel `xorm:"-"`
}
@@ -58,8 +59,33 @@ func (task *Task) Create() (insertId int, err error) {
return
}
// 新增测试任务
func (task *Task) CreateTestTask() {
// HTTP任务
task.Name = "测试HTTP任务"
task.Protocol = TaskHTTP
task.Spec = "*/30 * * * * *"
// 查询IP地址区域信息
task.Command = "http://ip.taobao.com/service/getIpInfo.php?ip=117.27.140.253"
task.Status = Enabled
task.Create()
// 系统命令
task.Id = 0
task.Name = "测试系统命令任务"
task.Protocol = TaskLocalCommand
task.Spec = "@every 1m"
task.Status = Enabled
if utils.IsWindows() {
task.Command = "dir"
} else {
task.Command = "ls"
}
task.Create()
}
func (task *Task) UpdateBean(id int) (int64, error) {
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,status,notify_status,notify_type,notify_receiver_id").Update(task)
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,notify_status,notify_type,notify_receiver_id").Update(task)
}
// 更新
+19 -2
View File
@@ -72,9 +72,26 @@ func (taskLog *TaskLog) List(params CommonMap) ([]TaskLog, error) {
return list, err
}
// 根据通知ID获取任务ID
func (taskLog *TaskLog) GetTaskIdByNotifyId(notifyId string) (taskId int, err error) {
exist, err := Db.Where("notify_id = ?", notifyId).Get(taskLog)
if !exist || err != nil {
return
}
taskId = taskLog.TaskId
return
}
// 清空表
func (TaskLog *TaskLog) Clear() (int64, error) {
return Db.Where("1=1").Delete(TaskLog);
func (taskLog *TaskLog) Clear() (int64, error) {
return Db.Where("1=1").Delete(taskLog);
}
// 删除N个月前的日志
func (taskLog *TaskLog) Remove(id int) (int64, error) {
t := time.Now().AddDate(0, -id, 0)
return Db.Where("start_time <= ?", t.Format(DefaultTimeFormat)).Delete(taskLog)
}
func (taskLog *TaskLog) Total(params CommonMap) (int64, error) {
+12 -1
View File
@@ -25,7 +25,18 @@ func Get(url string, timeout int) ResponseWrapper {
return request(req, timeout)
}
func PostBody(url string, body string, timeout int) ResponseWrapper {
func PostParams(url string,params string, timeout int) ResponseWrapper {
buf := bytes.NewBufferString(params)
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return createRequestError(err)
}
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
return request(req, timeout)
}
func PostJson(url string, body string, timeout int) ResponseWrapper {
buf := bytes.NewBufferString(body)
req, err := http.NewRequest("POST", url, buf)
if err != nil {
+1 -1
View File
@@ -66,7 +66,7 @@ func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message)
i += 1
time.Sleep(2 * time.Second)
if i < maxTimes {
logger.Error("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
logger.Errorf("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
}
}
}
+2 -2
View File
@@ -43,14 +43,14 @@ func (slack *Slack) send(msg Message, slackUrl string, channel string) {
maxTimes := 3
i := 0
for i < maxTimes {
resp := httpclient.PostBody(slackUrl, formatBody, timeout)
resp := httpclient.PostJson(slackUrl, formatBody, timeout)
if resp.StatusCode == 200 {
break;
}
i += 1
time.Sleep(2 * time.Second)
if i < maxTimes {
logger.Error("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
logger.Errorf("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
}
}
}
+110
View File
@@ -0,0 +1,110 @@
package delaytask
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
"github.com/ouqiang/gocron/modules/utils"
"strings"
"github.com/ouqiang/gocron/service"
"github.com/ouqiang/gocron/modules/logger"
"github.com/Unknwon/paginater"
"fmt"
"github.com/ouqiang/gocron/routers/base"
"html/template"
"github.com/ouqiang/gocron/modules/app"
)
func Index(ctx *macaron.Context) {
delayTaskModel := new(models.DelayTask)
queryParams := parseQueryParams(ctx)
total, err := delayTaskModel.Total(queryParams)
tasks, err := delayTaskModel.List(queryParams)
if err != nil {
logger.Error(err)
}
PageParams := fmt.Sprintf("status=%d&page_size=%d",
queryParams["Status"], queryParams["PageSize"]);
queryParams["PageParams"] = template.URL(PageParams)
p := paginater.New(int(total), queryParams["PageSize"].(int), queryParams["Page"].(int), 5)
ctx.Data["Pagination"] = p
ctx.Data["Title"] = "延时任务列表"
ctx.Data["Tasks"] = tasks
ctx.Data["Params"] = queryParams
ctx.HTML(200, "task/delay_task")
}
func Create(ctx *macaron.Context) string {
url := ctx.QueryTrim("url")
params := ctx.QueryTrim("params")
delay := ctx.QueryInt("delay")
json := utils.JsonResponse{}
delayTaskEnabled, _ := app.Setting.Key("delay.task.enable").Bool()
if !delayTaskEnabled {
return json.CommonFailure("系统未开启延时任务")
}
if url == "" {
return json.CommonFailure("url地址不能为空")
}
lowerUrl := strings.ToLower(url)
if !strings.HasPrefix(lowerUrl, "http") &&
!strings.HasPrefix(lowerUrl, "https") {
return json.CommonFailure("无效的url地址")
}
if len(url) > 128 {
return json.CommonFailure("url长度不能超过128")
}
maxDelay := 1 << 31
if delay <= 0 || delay > maxDelay {
return json.CommonFailure("无效的delay, 取值范围1-(2^31-1)")
}
if len(params) > 256 {
return json.CommonFailure("params长度不能超过256")
}
delayTask := new(models.DelayTask)
delayTask.Url = url
delayTask.Params = params
delayTask.Delay = delay
delayTask.Status = models.Waiting
_, err := delayTask.Create()
if err != nil {
return json.CommonFailure("添加失败", err)
}
logger.Infof("新增延时任务#id-%d#url-%s#params-%s#delay-%d",
delayTask.Id, delayTask.Url, delayTask.Params, delayTask.Delay)
delayTaskService := new(service.DelayTask)
delayTaskService.Add(*delayTask)
return json.Success("添加成功", nil)
}
// 删除N个月前的日志
func Remove(ctx *macaron.Context) string {
month := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
if month < 1 || month > 12 {
return json.CommonFailure("参数取值范围1-12")
}
delayTaskModel := new(models.DelayTask)
_, err := delayTaskModel.Remove(month)
if err != nil {
return json.CommonFailure("删除失败", err)
}
return json.Success("删除成功", nil)
}
// 解析查询参数
func parseQueryParams(ctx *macaron.Context) (models.CommonMap) {
var params models.CommonMap = models.CommonMap{}
status := ctx.QueryInt("status")
if status >=0 {
status -= 1
}
params["Status"] = status
base.ParsePageAndPageSize(ctx, params)
return params
}
+3
View File
@@ -113,6 +113,9 @@ func writeConfig(form InstallForm) error {
"db.charset": "utf8",
"allow_ips" : "",
"app.name": "定时任务管理系统", // 应用名称
"delay.task.enable": "false", // 是否开启延时任务
"delay.task.slots": "3600", // 时间轮槽数量
"delay.task.tick": "1s", // 时间轮每次转动的时间
}
return setting.Write(dbConfig, app.AppConfig)
+8
View File
@@ -105,6 +105,14 @@ func UpdateMailServer(ctx *macaron.Context, form MailServerForm) string {
return utils.JsonResponseByErr(err)
}
func ClearMailServer(ctx *macaron.Context) string {
jsonByte, _ := json.Marshal(MailServerForm{})
settingModel := new(models.Setting)
_, err := settingModel.UpdateMailServer(string(jsonByte))
return utils.JsonResponseByErr(err)
}
func CreateMailUser(ctx *macaron.Context) string {
username := ctx.QueryTrim("username")
email := ctx.QueryTrim("email")
+18 -4
View File
@@ -17,6 +17,7 @@ import (
"github.com/go-macaron/gzip"
"github.com/ouqiang/gocron/routers/manage"
"github.com/ouqiang/gocron/routers/loginlog"
"github.com/ouqiang/gocron/routers/delaytask"
)
// 静态文件目录
@@ -43,7 +44,7 @@ func Register(m *macaron.Macaron) {
m.Post("/editPassword", user.UpdatePassword)
})
// 任务
// 定时任务
m.Group("/task", func() {
m.Get("/create", task.Create)
m.Post("/store", binding.Bind(task.TaskForm{}), task.Store)
@@ -57,6 +58,11 @@ func Register(m *macaron.Macaron) {
m.Get("/run/:id", task.Run)
})
// 延时任务
m.Group("/delaytask", func() {
m.Get("", delaytask.Index)
})
// 主机
m.Group("/host", func() {
m.Get("/create", host.Create)
@@ -80,6 +86,7 @@ func Register(m *macaron.Macaron) {
m.Get("/", manage.Mail)
m.Get("/edit", manage.EditMail)
m.Post("/server", binding.Bind(manage.MailServerForm{}), manage.UpdateMailServer)
m.Post("/server/clear", manage.ClearMailServer)
m.Post("/user", manage.CreateMailUser)
m.Post("/user/remove/:id", manage.RemoveMailUser)
})
@@ -88,7 +95,10 @@ func Register(m *macaron.Macaron) {
// API
m.Group("/api/v1", func() {
m.Get("/tasklog/update-status", tasklog.UpdateStatus)
m.Route("/tasklog/update-status", "GET,POST", tasklog.UpdateStatus)
m.Post("/tasklog/remove/:id", tasklog.Remove)
m.Post("/delaytask/push", delaytask.Create)
m.Post("/delaytask/log/remove/:id", delaytask.Remove)
});
// 404错误
@@ -149,7 +159,9 @@ func RegisterMiddleware(m *macaron.Macaron) {
})
}
// 系统未安装,重定向到安装页面
// region 自定义中间件
/** 系统未安装,重定向到安装页面 **/
func checkAppInstall(m *macaron.Macaron) {
m.Use(func(ctx *macaron.Context) {
installUrl := "/install"
@@ -195,7 +207,9 @@ func userAuth(ctx *macaron.Context, sess session.Store) {
}
}
// 设置共享数据
// endregion
/** 设置共享数据 **/
func setShareData(ctx *macaron.Context, sess session.Store) {
ctx.Data["URI"] = ctx.Req.URL.Path
urlPath := strings.TrimPrefix(ctx.Req.URL.Path, "/")
+21 -27
View File
@@ -13,6 +13,7 @@ import (
"html/template"
"github.com/ouqiang/gocron/routers/base"
"github.com/go-macaron/binding"
"strings"
)
type TaskForm struct {
@@ -26,7 +27,6 @@ type TaskForm struct {
RetryTimes int8
HostId int16
Remark string
Status models.Status `binding:"In(1,2)"`
NotifyStatus int8 `binding:In(1,2,3)`
NotifyType int8 `binding:In(1,2)`
NotifyReceiverId string
@@ -82,11 +82,6 @@ func Create(ctx *macaron.Context) {
// 编辑页面
func Edit(ctx *macaron.Context) {
id := ctx.ParamsInt(":id")
hostModel := new(models.Host)
hosts, err := hostModel.List(models.CommonMap{})
if err != nil || len(hosts) == 0 {
logger.Error(err)
}
taskModel := new(models.Task)
task, err := taskModel.Detail(id)
if err != nil || task.Id != id {
@@ -95,11 +90,7 @@ func Edit(ctx *macaron.Context) {
}
ctx.Data["Task"] = task
ctx.Data["Title"] = "编辑"
ctx.Data["Hosts"] = hosts
if len(hosts) > 0 {
ctx.Data["FirstHostName"] = hosts[0].Name
ctx.Data["FirstHostId"] = hosts[0].Id
}
setHostsToTemplate(ctx)
ctx.HTML(200, "task/task_form")
}
@@ -134,12 +125,8 @@ func Store(ctx *macaron.Context, form TaskForm) string {
taskModel.Command = form.Command
taskModel.Timeout = form.Timeout
taskModel.Remark = form.Remark
taskModel.Status = form.Status
taskModel.Multi = form.Multi
taskModel.RetryTimes = form.RetryTimes
if taskModel.Status != models.Enabled {
taskModel.Status = models.Disabled
}
if taskModel.Multi != 1 {
taskModel.Multi = 0
}
@@ -148,14 +135,30 @@ func Store(ctx *macaron.Context, form TaskForm) string {
taskModel.NotifyReceiverId = form.NotifyReceiverId
taskModel.Spec = form.Spec
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
return json.CommonFailure("请至少选择一个接收者", err)
return json.CommonFailure("请至少选择一个接收者")
}
if taskModel.Protocol == models.TaskHTTP && taskModel.Timeout == -1 {
return json.CommonFailure("HTTP任务不支持后台运行", err)
if taskModel.Protocol == models.TaskHTTP {
command := strings.ToLower(taskModel.Command)
if !strings.HasPrefix(command, "http://") && !strings.HasPrefix(command, "https://") {
return json.CommonFailure("请输入正确的URL地址")
}
if taskModel.Timeout == -1 {
return json.CommonFailure("HTTP任务不支持后台运行")
}
if taskModel.Timeout > 300 {
return json.CommonFailure("HTTP任务超时时间不能超过300秒")
}
}
if taskModel.RetryTimes > 10 || taskModel.RetryTimes < 0 {
return json.CommonFailure("任务重试次数取值0-10")
}
if taskModel.Protocol != models.TaskSSH {
taskModel.HostId = 0
}
if id == 0 {
id, err = taskModel.Create()
} else {
@@ -165,11 +168,6 @@ func Store(ctx *macaron.Context, form TaskForm) string {
return json.CommonFailure("保存失败", err)
}
// 任务处于激活状态,加入调度管理
if (taskModel.Status == models.Enabled) {
addTaskToTimer(id)
}
return json.Success("保存成功", nil)
}
@@ -273,8 +271,4 @@ func setHostsToTemplate(ctx *macaron.Context) {
logger.Error(err)
}
ctx.Data["Hosts"] = hosts
if len(hosts) > 0 {
ctx.Data["FirstHostName"] = hosts[0].Name
ctx.Data["FirstHostId"] = hosts[0].Id
}
}
+41 -3
View File
@@ -1,5 +1,7 @@
package tasklog
// 任务日志
import (
"gopkg.in/macaron.v1"
"github.com/ouqiang/gocron/models"
@@ -9,11 +11,10 @@ import (
"fmt"
"html/template"
"github.com/ouqiang/gocron/routers/base"
"github.com/ouqiang/gocron/service"
"errors"
)
// @author qiang.ou<qingqianludao@gmail.com>
// @date 2017/4/7-21:18
func Index(ctx *macaron.Context) {
logModel := new(models.TaskLog)
queryParams := parseQueryParams(ctx)
@@ -49,12 +50,29 @@ func Clear(ctx *macaron.Context) string {
return json.Success(utils.SuccessContent, nil)
}
// 删除N个月前的日志
func Remove(ctx *macaron.Context) string {
month := ctx.ParamsInt(":id")
json := utils.JsonResponse{}
if month < 1 || month > 12 {
return json.CommonFailure("参数取值范围1-12")
}
taskLogModel := new(models.TaskLog)
_, err := taskLogModel.Remove(month)
if err != nil {
return json.CommonFailure("删除失败", err)
}
return json.Success("删除成功", nil)
}
// 更新任务状态
func UpdateStatus(ctx *macaron.Context) string {
id := ctx.QueryTrim("id")
status := ctx.QueryInt("status")
result := ctx.QueryTrim("result")
json := utils.JsonResponse{}
if id == "" {
return json.CommonFailure("任务ID不能为空")
}
@@ -70,6 +88,26 @@ func UpdateStatus(ctx *macaron.Context) string {
return json.CommonFailure("更新任务状态失败")
}
// 发送通知
taskId, err := taskLogModel.GetTaskIdByNotifyId(id)
if err != nil || taskId <= 0 {
logger.Error("异步任务回调#根据notify-id获取taskId失败", err)
return json.Success("success", nil)
}
taskModel := new(models.Task)
task, err := taskModel.Detail(taskId)
if err != nil || task.Id <= 0 {
logger.Error("异步任务回调#根据获取任务详情失败", err)
return json.Success("success", nil)
}
taskResult := service.TaskResult{}
taskResult.Result = result
if status == 0 {
taskResult.Err = errors.New("error")
}
service.SendNotification(task, taskResult)
return json.Success("success", nil)
}
+110
View File
@@ -0,0 +1,110 @@
package service
import (
"github.com/ouqiang/gocron/models"
"time"
"github.com/ouqiang/gocron/modules/logger"
"math"
"github.com/ouqiang/gocron/modules/httpclient"
"strings"
"github.com/ouqiang/timewheel"
)
var tw *timewheel.TimeWheel
type DelayTask struct {}
// 从数据库中取出所有延迟任务
func (task *DelayTask) Initialize(tick time.Duration, slots int) {
tw = timewheel.New(tick, slots)
tw.Start()
taskModel := new(models.DelayTask)
currentTime := time.Now()
taskNum, err := taskModel.ActiveNum(currentTime)
if err != nil {
logger.Error("延迟任务初始化#获取待执行的任务失败", err)
return
}
if taskNum == 0 {
logger.Debugf("延迟任务初始化#待执行的任务数量为0")
return
}
pageSize := 100
totalPage := int( math.Ceil(float64(taskNum) / float64(pageSize)) )
logger.Infof("延迟任务初始化#待执行的任务数量-%d#共%d页#每页取%d条", taskNum, totalPage, pageSize)
taskModel.PageSize = pageSize
for page := 1; page <= totalPage; page++ {
taskModel.Page = page
logger.Debugf("延迟任务初始化#取出任务列表#第%d页", page)
taskList, err := taskModel.ActiveList(currentTime)
if err != nil {
logger.Error("延迟任务初始化#获取任务列表失败", err)
}
task.BatchAdd(taskList)
}
logger.Info("延迟任务初始化完成")
}
// 批量添加任务
func (task *DelayTask) BatchAdd(taskList []models.DelayTask) {
for _, item := range(taskList) {
task.Add(item)
}
}
// 添加任务
func (task *DelayTask) Add(taskModel models.DelayTask) {
currentTimestamp := time.Now().Unix()
execTimestamp := taskModel.Created.Unix() + int64(taskModel.Delay)
// 时间过期, 立即执行任务
if execTimestamp <= currentTimestamp {
go task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
return
}
delay := execTimestamp - currentTimestamp
tw.Add(time.Duration(delay) * time.Second, func() {
task.Run(taskModel.Id, taskModel.Url, taskModel.Params)
})
}
// 运行任务
func (task *DelayTask) Run(id int64, url, params string) {
taskModel := new(models.DelayTask)
_, err := taskModel.UpdateStatus(id, models.Running)
if err != nil {
logger.Error("延迟任务开始执行#更新任务状态失败", err)
return
}
timeout := 300
tryTimes := 3
success := false
logger.Infof("延迟任务开始执行#id-%d#url-%s#params-%s", id, url, params)
for i := 0; i < tryTimes; {
response := httpclient.PostParams(url, params, timeout)
if response.StatusCode == 200 && strings.TrimSpace(response.Body) == "success"{
success = true
break;
}
i++
if i < tryTimes {
logger.Errorf("延迟任务执行失败#重试第%d次#任务Id-%d#HTTP状态码-%d#HTTP-BODY-%s",
i,id,response.StatusCode,response.Body)
time.Sleep(30 * time.Second)
}
}
logger.Infof("延迟任务执行完成#id-%d", id)
var status models.Status
if success {
status = models.Finish
} else {
status = models.Failure
}
_ ,err = taskModel.UpdateStatus(id, status)
if err != nil {
logger.Error("延迟任务执行完成#更新任务状态失败", err)
}
}
func (task *DelayTask) Stop() {
tw.Stop()
}
+13 -2
View File
@@ -173,7 +173,13 @@ func (h *LocalCommandHandler) runOnUnix(taskModel models.TaskHost) (string, erro
// HTTP任务
type HTTPHandler struct{}
// http任务执行时间不超过300秒
const HttpExecTimeout = 300
func (h *HTTPHandler) Run(taskModel models.TaskHost) (result string, err error) {
if taskModel.Timeout <= 0 || taskModel.Timeout > HttpExecTimeout {
taskModel.Timeout = HttpExecTimeout
}
resp := httpclient.Get(taskModel.Command, taskModel.Timeout)
// 返回状态码非200,均为失败
if resp.StatusCode != 200 {
@@ -274,7 +280,9 @@ func createJob(taskModel models.TaskHost) cron.FuncJob {
if taskLogId <= 0 {
return
}
logger.Infof("开始执行任务#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
taskResult := execJob(handler, taskModel)
logger.Infof("任务完成#%s#命令-%s", taskModel.Task.Name, taskModel.Command)
afterExecJob(taskModel, taskResult, taskLogId)
}
@@ -338,11 +346,11 @@ func afterExecJob(taskModel models.TaskHost, taskResult TaskResult, taskLogId in
return
}
sendNotification(taskModel, taskResult)
SendNotification(taskModel, taskResult)
}
// 发送任务结果通知
func sendNotification(taskModel models.TaskHost, taskResult TaskResult) {
func SendNotification(taskModel models.TaskHost, taskResult TaskResult) {
var statusName string
// 未开启通知
if taskModel.NotifyStatus == 0 {
@@ -352,6 +360,9 @@ func sendNotification(taskModel models.TaskHost, taskResult TaskResult) {
// 执行失败才发送通知
return
}
if taskModel.NotifyReceiverId == "" {
return
}
if taskResult.Err != nil {
statusName = "失败"
} else {
+1
View File
@@ -68,6 +68,7 @@
</div>
<div class="ui primary submit button">保存</div>
<a class="ui button" onclick="location.href='/task';">取消</a>
</form>
</div>
</div>
+9 -1
View File
@@ -90,9 +90,17 @@
$('.ui.form').form(
{
onSuccess: function(event, fields) {
swal({
title: '',
text: "系统安装中.......",
type: 'info',
showConfirmButton: false
});
util.post('/install/store', fields, function(code, message) {
swal('安装成功');
location.href = "/";
setTimeout(function() {
location.href = "/";
}, 2000)
});
return false;
},
+11 -2
View File
@@ -16,7 +16,7 @@
<div class="four fields">
<div class="field">
<label>
主机名
SMTP服务器
</label>
<div class="ui small input">
<input type="text" name="host" value="{{{.Mail.Host}}}">
@@ -48,6 +48,9 @@
</div>
</div>
<button class="ui primary button">保存</button>
{{{if .Mail.Host}}}
<a class="ui green button" onclick="clearMailServer()">删除</a>
{{{end}}}
<br><br><br>
<div>
<div class="content">邮箱用户</div><p></p>
@@ -98,7 +101,7 @@
util.post('/manage/mail/server',
fields,
function(code, message) {
util.alertSuccess();
location.reload();
}
);
return false;
@@ -188,5 +191,11 @@
location.reload();
});
}
function clearMailServer() {
util.post('/manage/mail/server/clear', {}, function(code, message) {
location.reload();
});
}
</script>
{{{ template "common/footer" . }}}
+84
View File
@@ -0,0 +1,84 @@
{{{ template "common/header" . }}}
<style type="text/css">
pre {
white-space: pre-wrap;
word-wrap: break-word;
padding:10px;
background-color: #4C4C4C;
color: white;
}
</style>
<div class="ui grid">
<!--the vertical menu-->
{{{ template "task/menu" . }}}
<div class="twelve wide column">
<div class="pageHeader">
<div class="segment">
<h3 class="ui dividing header">
<div class="content">
</div>
</h3>
</div>
</div>
<form class="ui form">
<div class="fields search">
<div class="field">
<select name="status">
<option value="0">任务状态</option>
<option value="1" {{{if eq .Params.Status 0}}}selected{{{end}}} >失败</option>
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
<option value="6" {{{if eq .Params.Status 5}}}selected{{{end}}}>待执行</option>
</select>
</div>
<div class="field">
<button class="ui linkedin submit button">搜索</button>
</div>
</div>
</form>
<table class="ui pink table">
<thead>
<tr>
<th>任务ID</th>
<th>URL</th>
<th>参数</th>
<th>延迟时间</th>
<th>创建时间</th>
<th>完成时间</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{{{range $i, $v := .Tasks}}}
<tr>
<td>{{{.Id}}}</td>
<td>{{{.Url}}}</td>
<td>{{{.Params}}}</td>
<td>{{{.Delay}}}</td>
<td>{{{.Created.Format "2006-01-02 15:04:05" }}}</td>
<td>
{{{if or (eq .Status 0) (eq .Status 2) }}}
{{{.Updated.Format "2006-01-02 15:04:05" }}}
{{{end}}}
</td>
<td>
{{{if eq .Status 2}}}
成功
{{{else if eq .Status 1}}}
<span style="color:green">执行中</span>
{{{else if eq .Status 0}}}
<span style="color:red">失败</span>
{{{else if eq .Status 5}}}
<span style="color:#43A102">待执行</span>
{{{end}}}
</td>
</tr>
{{{end}}}
</tbody>
</table>
{{{ template "common/pagination" .}}}
</div>
</div>
{{{ template "common/footer" . }}}
+1 -1
View File
@@ -62,7 +62,7 @@
<p>cron表达式 {{{.Spec}}}</p>
<p>执行方式: {{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else if eq .Protocol 3}}}本地命令{{{end}}}</p>
<p class="sensorStatus">命令{{{.Command}}}</p>
<p class="sensorStatus">超时时间{{{if eq .Timeout -1}}}后台运行{{{else if eq .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</p>
<p class="sensorStatus">超时时间{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</p>
<p>重试次数: {{{.RetryTimes}}}</p>
<p class="sensorStatus">是否允许多实例运行{{{if gt .Multi 0}}}{{{else}}}{{{end}}}</p>
{{{if eq .Protocol 2}}}
+3 -3
View File
@@ -42,7 +42,7 @@
<option value="2" {{{if eq .Params.Status 1}}}selected{{{end}}}>执行中</option>
<option value="3" {{{if eq .Params.Status 2}}}selected{{{end}}}>成功</option>
<option value="4" {{{if eq .Params.Status 3}}}selected{{{end}}}>取消</option>
<option value="5" {{{if eq .Params.Status 3}}}selected{{{end}}}>后台运行</option>
<option value="5" {{{if eq .Params.Status 4}}}selected{{{end}}}>后台运行</option>
</select>
</div>
<div class="field">
@@ -72,7 +72,7 @@
<td>{{{.Name}}}</td>
<td>{{{.Spec}}}</td>
<td>{{{if eq .Protocol 1}}} HTTP {{{else if eq .Protocol 2}}} SSH {{{else}}} 系统命令 {{{end}}}</td>
<td>{{{if eq .Timeout -1}}}后台运行{{{else if eq .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</td>
<td>{{{if eq .Timeout -1}}}后台运行{{{else if gt .Timeout 0}}}{{{.Timeout}}}{{{else}}}不限制{{{end}}}</td>
<td>{{{.RetryTimes}}}</td>
<td>{{{.Hostname}}}</td>
<td>
@@ -98,7 +98,7 @@
{{{end}}}
</td>
<td>
{{{if and (ne .Status 1) (ne .Status 3)}}}
{{{if or (eq .Status 2) (eq .Status 0)}}}
<button class="ui small primary button"
onclick="showResult('{{{.Name}}}', '{{{.Command}}}', '{{{.Result}}}')"
>查看结果
+23 -19
View File
@@ -56,14 +56,14 @@
{{{range $i, $v := .Hosts}}}
<option value="{{{.Id}}}" {{{if $.Task}}}{{{if eq $.Task.HostId .Id }}} selected {{{end}}} {{{end}}}>{{{.Alias}}}-{{{.Name}}}</option>
{{{end}}}
</select> &nbsp; <a class="ui blue button" href="/host/create">添加主机</a>
</select> &nbsp; <a class="ui blue button" href="/host/create" target="_blank">添加主机</a>
</div>
</div>
<div class="two fields">
<div class="field">
<label>命令</label>
<textarea rows="5" name="command">{{{.Task.Command}}}</textarea>
<textarea rows="5" name="command" placeholder="请输入系统命令" id="command">{{{.Task.Command}}}</textarea>
</div>
</div>
<div class="six fields">
@@ -81,27 +81,12 @@
<div class="three fields">
<div class="field">
<label>允许多实例同时运行</label>
<div class="ui blue message">
前次任务未执行完成本次任务是否执行
</div>
<select name="multi">
<option value="1"{{{if .Task}}} {{{if eq .Task.Multi 1}}}selected{{{end}}} {{{end}}}></option>
<option value="2" {{{if .Task}}} {{{if eq .Task.Multi 0}}}selected{{{end}}} {{{end}}}></option>
</select>
</div>
</div>
<div class="three fields">
<div class="field">
<label>任务状态</label>
<div class="ui blue message">
任务添加成功后是否立即调度
</div>
<select name="status">
<option value="2"{{{if .Task}}} {{{if eq .Task.Status 0}}}selected{{{end}}} {{{end}}}>停止</option>
<option value="1" {{{if .Task}}} {{{if eq .Task.Status 1}}}selected{{{end}}} {{{end}}}>激活</option>
</select>
</div>
</div>
<div class="three fields">
<div class="field">
<label>任务通知</label>
@@ -129,7 +114,7 @@
<textarea rows="5" name="remark">{{{.Task.Remark}}}</textarea>
</div>
</div>
<div class="ui primary submit button">保存</div>
<div class="ui primary submit button">保存</div> <a class="ui button" onclick="location.href='/task';">取消</a>
</form>
</div>
</div>
@@ -162,14 +147,18 @@
<script type="text/javascript">
$(function() {
changeCommandPlaceholder();
changeProtocol();
showNotify();
});
$('#protocol').change(function() {
changeProtocol();
changeCommandPlaceholder();
changeProtocol();
});
$('#task-status').change(function() {
var selected = $(this).val();
if (selected == 1) {
@@ -185,6 +174,21 @@
changeNotify();
});
function changeCommandPlaceholder() {
var selectedId = $('#protocol').val();
switch (selectedId) {
case '1':
$('#command').attr('placeholder', '请输入URL地址');
break;
case '2':
$('#command').attr('placeholder', '请输入shell命令');
break;
case '3':
$('#command').attr('placeholder', '请输入系统命令');
break;
}
}
function showNotify() {
var notifyStatus = {{{.Task.NotifyStatus}}};
if (notifyStatus > 0) {
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# set -x -u
# 上传二进制包到七牛
if [[ -z $QINIU_ACCESS_KEY || -z $QINIU_SECRET_KEY || -z $QINIU_URL ]];then
echo 'QINIU_ACCESS_KEY | QINIU_SECRET_KEY | QINIU_URL is need'
exit 1
fi
# 打包
for i in linux darwin windows
do
./build.sh -p $i
if [[ ! $? ]];then
break
fi
done
# 身份认证
qrsctl login $QINIU_ACCESS_KEY $QINIU_SECRET_KEY
# 上传
for i in `ls gocron*.gz gocron*.zip`
do
# 上传文件 qrsctl put bucket key srcFile
KEY=gocron/$i
qrsctl put github $KEY $i
if [[ ! $? ]];then
break
fi
echo "刷新七牛CDN-" $QINIU_URL/$KEY
qrsctl cdn/refresh $QINIU_URL/$KEY
rm $i
done
echo '打包并上传成功'
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 qiang.ou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+40
View File
@@ -0,0 +1,40 @@
# timewheel
Golang实现的时间轮
![时间轮](https://raw.githubusercontent.com/ouqiang/timewheel/master/timewheel.jpg)
# 安装
```shell
go get -u github.com/ouqiang/timewheel
```
# 使用
```go
package main
import (
"github.com/ouqiang/timewheel"
"time"
)
func main() {
// tick刻度为1秒, 3600个槽
tw := timewheel.New(1 * time.Second, 3600)
tw.Start()
tw.Add(5 * time.Second, func() {
// do something
})
tw.Add(10 * time.Minute, func() {
// do something
})
tw.Add(35 * time.Hour, func() {
// do something
})
// 停止
tw.Stop()
}
```
+124
View File
@@ -0,0 +1,124 @@
package timewheel
import (
"time"
"container/list"
)
// @author qiang.ou<qingqianludao@gmail.com>
type TimeWheel struct {
interval time.Duration
ticker *time.Ticker
slots []*list.List
currentPos int
slotNum int
taskChannel chan Task
stopChannel chan bool
}
type Task struct {
delay time.Duration
circle int
job Job
}
type Job func()
func New(interval time.Duration, slotNum int) *TimeWheel {
if interval <= 0 || slotNum <= 0 {
return nil
}
tw := &TimeWheel{
interval: interval,
slots: make([]*list.List, slotNum),
currentPos: 0,
slotNum: slotNum,
taskChannel: make(chan Task),
stopChannel: make(chan bool),
}
tw.initSlots()
return tw
}
func (tw *TimeWheel) initSlots() {
for i := 0; i < tw.slotNum; i++ {
tw.slots[i] = list.New()
}
}
func (tw *TimeWheel) Start() {
tw.ticker = time.NewTicker(tw.interval)
go tw.start()
}
func (tw *TimeWheel) Add(delay time.Duration, job Job) {
if delay < 0 || job == nil {
return
}
tw.taskChannel <- Task{delay:delay, job: job}
}
func (tw *TimeWheel) Stop() {
tw.stopChannel <- true
}
func (tw *TimeWheel) start() {
for {
select {
case <- tw.ticker.C:
tw.tickHandler()
case task := <- tw.taskChannel:
tw.addTask(&task)
case <- tw.stopChannel:
tw.ticker.Stop()
return
}
}
}
func (tw *TimeWheel) tickHandler() {
l := tw.slots[tw.currentPos]
tw.scanAndRunTask(l)
if tw.currentPos == tw.slotNum - 1 {
tw.currentPos = 0
} else {
tw.currentPos++
}
}
func (tw *TimeWheel) scanAndRunTask(l *list.List) {
for e := l.Front(); e != nil; {
task := e.Value.(*Task)
if task.circle > 0 {
task.circle--
e = e.Next()
continue
}
go task.job()
next := e.Next()
l.Remove(e)
e = next
}
}
func (tw *TimeWheel) addTask(task *Task) {
pos, circle := tw.getPositionAndCircle(task.delay)
task.circle = circle
tw.slots[pos].PushBack(task)
}
func (tw *TimeWheel) getPositionAndCircle(d time.Duration) (pos int, circle int) {
delaySeconds := int(d.Seconds())
intervalSeconds := int(tw.interval.Seconds())
circle = int(delaySeconds / intervalSeconds / tw.slotNum)
pos = int(tw.currentPos + delaySeconds / intervalSeconds) % tw.slotNum
return
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+6
View File
@@ -132,6 +132,12 @@
"revision": "09cded8978dc9e80714c4d85b0322337b0a1e5e0",
"revisionTime": "2016-03-02T07:53:16Z"
},
{
"checksumSHA1": "kIFW+u9fHefC8sWE4W9pYIfJv5k=",
"path": "github.com/ouqiang/timewheel",
"revision": "c28ec761087c32fd75ad7514db2a4988d5c872d9",
"revisionTime": "2017-05-14T12:16:09Z"
},
{
"checksumSHA1": "1keN4Q9F8uHk/Gt5YXG0oCEecKM=",
"path": "github.com/urfave/cli",