-
Notifications
You must be signed in to change notification settings - Fork 0
/
gochronos.go
306 lines (258 loc) · 8.2 KB
/
gochronos.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
package gochronos
import (
// "fmt"
"sync"
"time"
)
const (
FREQ_SECOND int = 1 + iota
FREQ_MINUTE
FREQ_HOUR
FREQ_DAY
FREQ_WEEK
FREQ_MONTH
FREQ_YEAR
)
// A command that can be sent to a goroutine.
type command int
const (
// Cancel the goroutine for a scheduled action
CMD_CANCEL command = 1 + iota
CMD_UPDATE_TIME
)
// ActionFunc is basically a function to call when time is up, with optional parameters supplied when
// scheduled action was added.
type ActionFunc func(args ...interface{})
// A specification of when to execute an action. This can either be one-off, created by NewOneOff(), or
// recurring, created by NewRecurring().
type TimeSpec struct {
recurring bool
when time.Time
startTime time.Time
endTime time.Time
frequency int // one of FREQ_ constants
interval int
// byday
// byhours
// byminute
maxNum int
}
// ScheduledAction represents an action that is scheduled in time. When added to the schedule,
// it will execute in accordance with the time specification.
type ScheduledAction struct {
// @todo consider sync.Mutex if the goroutine can modify the struct.
// specification of when the action should trigger
When *TimeSpec
// The action to invoke when time is met
Action ActionFunc
// Parameters passed to the action.
Parameters []interface{}
cmdChan chan command
}
// A list of scheduled actions. This is the schedule that is executed.
var schedule map[*ScheduledAction]bool
// This is used to synchronise updates to the schedule across goroutines.
var scheduleLock sync.Mutex
func init() {
ClearAll()
}
// create a new scheduled action. To add to the schedule, call AddToScheduled, or just Add which creates
// and adds to schedule.
func NewScheduledAction(ts *TimeSpec, f ActionFunc, args []interface{}) *ScheduledAction {
return &ScheduledAction{When: ts, Action: f, Parameters: args}
}
// Add a scheduled action to the schedule
func AddToSchedule(sa *ScheduledAction) {
scheduleLock.Lock()
// add a scheduled action to the list
schedule[sa] = true
scheduleLock.Unlock()
sa.startTimer()
}
// Add a scheduled action to the schedule.
func Add(ts *TimeSpec, f ActionFunc, args ...interface{}) *ScheduledAction {
sa := NewScheduledAction(ts, f, args)
AddToSchedule(sa)
return sa
}
// Remove a scheduled action from the schedule.
func Remove(sa *ScheduledAction) {
// Tell the timer goroutine to stop. This in turn will trigger the goroutine to remove itself.
sa.stopTimer()
}
// Remove scheduled action from list. This assumes the timer goroutine
// is not going to trigger more events. This can be called by the timer
// goroutines when they reach termination, so locking is required on the structure.
func remove(sa *ScheduledAction) {
scheduleLock.Lock()
delete(schedule, sa)
scheduleLock.Unlock()
}
// Change the time specification on a scheduled action. If the timer goroutine
// has been started, send it a command to tell it to update when it next executes.
// The change takes effect immediately.
func (sa *ScheduledAction) SetTimeSpec(ts *TimeSpec) {
sa.When = ts
if sa.cmdChan != nil {
sa.cmdChan <- CMD_UPDATE_TIME
}
}
// Change the action.
func (sa *ScheduledAction) SetAction(f ActionFunc) {
sa.Action = f
}
// Change the parameters.
// @todo Consider merging with action so they occur atomically, as we wouldn't want
// @todo to execute an action with wrong parameters.
func (sa *ScheduledAction) SetParams(args ...interface{}) {
sa.Parameters = args
}
// Given a scheduled action, start a goroutine for executing.
func (sc *ScheduledAction) startTimer() {
sc.cmdChan = make(chan command)
go func() {
var timer *time.Timer
loop:
for t := sc.When.GetNextExec(); !t.IsZero(); {
d := t.Sub(time.Now())
if d < 0 {
d = 0
}
// create the time first time around, or reset it if we're re-using it.
if timer == nil {
timer = time.NewTimer(d)
} else {
timer.Reset(d)
}
// wait for either the time, or a command from the command channel
select {
case _ = <-timer.C:
// when timer goes off, we execute the action and repeat the loop
sc.Action(sc.Parameters...)
case cmd := <-sc.cmdChan:
if cmd == CMD_CANCEL {
// the scheduled action is being cancelled
timer.Stop()
break loop
} else if cmd == CMD_UPDATE_TIME {
// the scheduled action has been updated, and we need to
// re-evaluate
t = sc.When.GetNextExec()
continue loop
}
}
t = sc.When.GetNextExec()
}
remove(sc)
}()
}
// Stop a scheduled action.
func (sc *ScheduledAction) stopTimer() {
// send cancel command to the goroutine
sc.cmdChan <- CMD_CANCEL
}
// Create a new one-off time specification from a Time.
func NewOneOff(t time.Time) *TimeSpec {
return &TimeSpec{recurring: false, when: t}
}
// Create a new recurring time specification from a map.
func NewRecurring(config map[string]interface{}) *TimeSpec {
result := &TimeSpec{
recurring: true,
interval: 1,
startTime: time.Time{},
endTime: time.Time{},
frequency: -1,
maxNum: -1,
}
for k, v := range config {
switch k {
case "starttime": // expect time
result.startTime = v.(time.Time)
case "frequency": // expect int, which should be a FREQ_* constant
result.frequency = v.(int)
case "interval": // expect int: multiplier for frequency e.g. 2 week is a fortnight
result.interval = v.(int)
// case "byday": // - (optional) a string or array of strings that define days of the week when the action is to be executed. Valid values are "su","mo","tu","we","th","fr","sa"
// case "byhours": // byhour - (optional) an int or array of ints that define the hours of the day when the action is to be executed.
// case "byminute": // - (optional) an int or array of ints that define the minutes of the hours when the action is to be executed
case "endtime": // expect time
result.endTime = v.(time.Time)
case "maxnum": // expect int
result.maxNum = v.(int)
}
}
// ensure startime and frequency are provided.
if result.startTime.IsZero() {
panic("recurring scheduled action must have a start date")
}
if result.frequency < FREQ_SECOND || result.frequency > FREQ_YEAR {
panic("recurring scheduled action must have a frequency")
}
return result
}
// Given the current time, evaluate what the next execution time is according to the time spec.
// Logic is as follows:
// - if timespec is one-off:
// - if the time is in the past, return the zero value for Time. Past scheduled events are not executed.
// - otherwise return the time
// - if timespec is recurring:
// - if termination condition is met, return the zero value for Time.
// - compute forward from the start date, finding the closest date in the future that meets the spec, and return that.
func (t *TimeSpec) GetNextExec() time.Time {
now := time.Now()
if t.recurring {
// if termination condition is met, return zero time
if !t.endTime.IsZero() && t.endTime.Before(now) {
return time.Time{}
}
// if start time is in the future, return that
if t.startTime.After(now) {
return t.startTime
}
// determine period in seconds
period := 0
switch t.frequency {
case FREQ_SECOND:
period = 1
case FREQ_MINUTE:
period = 60
case FREQ_HOUR:
period = 3600
case FREQ_DAY:
period = 86400
case FREQ_WEEK:
period = 604800
}
if period > 0 {
// it's a fixed number of seconds period, which excludes months and years
period *= t.interval
// @todo take into account byday, byhour, byminute
delta := now.Sub(t.startTime) // difference between start and now.
td := int(delta*time.Second) % period
prev := time.Unix(now.Unix()-int64(td), 0)
next := prev.Add(time.Duration(period) * time.Second)
return next
}
// @todo implement month and year
switch t.frequency {
case FREQ_MONTH:
case FREQ_YEAR:
}
return time.Time{}
} else {
if t.when.Before(now) {
return time.Time{}
}
return t.when
}
}
// Register an instance of a type that might be used for schedule. This is required if actions
// are being serialised, so that when deserialising, we know how to treat
// func RegisterType(Action) {
// }
// Clear the schedule of all scheduled actions.
// @todo if schedule is already defined and there are executing scheduled actions, terminate them so they're GC'd.
func ClearAll() {
schedule = make(map[*ScheduledAction]bool)
}