Merge pull request #109 from shunfei/feature/time-at

Feature/time at
pull/116/head
QLeelulu 2018-10-18 11:06:13 +08:00 committed by GitHub
commit f7de1efe85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 221 additions and 42 deletions

21
job.go
View File

@ -411,6 +411,26 @@ func (j *Job) String() string {
return string(data)
}
// GetNextRunTime return the job's next run time by now,
// will return zero time if job will not run.
func (j *Job) GetNextRunTime() time.Time {
nextTime := time.Time{}
if len(j.Rules) < 1 {
return nextTime
}
for i, r := range j.Rules {
sch, err := cron.Parse(r.Timer)
if err != nil {
return nextTime
}
t := sch.Next(time.Now())
if i == 0 || t.UnixNano() < nextTime.UnixNano() {
nextTime = t
}
}
return nextTime
}
// Run 执行任务
func (j *Job) Run() bool {
var (
@ -738,6 +758,5 @@ func (j *Job) CreateCmdAttr() (*syscall.SysProcAttr, error) {
}
}
return sysProcAttr, nil
}

34
node/cron/at.go Normal file
View File

@ -0,0 +1,34 @@
package cron
import (
"sort"
"time"
)
// TimeListSchedule will run at the specify giving time.
type TimeListSchedule struct {
timeList []time.Time
}
// At returns a crontab Schedule that activates every specify time.
func At(tl []time.Time) *TimeListSchedule {
sort.Slice(tl, func(i, j int) bool { return tl[i].Unix() < tl[j].Unix() })
return &TimeListSchedule{
timeList: tl,
}
}
// Next returns the next time this should be run.
// This rounds so that the next activation time will be on the second.
func (schedule *TimeListSchedule) Next(t time.Time) time.Time {
cur := 0
for cur < len(schedule.timeList) {
nextt := schedule.timeList[cur]
cur++
if nextt.UnixNano() <= t.UnixNano() {
continue
}
return nextt
}
return time.Time{}
}

89
node/cron/at_test.go Normal file
View File

@ -0,0 +1,89 @@
package cron
import (
"testing"
"time"
)
func TestTimeListNext(t *testing.T) {
tests := []struct {
startTime string
times []string
expected []string
}{
// Simple cases
{
"2018-09-01 08:01:02",
[]string{"2018-09-01 10:01:02"},
[]string{"2018-09-01 10:01:02"},
},
// sort list
{
"2018-09-01 08:01:02",
[]string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"},
[]string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"},
},
// sort list with middle start time
{
"2018-09-01 10:11:02",
[]string{"2018-09-01 10:01:02", "2018-09-02 10:01:02"},
[]string{"2018-09-02 10:01:02"},
},
// unsorted list
{
"2018-07-01 08:01:02",
[]string{"2018-09-01 10:01:00", "2018-08-01 10:00:00", "2018-09-01 10:00:00", "2018-08-02 10:01:02"},
[]string{"2018-08-01 10:00:00", "2018-08-02 10:01:02", "2018-09-01 10:00:00", "2018-09-01 10:01:00"},
},
// unsorted list with middle start time
{
"2018-08-03 12:00:00",
[]string{"2018-09-01 10:01:00", "2018-08-01 10:00:00", "2018-09-01 10:00:00", "2018-08-02 10:01:02"},
[]string{"2018-09-01 10:00:00", "2018-09-01 10:01:00"},
},
}
for _, c := range tests {
tls := At(getAtTimes(c.times))
nextTime := getAtTime(c.startTime)
for _, trun := range c.expected {
actual := tls.Next(nextTime)
expected := getAtTime(trun)
if actual != expected {
t.Errorf("%s, \"%s\": (expected) %v != %v (actual)",
c.startTime, c.times, expected, actual)
}
nextTime = actual
}
if actual := tls.Next(nextTime); !actual.IsZero() {
t.Errorf("%s, \"%s\": next time should be zero, but got %v (actual)",
c.startTime, c.times, actual)
}
}
}
func getAtTime(value string) time.Time {
if value == "" {
panic("time string is empty")
}
t, err := time.Parse("2006-01-02 15:04:05", value)
if err != nil {
panic(err)
}
return t
}
func getAtTimes(values []string) []time.Time {
tl := []time.Time{}
for _, v := range values {
tl = append(tl, getAtTime(v))
}
return tl
}

View File

