-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlocal-queue.go
More file actions
101 lines (90 loc) · 2.25 KB
/
Copy pathlocal-queue.go
File metadata and controls
101 lines (90 loc) · 2.25 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
// Package localqueue provides a type-safe local in-memory queue for development.
// A single background worker processes messages one-by-one. No background task
// runs until a consumer is registered.
package localqueue
import (
"container/list"
"sync"
)
// Options configures queue behaviour.
type Options struct {
// MaxRetries is the number of retries after a consumer returns an error (default 3).
MaxRetries int
}
func (o *Options) maxRetries() int {
if o != nil && o.MaxRetries > 0 {
return o.MaxRetries
}
return 3
}
type item[T any] struct {
payload T
retries int
}
// Queue is a type-safe queue that processes messages one-by-one in a single background goroutine.
// The background task is started only when Consume is called.
type Queue[T any] struct {
name string
opts *Options
mu sync.Mutex
list *list.List
cond *sync.Cond
consume func(T) error
running bool
}
// NewQueue creates a new queue with the given name and options.
// Options may be nil to use defaults (MaxRetries: 3).
func NewQueue[T any](name string, opts *Options) *Queue[T] {
q := &Queue[T]{
name: name,
opts: opts,
list: list.New(),
}
q.cond = sync.NewCond(&q.mu)
return q
}
// Push enqueues a message. It is non-blocking and thread-safe.
func (q *Queue[T]) Push(msg T) {
q.mu.Lock()
defer q.mu.Unlock()
q.list.PushBack(&item[T]{payload: msg, retries: 0})
q.cond.Signal()
}
// Consume registers the consumer and starts the single background worker.
// It must be called at most once per queue. Messages are processed one-by-one.
// If the consumer returns an error, the message is retried up to MaxRetries, then dropped.
func (q *Queue[T]) Consume(consumer func(T) error) {
q.mu.Lock()
defer q.mu.Unlock()
if q.consume != nil {
panic("localqueue: Consume already called")
}
q.consume = consumer
if !q.running {
q.running = true
go q.run()
}
}
func (q *Queue[T]) run() {
maxRetries := q.opts.maxRetries()
for {
q.mu.Lock()
for q.list.Len() == 0 {
q.cond.Wait()
}
front := q.list.Front()
q.list.Remove(front)
it := front.Value.(*item[T])
q.mu.Unlock()
err := q.consume(it.payload)
if err != nil {
q.mu.Lock()
if it.retries < maxRetries {
it.retries++
q.list.PushBack(it)
q.cond.Signal()
}
q.mu.Unlock()
}
}
}