Skip to content

Commit 0fffbad

Browse files
authored
Merge pull request #1 from alexsniffin/update-interface
Upgrade to Go 1.19 and API to support generics
2 parents f9d5230 + 85ae62e commit 0fffbad

16 files changed

+259
-325
lines changed

.golangci.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ output:
1010
print-issued-lines: true
1111
print-linter-name: true
1212
uniq-by-line: true
13-
new: false
13+
new: false
14+

.travis.yml

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
dist: bionic
1+
dist: jammy
22

33
services:
44
- docker
55

66
script:
7-
- docker build --build-arg VERSION=1.13 .
8-
- docker build --build-arg VERSION=1.14 .
9-
- docker build --build-arg VERSION=1.15 .
7+
- docker build --build-arg VERSION=1.18 .
8+
- docker build --build-arg VERSION=1.19 .

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ COPY . $GOPATH/src/github.com/alexsniffin/gosd.git/
1414
WORKDIR $GOPATH/src/github.com/alexsniffin/gosd.git/
1515

1616
# Pull dependencies
17-
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0
17+
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
1818

1919
# Run quality check
2020
RUN golangci-lint run --timeout 10m0s -v --build-tags mus -c .golangci.yml \

README.md

+23-14
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
go-schedulable-dispatcher (gosd), is a library for scheduling when to dispatch a message to a channel.
88

99
## Implementation
10-
The implementation provides an ease-of-use API with both an ingress (ingest) channel and egress (dispatch) channel. Messages are ingested and processed into a heap based priority queue for dispatching. At most two separate goroutines are used, one for processing of messages from both the ingest channel and heap then the other as a timer. Order is not guaranteed by default when messages have the same scheduled time, but can be changed through the config. By guaranteeing order, performance will be slightly worse. If strict-ordering isn't critical to your application, it's recommended to keep the default setting.
10+
The implementation provides an interactive API to handle scheduling messages with a dispatcher. Messages are ingested and processed into a heap based priority queue. Order is not guaranteed by default when messages have the same scheduled time, but can be changed through the config. By guaranteeing order, performance will be slightly worse. If strict-ordering isn't critical to your application, it's recommended to keep the default setting.
1111

