forked from Ilhasoft/mailroom
-
Notifications
You must be signed in to change notification settings - Fork 2
/
workers.go
211 lines (175 loc) · 5.38 KB
/
workers.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
package mailroom
import (
"context"
"runtime/debug"
"sync"
"time"
"github.com/nyaruka/mailroom/core/queue"
"github.com/nyaruka/mailroom/runtime"
"github.com/nyaruka/mailroom/runtime/metrics"
"github.com/sirupsen/logrus"
)
// Foreman takes care of managing our set of workers and assigns msgs for each to send
type Foreman struct {
rt *runtime.Runtime
wg *sync.WaitGroup
queue string
workers []*Worker
availableWorkers chan *Worker
quit chan bool
}
// NewForeman creates a new Foreman for the passed in server with the number of max workers
func NewForeman(rt *runtime.Runtime, wg *sync.WaitGroup, queue string, maxWorkers int) *Foreman {
foreman := &Foreman{
rt: rt,
wg: wg,
queue: queue,
workers: make([]*Worker, maxWorkers),
availableWorkers: make(chan *Worker, maxWorkers),
quit: make(chan bool),
}
for i := 0; i < maxWorkers; i++ {
foreman.workers[i] = NewWorker(foreman, i)
}
return foreman
}
// Start starts the foreman and all its workers, assigning jobs while there are some
func (f *Foreman) Start() {
metrics.SetAvailableWorkers(f.queue, len(f.workers))
metrics.SetUsedWorkers(f.queue, 0)
for _, worker := range f.workers {
worker.Start()
}
go f.Assign()
}
// Stop stops the foreman and all its workers, the wait group of the worker can be used to track progress
func (f *Foreman) Stop() {
for _, worker := range f.workers {
worker.Stop()
}
close(f.quit)
logrus.WithField("comp", "foreman").WithField("queue", f.queue).WithField("state", "stopping").Info("foreman stopping")
metrics.SetAvailableWorkers(f.queue, 0)
metrics.SetUsedWorkers(f.queue, 0)
}
// Assign is our main loop for the Foreman, it takes care of popping the next outgoing task from our
// backend and assigning them to workers
func (f *Foreman) Assign() {
f.wg.Add(1)
defer f.wg.Done()
log := logrus.WithField("comp", "foreman").WithField("queue", f.queue)
log.WithFields(logrus.Fields{
"state": "started",
"workers": len(f.workers),
"queue": f.queue,
}).Info("workers started and waiting")
lastSleep := false
for {
metrics.SetAvailableWorkers(f.queue, len(f.availableWorkers))
metrics.SetUsedWorkers(f.queue, len(f.workers)-len(f.availableWorkers))
select {
// return if we have been told to stop
case <-f.quit:
log.WithField("state", "stopped").Info("foreman stopped")
return
// otherwise, grab the next task and assign it to a worker
case worker := <-f.availableWorkers:
// see if we have a task to work on
rc := f.rt.RP.Get()
task, err := queue.PopNextTask(rc, f.queue)
rc.Close()
if err == nil && task != nil {
// if so, assign it to our worker
worker.job <- task
lastSleep = false
} else {
// we received an error getting the next message, log it
if err != nil {
log.WithError(err).Error("error popping task")
}
// add our worker back to our queue and sleep a bit
if !lastSleep {
log.Debug("sleeping, no tasks")
lastSleep = true
}
f.availableWorkers <- worker
time.Sleep(250 * time.Millisecond)
}
}
}
}
// Worker is our type for a single goroutine that is handling queued events
type Worker struct {
id int
foreman *Foreman
job chan *queue.Task
}
// NewWorker creates a new worker responsible for working on events
func NewWorker(foreman *Foreman, id int) *Worker {
worker := &Worker{
id: id,
foreman: foreman,
job: make(chan *queue.Task, 1),
}
return worker
}
// Start starts our Worker's goroutine and has it start waiting for tasks from the foreman
func (w *Worker) Start() {
w.foreman.wg.Add(1)
go func() {
defer w.foreman.wg.Done()
log := logrus.WithField("queue", w.foreman.queue).WithField("worker_id", w.id)
log.Debug("started")
for {
// list ourselves as available for work
w.foreman.availableWorkers <- w
// grab our next piece of work
task := <-w.job
// exit if we were stopped
if task == nil {
log.Debug("stopped")
return
}
w.handleTask(task)
}
}()
}
// Stop stops our worker
func (w *Worker) Stop() {
close(w.job)
}
func (w *Worker) handleTask(task *queue.Task) {
log := logrus.WithField("queue", w.foreman.queue).WithField("worker_id", w.id).WithField("task_type", task.Type).WithField("org_id", task.OrgID)
defer func() {
// catch any panics and recover
panicLog := recover()
if panicLog != nil {
debug.PrintStack()
log.WithField("task", string(task.Task)).WithField("task_type", task.Type).WithField("org_id", task.OrgID).Errorf("panic handling task: %s", panicLog)
}
// mark our task as complete
rc := w.foreman.rt.RP.Get()
err := queue.MarkTaskComplete(rc, w.foreman.queue, task.OrgID)
if err != nil {
log.WithError(err)
}
rc.Close()
}()
log.Info("starting handling of task")
start := time.Now()
taskFunc, found := taskFunctions[task.Type]
if found {
err := taskFunc(context.Background(), w.foreman.rt, task)
if err != nil {
log.WithError(err).WithField("task", string(task.Task)).Error("error running task")
}
} else {
log.Error("unable to find function for task type")
}
elapsed := time.Since(start)
log.WithField("elapsed", elapsed).Info("task complete")
// additionally if any task took longer than 1 minute, log as warning
if elapsed > time.Minute {
log.WithField("task", string(task.Task)).WithField("elapsed", elapsed).Warn("long running task")
}
}