-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexecutor.go
More file actions
210 lines (169 loc) · 4.42 KB
/
Copy pathexecutor.go
File metadata and controls
210 lines (169 loc) · 4.42 KB
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
package sequent
import (
"context"
"sync"
"time"
"github.com/efritz/backoff"
"github.com/efritz/watchdog"
)
type (
// Executor abstracts a sequence of tasks which are processed, in order, in a
// background goroutine. New tasks can be scheduled in a non-blocking manner.
Executor interface {
// Start will begin processing scheduled tasks. This method must not block.
Start()
// Schedule will put a task at the end of the processing queue. This method
// will block if Start has not been called, and must not be called after a
// call to Stop or Flush.
Schedule(task Task)
// Stop immediately drops the current task and exit the processing loop. This
// method does not attempt to interrupt the currently running task, but its
// return value will be ignored by the calling function.
Stop()
// Flush blocks until the queue has been completely processed. This method is
// the graceful version of Stop.
Flush()
}
// Task is a function that returns true on success and false on failure.
Task func() bool
executor struct {
backoff backoff.Backoff
buffer []Task
mutex sync.Mutex
tasks chan Task
halt chan struct{}
done chan struct{}
ready chan struct{}
once *sync.Once
}
// ConfigFunc is a function used to initialize a new executor.
ConfigFunc func(*executor)
)
// NewExecutor creates a new Executor.
func NewExecutor(configs ...ConfigFunc) Executor {
backoff := backoff.NewExponentialBackoff(
10*time.Millisecond,
30*time.Second,
)
executor := &executor{
backoff: backoff,
buffer: []Task{},
tasks: make(chan Task),
halt: make(chan struct{}),
done: make(chan struct{}),
ready: make(chan struct{}, 1),
once: &sync.Once{},
}
for _, config := range configs {
config(executor)
}
return executor
}
// WithBackoff sets the backoff strategy to use (default is
// an exponential strategy with a maximum of 30 seconds).
func WithBackoff(backoff backoff.Backoff) ConfigFunc {
return func(e *executor) {
e.backoff = backoff
}
}
func (e *executor) Start() {
go e.queue()
go e.process()
}
func (e *executor) Schedule(task Task) {
e.tasks <- task
}
func (e *executor) Stop() {
e.once.Do(func() {
close(e.halt)
close(e.tasks)
})
}
func (e *executor) Flush() {
close(e.tasks)
<-e.done
close(e.halt)
}
func (e *executor) queue() {
defer close(e.ready)
// Put each task we get from calls to Schedule onto a
// queue so that we don't block the producer.
for task := range e.tasks {
e.push(task)
select {
case e.ready <- struct{}{}:
default:
}
}
}
func (e *executor) process() {
// Close this channel once this goroutine exits so that
// flush has something to synchronize on.
defer close(e.done)
// The outer loop condition blocks until a value is sent
// on ready signifying a non-empty queue. The outer loop
// halts once the halt channel is closed. This causes an
// immediate exit of the process loop, regardless of the
// state of the queue.
outer:
for blockOnSignal(e.halt, e.ready) {
// The inner loop will iterate while the halt channel
// is still open. Once this channel is closed the loop
// will hit the return below.
for !isClosed(e.halt) {
// While we have a task in the buffer attempt to
// process it. If we have an empty queue, kick back
// up to the outer loop and wait until the queue is
// non-empty again (this prevents busy-waiting).
task, ok := e.pop()
if !ok {
continue outer
}
e.call(task)
}
return
}
}
func (e *executor) call(task Task) {
// Create a context that is canceled when a value is received
// on the halt channel (and, to clean up, when the watcher is
// finished - this can be called twice without issue).
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
defer cancel()
<-e.halt
}()
watchdog.BlockUntilSuccess(ctx, watchdog.RetryFunc(task), e.backoff)
}
func (e *executor) push(task Task) {
e.mutex.Lock()
e.buffer = append(e.buffer, task)
e.mutex.Unlock()
}
func (e *executor) pop() (Task, bool) {
e.mutex.Lock()
defer e.mutex.Unlock()
if len(e.buffer) == 0 {
return nil, false
}
var task Task
task, e.buffer = e.buffer[0], e.buffer[1:]
return task, true
}
func blockOnSignal(halt <-chan struct{}, ready <-chan struct{}) bool {
select {
case <-halt:
return false
case _, ok := <-ready:
return ok
}
}
func isClosed(ch <-chan struct{}) bool {
select {
case <-ch:
return true
default:
return false
}
}