@ -373,5 +373,21 @@ func parseDescriptor(descriptor string) (Schedule, error) {
return Every(duration), nil
}
const at = "@at "
if strings.HasPrefix(descriptor, at) {
tss := strings.Split(descriptor[len(at):], ",")
atls := make([]time.Time, 0, len(tss))
for _, ts := range tss {
ts = strings.TrimSpace(ts)
att, err := time.ParseInLocation("2006-01-02 15:04:05", ts, time.Local)
if err != nil {
return nil, fmt.Errorf("Failed to parse time %s: %s", descriptor, err)
}
atls = append(atls, att)
}
return At(atls), nil
}
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
}

View File

@ -219,6 +219,7 @@ func (j *Job) GetList(ctx *Context) {
type jobStatus struct {
*cronsun.Job
LatestStatus *cronsun.JobLatestLog `json:"latestStatus"`
NextRunTime string `json:"nextRunTime"`
}
resp, err := cronsun.DefalutClient.Get(prefix, clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
@ -263,6 +264,12 @@ func (j *Job) GetList(ctx *Context) {
} else {
for i := range jobList {
jobList[i].LatestStatus = m[jobList[i].ID]
nt := jobList[i].GetNextRunTime()
if nt.IsZero() {
jobList[i].NextRunTime = "NO!!"
} else {
jobList[i].NextRunTime = nt.Format("2006-01-02 15:04:05")
}
}
}

View File

@ -148,8 +148,8 @@ func (s *embeddedFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
fp += s.IndexFile
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "0")
// w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
// w.Header().Set("Expires", "0")
b, err = Asset(fp)
if err == nil {

File diff suppressed because one or more lines are too long

View File

@ -61,7 +61,7 @@
<div id="app">
<div id="initloader"></div>
</div>
<script src="build.js?v=8fbb71c"></script>
<script src="build.js?v=1d90ef8"></script>
</body>
</html>

View File

@ -35,7 +35,7 @@
<tr>
<th class="collapsing center aligned">{{$L('operation')}}</th>
<th class="collapsing center aligned">{{$L('status')}}</th>
<th width="200px" class="center aligned">{{$L('group')}}</th>
<th class="center aligned">{{$L('group')}}</th>
<th class="center aligned">{{$L('user')}}</th>
<th class="center aligned">{{$L('name')}}</th>
<th class="center aligned">{{$L('latest executed')}}</th>
@ -66,6 +66,8 @@
<td>
<span v-if="!job.latestStatus">-</span>
<span v-else>{{formatLatest(job.latestStatus)}}</span>
<br/>
<span>{{formatNextRunTime(job.nextRunTime)}}</span>
</td>
<td :class="{error: job.latestStatus && !job.latestStatus.success}">
<span v-if="!job.latestStatus">-</span>
@ -186,6 +188,10 @@ export default {
return this.$L('on {node} took {times}, {begin ~ end}', this.$store.getters.hostshowsWithoutTip(latest.node), formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
},
formatNextRunTime: function(nextRunTime){
return this.$L('next schedule: {nextTime}', nextRunTime);
},
showExecuteJobModal: function(jobName, jobGroup, jobId){
this.$refs.executeJobModal.show(jobName, jobGroup, jobId);
},

View File

@ -5,7 +5,7 @@
<div class="field">
<div class="ui icon input">
<input type="text" v-bind:value="rule.timer" v-on:input="change('timer', $event.target.value)" :placeholder="$L('0 * * * * *, rules see the 「?」on the right')"/>
<i ref="ruletip" class="large help circle link icon" data-position="top right" :data-content="$L('<sec> <min> <hr> <day> <month> <week>, rules is same with Cron')" data-variation="wide"></i>
<i ref="ruletip" class="large help circle link icon" data-position="top right" :data-html="$L('<sec> <min> <hr> <day> <month> <week>, rules is same with Cron')" data-variation="wide"></i>
</div>
</div>
<div class="field">

View File

@ -89,7 +89,7 @@ export default {
begin: '',
end: '',
latest: false,
failedOnly: '',
failedOnly: false,
list: [],
total: 0,
page: 1
@ -101,8 +101,8 @@ export default {
this.fetchList(this.buildQuery());
var vm = this;
$(this.$refs.latest).checkbox({'onChange': ()=>{vm.latest = !vm.latest}});
$(this.$refs.failedOnly).checkbox({'onChange': ()=>{vm.failedOnly = !vm.failedOnly}});
$(this.$refs.latest).checkbox();
$(this.$refs.failedOnly).checkbox();
},
watch: {
@ -121,8 +121,8 @@ export default {
this.begin = this.$route.query.begin || '';
this.end = this.$route.query.end || '';
this.page = this.$route.query.page || 1;
this.latest = this.$route.query.latest == 'true' ? true : false;
this.failedOnly = this.$route.query.failedOnly ? true : false;
this.latest = this.$route.query.latest === 'true' || this.$route.query.latest === true;
this.failedOnly = this.$route.query.failedOnly === 'true' || this.$route.query.failedOnly === true;
},
fetchList(query){

View File

@ -75,6 +75,7 @@ var language = {
'all groups': 'All groups',
'all nodes': 'All nodes',
'on {node} took {times}, {begin ~ end}': 'On {0} took {1}, {2}',
'next schedule: {nextTime}': 'Next schedule: {0}',
'create job': 'Create job',
'update job': 'Update job',
'output': 'Output',
@ -106,7 +107,10 @@ var language = {
'timeout(in seconds, 0 for no limits)': 'Timeout(in seconds, 0 for no limits)',
'log expiration(log expired after N days, 0 will use default setting: {n} days)': 'Log expiration(log expired after N days, 0 will use default setting: {0} days)',
'0 * * * * *, rules see the 「?」on the right': '0 * * * * *, rules see the 「?」on the right',
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<sec> <min> <hr> <day> <month> <week>, rules is same with Cron',
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '&lt;sec&gt; &lt;min&gt; &lt;hour&gt; &lt;day&gt; &lt;month&gt; &lt;week&gt;, rules is same with Cron.' +
'<br/>If want run job once at special time (like Linux\'s "at" command), you can use "@at 2006-01-02 15:04:05" to set it.' +
'<br/>You may use one of several pre-defined schedules in place of a cron expression. "@hourly" run once an hour, beginning of hour.' +
'<br/>More detail please visit the wiki.',
'and please running on those nodes': 'And please running on those nodes',
'do not running on those nodes': 'Do not running on those nodes',
'the job dose not have a timer currently, please click the button below to add a timer': 'The job dose not have a timer currently, please click the button below to add a timer',

View File

@ -76,6 +76,7 @@ var language = {
'all groups': '所有分组',
'all nodes': '所有节点',
'on {node} took {times}, {begin ~ end}': '于 {0} 耗时 {1}, {2}',
'next schedule: {nextTime}': '下个调度: {0}',
'create job': '新建任务',
'update job': '更新任务',
'output': '输出',
@ -90,8 +91,8 @@ var language = {
'single node single process': '单机单进程',
'group level common': '组级别普通任务',
'group level common help': '暂时没想到好名字,一个比较简单的说明是,把所有选中的节点视为一个大节点,那么该类型的任务就相当于在单个节点上的普通任务',
'warning on': '开启报警',
'warning off': '关闭报警',
'warning on': '报警已开启',
'warning off': '报警已关闭',
'job group': '任务分组',
'script path': '任务脚本',
'(only [{.suffixs}] files can be allowed)': '(只允许 [{0}] 文件)',
@ -107,7 +108,10 @@ var language = {
'timeout(in seconds, 0 for no limits)': '超时设置单位“秒”0 表示不限制)',
'log expiration(log expired after N days, 0 will use default setting: {n} days)': '日志过期日志保存天数0 表示使用默认设置:{0} 天)',
'0 * * * * *, rules see the 「?」on the right': '0 * * * * *, 规则参考右边的「?」',
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<秒> <分> <时> <日> <月> <周>,规则与 Cron 一样',
'<sec> <min> <hr> <day> <month> <week>, rules is same with Cron': '<秒> <分> <时> <日> <月> <周>,规则与 Cron 一样。' +
'<br/>如果要指定只在某个时间点执行一次类似Linux系统的at命令可以使用 "@at 2006-01-02 15:04:05" 这样来设定。' +
'<br/>也支持一些简写,例如 @daily 表示每天执行一次。' +
'<br/>更多请参考wiki。',
'and please running on those nodes': '同时在这些节点上面运行',
'do not running on those nodes': '不要在这些节点上面运行',
'the job dose not have a timer currently, please click the button below to add a timer': '当前任务没有定时器,点击下面按钮来添加定时器',