1212
## Example
1313
```go
1414
// create instance of dispatcher
15-
dispatcher, err := gosd.NewDispatcher(&gosd.DispatcherConfig{
15+
dispatcher, err := gosd.NewDispatcher[string](&gosd.DispatcherConfig{
1616
IngressChannelSize: 100,
1717
DispatchChannelSize: 100,
1818
MaxMessages: 100,
@@ -24,17 +24,15 @@ checkErr(err)
2424
go dispatcher.Start()
2525

2626
// schedule a message
27-
dispatcher.IngressChannel() <- &gosd.ScheduledMessage{
27+
dispatcher.IngressChannel() <- &gosd.ScheduledMessage[string]{
2828
At: time.Now().Add(1 * time.Second),
2929
Message: "Hello World in 1 second!",
3030
}
3131

3232
// wait for the message
3333
msg := <-dispatcher.DispatchChannel()
3434

35-
// type assert
36-
msgStr := msg.(string)
37-
fmt.Println(msgStr)
35+
fmt.Println(msg)
3836
// Hello World in 1 second!
3937

4038
// shutdown without deadline
@@ -44,13 +42,24 @@ dispatcher.Shutdown(context.Background(), false)
4442
More examples under [examples](examples).
4543

4644
## Benchmarking
47-
Tested with Intel Core i7-8700K CPU @ 3.70GHz, DDR4 RAM and 1000 messages per iteration.
45+
Tested with Go 1.19 and 1000 messages per iteration.
4846
```
49-
Benchmark_integration_unordered-12 142 8654906 ns/op
50-
Benchmark_integration_unorderedSmallBuffer-12 147 9503403 ns/op
51-
Benchmark_integration_unorderedSmallHeap-12 122 8860732 ns/op
52-
Benchmark_integration_ordered-12 96 13354174 ns/op
53-
Benchmark_integration_orderedSmallBuffer-12 121 10115702 ns/op
54-
Benchmark_integration_orderedSmallHeap-12 129 10441857 ns/op
55-
Benchmark_integration_orderedSameTime-12 99 12575961 ns/op
47+
goos: windows
48+
goarch: amd64
49+
pkg: github.com/alexsniffin/gosd/v2
50+
cpu: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz
51+
Benchmark_integration_unordered
52+
Benchmark_integration_unordered-12 307 3690528 ns/op
53+
Benchmark_integration_unorderedSmallBuffer
54+
Benchmark_integration_unorderedSmallBuffer-12 274 4120104 ns/op
55+
Benchmark_integration_unorderedSmallHeap
56+
Benchmark_integration_unorderedSmallHeap-12 348 3452703 ns/op
57+
Benchmark_integration_ordered
58+
Benchmark_integration_ordered-12 135 8650709 ns/op
59+
Benchmark_integration_orderedSmallBuffer
60+
Benchmark_integration_orderedSmallBuffer-12 207 5867338 ns/op
61+
Benchmark_integration_orderedSmallHeap
62+
Benchmark_integration_orderedSmallHeap-12 350 3592990 ns/op
63+
Benchmark_integration_orderedSameTime
64+
Benchmark_integration_orderedSameTime-12 133 8909311 ns/op
5665
```

delayer.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"time"
55
)
66

7-
type delayer interface {
7+
type delayer[T any] interface {
88
stop(drain bool)
9-
wait(msg *ScheduledMessage)
9+
wait(msg *ScheduledMessage[T])
1010
available() bool
1111
}
1212

@@ -17,31 +17,33 @@ const (
1717
waiting
1818
)
1919

20-
type delay struct {
21-
state delayState
20+
type delay[T any] struct {
21+
state delayState // nolint:unused
2222

2323
idleChannel chan<- bool
24-
egressChannel chan<- interface{}
24+
egressChannel chan<- T
2525
cancelChannel chan bool
2626
}
2727

28-
func newDelay(egressChannel chan<- interface{}, idleChannel chan<- bool) *delay {
29-
return &delay{
28+
func newDelay[T any](egressChannel chan<- T, idleChannel chan<- bool) *delay[T] {
29+
return &delay[T]{
3030
idleChannel: idleChannel,
3131
egressChannel: egressChannel,
3232
cancelChannel: make(chan bool, 1),
3333
}
3434
}
3535

3636
// stop sends a cancel signal to the current timer process.
37-
func (d *delay) stop(drain bool) {
37+
// nolint:unused
38+
func (d *delay[T]) stop(drain bool) {
3839
if d.state == waiting {
3940
d.cancelChannel <- drain
4041
}
4142
}
4243

4344
// wait will create a timer based on the time from `msg.At` and dispatch the message to the egress channel asynchronously.
44-
func (d *delay) wait(msg *ScheduledMessage) {
45+
// nolint:unused
46+
func (d *delay[T]) wait(msg *ScheduledMessage[T]) {
4547
d.state = waiting
4648
curTimer := time.NewTimer(time.Until(msg.At))
4749

@@ -69,6 +71,7 @@ func (d *delay) wait(msg *ScheduledMessage) {
6971
}
7072

7173
// available returns whether the delay is able to accept a new message to wait on.
72-
func (d *delay) available() bool {
74+
// nolint
75+
func (d *delay[T]) available() bool {
7376
return d.state == idle
7477
}

delayer_test.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func Test_delay_stop(t *testing.T) {
2121
}
2222
for _, tt := range tests {
2323
t.Run(tt.name, func(t *testing.T) {
24-
d := &delay{
24+
d := &delay[any]{
2525
state: tt.fields.state,
2626
egressChannel: tt.fields.egressChannel,
2727
cancelChannel: tt.fields.cancelChannel,
@@ -42,17 +42,17 @@ func Test_delay_wait(t *testing.T) {
4242
cancelChannel chan bool
4343
}
4444
type args struct {
45-
msg *ScheduledMessage
45+
msg *ScheduledMessage[any]
4646
}
4747
tests := []struct {
4848
name string
4949
fields fields
5050
args args
51-
customAssertion func(fields, *delay)
51+
customAssertion func(fields, *delay[any])
5252
}{
5353
{"egressMessage", fields{
5454
egressChannel: make(chan interface{}),
55-
idleChannel: make(chan bool)}, args{msg: &ScheduledMessage{At: time.Now()}}, func(f fields, d *delay) {
55+
idleChannel: make(chan bool)}, args{msg: &ScheduledMessage[any]{At: time.Now()}}, func(f fields, d *delay[any]) {
5656
if d.state != waiting {
5757
t.Errorf("wait() unexpected state = %+v, want Waiting", d.state)
5858
}
@@ -68,7 +68,7 @@ func Test_delay_wait(t *testing.T) {
6868
}},
6969
{"cancelMessage", fields{
7070
cancelChannel: make(chan bool, 1),
71-
idleChannel: make(chan bool)}, args{msg: &ScheduledMessage{At: time.Now().Add(10 + time.Second)}}, func(f fields, d *delay) {
71+
idleChannel: make(chan bool)}, args{msg: &ScheduledMessage[any]{At: time.Now().Add(10 + time.Second)}}, func(f fields, d *delay[any]) {
7272
if d.state != waiting {
7373
t.Errorf("wait() unexpected state = %+v, want Waiting", d.state)
7474
}
@@ -83,7 +83,7 @@ func Test_delay_wait(t *testing.T) {
8383
}
8484
for _, tt := range tests {
8585
t.Run(tt.name, func(t *testing.T) {
86-
d := &delay{
86+
d := &delay[any]{
8787
state: tt.fields.state,
8888
idleChannel: tt.fields.idleChannel,
8989
egressChannel: tt.fields.egressChannel,
@@ -111,7 +111,7 @@ func Test_delay_available(t *testing.T) {
111111
}
112112
for _, tt := range tests {
113113
t.Run(tt.name, func(t *testing.T) {
114-
d := &delay{
114+
d := &delay[any]{
115115
state: tt.fields.state,
116116
egressChannel: tt.fields.egressChannel,
117117
cancelChannel: tt.fields.cancel,

dispatcher.go

+28-28
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,44 @@ const (
1818
)
1919

2020
// Dispatcher processes the ingress and dispatching of scheduled messages.
21-
type Dispatcher struct {
21+
type Dispatcher[T any] struct {
2222
state dispatcherState
2323
maxMessages int
2424

25-
pq priorityQueue
26-
nextMessage *ScheduledMessage
27-
delayer delayer
25+
pq priorityQueue[T]
26+
nextMessage *ScheduledMessage[T]
27+
delayer delayer[T]
2828

2929
delayerIdleChannel chan bool
30-
dispatchChannel chan interface{}
31-
ingressChannel chan *ScheduledMessage
30+
dispatchChannel chan T
31+
ingressChannel chan *ScheduledMessage[T]
3232
shutdown chan error
3333
stopProcess chan bool
3434

3535
mutex *sync.Mutex
3636
}
3737

3838
// NewDispatcher creates a new instance of a Dispatcher.
39-
func NewDispatcher(config *DispatcherConfig) (*Dispatcher, error) {
39+
func NewDispatcher[T any](config *DispatcherConfig) (*Dispatcher[T], error) {
4040
if config.MaxMessages <= 0 {
4141
return nil, errors.New("MaxMessages should be greater than 0")
4242
}
4343

4444
newIdleChannel := make(chan bool, 1)
45-
newDispatchChannel := make(chan interface{}, config.DispatchChannelSize)
46-
newPq := priorityQueue{
47-
items: make([]*item, 0),
45+
newDispatchChannel := make(chan T, config.DispatchChannelSize)
46+
newPq := priorityQueue[T]{
47+
items: make([]*item[T], 0),
4848
maintainOrder: config.GuaranteeOrder,
4949
}
5050

5151
heap.Init(&newPq)
52-
return &Dispatcher{
52+
return &Dispatcher[T]{
5353
pq: newPq,
5454
maxMessages: config.MaxMessages,
55-
delayer: newDelay(newDispatchChannel, newIdleChannel),
55+
delayer: newDelay[T](newDispatchChannel, newIdleChannel),
5656
delayerIdleChannel: newIdleChannel,
5757
dispatchChannel: newDispatchChannel,
58-
ingressChannel: make(chan *ScheduledMessage, config.IngressChannelSize),
58+
ingressChannel: make(chan *ScheduledMessage[T], config.IngressChannelSize),
5959
shutdown: make(chan error),
6060
stopProcess: make(chan bool),
6161
mutex: &sync.Mutex{},
@@ -67,7 +67,7 @@ func NewDispatcher(config *DispatcherConfig) (*Dispatcher, error) {
6767
//
6868
// If drainImmediately is true, then all messages will be dispatched immediately regardless of the schedule set. Order
6969
// can be lost if new messages are still being ingested.
70-
func (d *Dispatcher) Shutdown(ctx context.Context, drainImmediately bool) error {
70+
func (d *Dispatcher[T]) Shutdown(ctx context.Context, drainImmediately bool) error {
7171
if d.state == shutdown || d.state == shutdownAndDrain {
7272
return errors.New("shutdown has already happened")
7373
}
@@ -109,7 +109,7 @@ func (d *Dispatcher) Shutdown(ctx context.Context, drainImmediately bool) error
109109
}
110110

111111
// Start initializes the processing of scheduled messages and blocks.
112-
func (d *Dispatcher) Start() error {
112+
func (d *Dispatcher[T]) Start() error {
113113
d.mutex.Lock()
114114
if d.state == shutdown || d.state == shutdownAndDrain {
115115
return errors.New("dispatcher is already running and shutting/shut down")
@@ -124,7 +124,7 @@ func (d *Dispatcher) Start() error {
124124
}
125125

126126
// Pause updates the state of the Dispatcher to stop processing messages and will close the main process loop.
127-
func (d *Dispatcher) Pause() error {
127+
func (d *Dispatcher[T]) Pause() error {
128128
d.mutex.Lock()
129129
if d.state == shutdown || d.state == shutdownAndDrain {
130130
return errors.New("dispatcher is shutting/shut down and cannot be paused")
@@ -141,7 +141,7 @@ func (d *Dispatcher) Pause() error {
141141

142142
// Resume updates the state of the Dispatcher to start processing messages and starts the timer for the last message
143143
// being processed and blocks.
144-
func (d *Dispatcher) Resume() error {
144+
func (d *Dispatcher[T]) Resume() error {
145145
d.mutex.Lock()
146146
if d.state == shutdown || d.state == shutdownAndDrain {
147147
return errors.New("dispatcher is shutting/shut down")
@@ -159,7 +159,7 @@ func (d *Dispatcher) Resume() error {
159159
}
160160

161161
// process handles the processing of scheduled messages.
162-
func (d *Dispatcher) process() {
162+
func (d *Dispatcher[T]) process() {
163163
for {
164164
select {
165165
case <-d.stopProcess:
@@ -180,7 +180,7 @@ func (d *Dispatcher) process() {
180180
}
181181

182182
// handleShutdown drains the heap.
183-
func (d *Dispatcher) handleShutdownAndDrain() {
183+
func (d *Dispatcher[T]) handleShutdownAndDrain() {
184184
if d.state == shutdownAndDrain {
185185
d.delayer.stop(true)
186186
if len(d.delayerIdleChannel) > 0 {
@@ -192,7 +192,7 @@ func (d *Dispatcher) handleShutdownAndDrain() {
192192

193193
// handlePriorityQueue checks whether the heap is full and will Pop the next message if present and when the delayer is
194194
// idle.
195-
func (d *Dispatcher) handlePriorityQueue() (cont bool) {
195+
func (d *Dispatcher[T]) handlePriorityQueue() (cont bool) {
196196
// check if we've exceeded the maximum messages to store in the heap
197197
if d.pq.Len() >= d.maxMessages {
198198
if len(d.delayerIdleChannel) > 0 {
@@ -210,7 +210,7 @@ func (d *Dispatcher) handlePriorityQueue() (cont bool) {
210210

211211
// handleIngress checks for new messages off the ingress channel and will either dispatch if `shutdownAndDrain`, replace
212212
// the current delayer message or add to the heap.
213-
func (d *Dispatcher) handleIngress() {
213+
func (d *Dispatcher[T]) handleIngress() {
214214
if len(d.ingressChannel) > 0 {
215215
if msg, ok := <-d.ingressChannel; ok {
216216
if d.state == shutdownAndDrain {
@@ -232,26 +232,26 @@ func (d *Dispatcher) handleIngress() {
232232
}
233233
}
234234

235-
func (d *Dispatcher) waitNextMessage() {
236-
msg := heap.Pop(&d.pq).(*ScheduledMessage)
235+
func (d *Dispatcher[T]) waitNextMessage() {
236+
msg := heap.Pop(&d.pq).(*ScheduledMessage[T])
237237
d.nextMessage = msg
238238
d.delayer.wait(msg)
239239
}
240240

241-
func (d *Dispatcher) drainHeap() {
241+
func (d *Dispatcher[T]) drainHeap() {
242242
for d.pq.Len() > 0 {
243-
msg := heap.Pop(&d.pq).(*ScheduledMessage)
243+
msg := heap.Pop(&d.pq).(*ScheduledMessage[T])
244244
// dispatch the message immediately
245245
d.dispatchChannel <- msg.Message
246246
}
247247
}
248248

249249
// IngressChannel returns the send-only channel of type `ScheduledMessage`.
250-
func (d *Dispatcher) IngressChannel() chan<- *ScheduledMessage {
250+
func (d *Dispatcher[T]) IngressChannel() chan<- *ScheduledMessage[T] {
251251
return d.ingressChannel
252252
}
253253

254-
// DispatchChannel returns a receive-only channel of type `interface{}`.
255-
func (d *Dispatcher) DispatchChannel() <-chan interface{} {
254+
// DispatchChannel returns a receive-only channel of type `T`.
255+
func (d *Dispatcher[T]) DispatchChannel() <-chan T {
256256
return d.dispatchChannel
257257
}

0 commit comments

Comments
 (0)