-
Notifications
You must be signed in to change notification settings - Fork 1
/
task.go
456 lines (413 loc) · 10.5 KB
/
task.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
package tasks
import (
"fmt"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/juju/errors"
)
// TimeUnit specifies the time unit: 'minutes', 'hours'...
type TimeUnit uint
const (
// Never specifies the time unit to never run a task
Never TimeUnit = iota
// Seconds specifies the time unit in seconds
Seconds
// Minutes specifies the time unit in minutes
Minutes
// Hours specifies the time unit in hours
Hours
// Days specifies the time unit in days
Days
// Weeks specifies the time unit in weeks
Weeks
)
// Task defines task interface
type Task interface {
// Name returns a name of the task
Name() string
// RunCount species the number of times the task executed
RunCount() uint32
// NextScheduledTime returns the time of when this task is to run next
NextScheduledTime() time.Time
// LastRunTime returns the time of last run
LastRunTime() time.Time
// Duration returns interval between runs
Duration() time.Duration
// ShouldRun returns true if the task should be run now
ShouldRun() bool
// Run will try to run the task, if it's not already running
// and immediately reschedule it after run
Run() bool
// Do accepts a function that should be called every time the task runs
Do(taskName string, task interface{}, params ...interface{}) Task
}
// task describes a task schedule
type task struct {
// pause interval * unit bettween runs
interval uint64
// time units, ,e.g. 'minutes', 'hours'...
unit TimeUnit
// number of runs
count uint32
// datetime of last run
lastRunAt *time.Time
// datetime of next run
nextRunAt time.Time
// cache the period between last an next run
period time.Duration
// Specific day of the week to start on
startDay time.Weekday
// the task name
name string
// callback is the function to execute
callback reflect.Value
// params for the callback functions
params []reflect.Value
runLock chan struct{}
running bool
}
// NewTaskAtIntervals creates a new task with the time interval.
func NewTaskAtIntervals(interval uint64, unit TimeUnit) Task {
return &task{
interval: interval,
unit: unit,
lastRunAt: nil,
nextRunAt: time.Unix(0, 0),
period: 0,
startDay: time.Sunday,
runLock: make(chan struct{}, 1),
count: 0,
}
}
// NewTaskOnWeekday creates a new task to execute on specific day of the week.
func NewTaskOnWeekday(startDay time.Weekday, hour, minute int) Task {
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
logger.Panicf("reason='invalid time value', time='%d:%d'", hour, minute)
}
j := &task{
interval: 1,
unit: Weeks,
lastRunAt: nil,
nextRunAt: time.Unix(0, 0),
period: 0,
startDay: startDay,
runLock: make(chan struct{}, 1),
count: 0,
}
return j.at(hour, minute)
}
// NewTaskDaily creates a new task to execute daily at specific time
func NewTaskDaily(hour, minute int) Task {
if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
logger.Panicf("reason='invalid time value', time='%d:%d'", hour, minute)
}
j := &task{
interval: 1,
unit: Days,
lastRunAt: nil,
nextRunAt: time.Unix(0, 0),
period: 0,
startDay: time.Sunday,
runLock: make(chan struct{}, 1),
count: 0,
}
return j.at(hour, minute)
}
// NewTask creates a new task from parsed format string.
// every %d
// seconds | minutes | ...
// Monday | .. | Sunday
// at %hh:mm
func NewTask(format string) (Task, error) {
return parseTaskFormat(format)
}
// Name returns a name of the task
func (j *task) Name() string {
return j.name
}
// RunCount species the number of times the task executed
func (j *task) RunCount() uint32 {
return atomic.LoadUint32(&j.count)
}
// ShouldRun returns true if the task should be run now
func (j *task) ShouldRun() bool {
return !j.running && time.Now().After(j.nextRunAt)
}
// NextScheduledTime returns the time of when this task is to run next
func (j *task) NextScheduledTime() time.Time {
return j.nextRunAt
}
// LastRunTime returns the time of last run
func (j *task) LastRunTime() time.Time {
if j.lastRunAt != nil {
return *j.lastRunAt
}
return time.Unix(0, 0)
}
// // Duration returns interval between runs
func (j *task) Duration() time.Duration {
if j.period == 0 {
switch j.unit {
case Seconds:
j.period = time.Duration(j.interval) * time.Second
break
case Minutes:
j.period = time.Duration(j.interval) * time.Minute
break
case Hours:
j.period = time.Duration(j.interval) * time.Hour
break
case Days:
j.period = time.Duration(j.interval) * time.Hour * 24
break
case Weeks:
j.period = time.Duration(j.interval) * time.Hour * 24 * 7
break
}
}
return j.period
}
// Do accepts a function that should be called every time the task runs
func (j *task) Do(taskName string, taskFunc interface{}, params ...interface{}) Task {
typ := reflect.TypeOf(taskFunc)
if typ.Kind() != reflect.Func {
logger.Panic("reason='only function can be schedule into the task queue'")
}
j.name = fmt.Sprintf("%s@%s", taskName, filepath.Base(getFunctionName(taskFunc)))
j.callback = reflect.ValueOf(taskFunc)
if len(params) != j.callback.Type().NumIn() {
logger.Panicf("reason='the number of parameters does not match the function'")
}
j.params = make([]reflect.Value, len(params))
for k, param := range params {
j.params[k] = reflect.ValueOf(param)
}
//schedule the next run
j.scheduleNextRun()
return j
}
func (j *task) at(hour, min int) *task {
y, m, d := time.Now().Date()
// time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
mock := time.Date(y, m, d, hour, min, 0, 0, loc)
if j.unit == Days {
if !time.Now().After(mock) {
// remove 1 day
mock = mock.UTC().AddDate(0, 0, -1).Local()
}
} else if j.unit == Weeks {
if j.startDay != time.Now().Weekday() || (time.Now().After(mock) && j.startDay == time.Now().Weekday()) {
i := int(mock.Weekday() - j.startDay)
if i < 0 {
i = 7 + i
}
mock = mock.UTC().AddDate(0, 0, -i).Local()
} else {
// remove 1 week
mock = mock.UTC().AddDate(0, 0, -7).Local()
}
}
j.lastRunAt = &mock
return j
}
// scheduleNextRun computes the instant when this task should run next
func (j *task) scheduleNextRun() time.Time {
now := time.Now()
if j.lastRunAt == nil {
if j.unit == Weeks {
i := now.Weekday() - j.startDay
if i < 0 {
i = 7 + i
}
y, m, d := now.Date()
now = time.Date(y, m, d-int(i), 0, 0, 0, 0, loc)
}
j.lastRunAt = &now
}
j.nextRunAt = j.lastRunAt.Add(j.Duration())
/*
logger.Tracef("lastRunAt='%v', nextRunAt='%v', task=%q",
j.lastRunAt.Format(time.RFC3339),
j.nextRunAt.Format(time.RFC3339),
j.Name())
*/
return j.nextRunAt
}
// for given function fn, get the name of function.
func getFunctionName(fn interface{}) string {
return runtime.FuncForPC(reflect.ValueOf((fn)).Pointer()).Name()
}
// Run will try to run the task, if it's not already running
// and immediately reschedule it after run
func (j *task) Run() bool {
timeout := time.Millisecond
timer := time.NewTimer(timeout)
select {
case j.runLock <- struct{}{}:
timer.Stop()
now := time.Now()
j.lastRunAt = &now
j.running = true
count := atomic.AddUint32(&j.count, 1)
logger.Infof("status=running, count=%d, started_at='%v', task=%q",
count,
j.lastRunAt.Format(time.RFC3339),
j.Name())
j.callback.Call(j.params)
j.running = false
j.scheduleNextRun()
<-j.runLock
return true
case <-time.After(timeout):
}
logger.Tracef("reason=already_running, count=%d, started_at='%v', task=%q",
j.count,
j.lastRunAt.Format(time.RFC3339),
j.Name())
return false
}
func parseTimeFormat(t string) (hour, min int, err error) {
var errTimeFormat = errors.NotValidf("%q time format", t)
ts := strings.Split(t, ":")
if len(ts) != 2 {
err = errors.Trace(errTimeFormat)
return
}
hour, err = strconv.Atoi(ts[0])
if err != nil {
err = errors.Trace(err)
return
}
min, err = strconv.Atoi(ts[1])
if err != nil {
err = errors.Trace(err)
return
}
if hour < 0 || hour > 23 || min < 0 || min > 59 {
err = errors.Trace(errTimeFormat)
return
}
return
}
func parseTaskFormat(format string) (*task, error) {
var errTimeFormat = errors.NotValidf("%q task format", format)
j := &task{
interval: 0,
unit: Never,
lastRunAt: nil,
nextRunAt: time.Unix(0, 0),
period: 0,
startDay: time.Sunday,
runLock: make(chan struct{}, 1),
count: 0,
}
ts := strings.Split(strings.ToLower(format), " ")
for _, t := range ts {
switch t {
case "every":
if j.interval > 0 {
return nil, errors.Trace(errTimeFormat)
}
j.interval = 1
break
case "second", "seconds":
j.unit = Seconds
break
case "minute", "minutes":
j.unit = Minutes
break
case "hour", "hours":
j.unit = Hours
break
case "day", "days":
j.unit = Days
break
case "week", "weeks":
j.unit = Weeks
break
case "monday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Monday
break
case "tuesday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Tuesday
break
case "wednesday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Wednesday
break
case "thursday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Thursday
break
case "friday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Friday
break
case "saturday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Saturday
break
case "sunday":
if j.interval > 1 || j.unit != Never {
return nil, errors.Trace(errTimeFormat)
}
j.unit = Weeks
j.startDay = time.Sunday
break
default:
if strings.Contains(t, ":") {
hour, min, err := parseTimeFormat(t)
if err != nil {
return nil, errors.Trace(errTimeFormat)
}
if j.unit == Never {
j.unit = Days
} else if j.unit != Days && j.unit != Weeks {
return nil, errors.Trace(errTimeFormat)
}
j.at(hour, min)
} else {
if j.interval > 1 {
return nil, errors.Trace(errTimeFormat)
}
interval, err := strconv.ParseUint(t, 10, 0)
if err != nil || interval < 1 {
return nil, errors.Trace(errTimeFormat)
}
j.interval = interval
}
}
}
if j.interval == 0 {
j.interval = 1
}
if j.unit == Never {
return nil, errors.Trace(errTimeFormat)
}
return j, nil
}