Skip to content

Commit a86a93d

Browse files
authored
Merge pull request #7 from alesr/redisstream-adapter
Redis stream adapter
2 parents 954e045 + f4ff4b0 commit a86a93d

4 files changed

Lines changed: 164 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![codecov](https://codecov.io/gh/alesr/workerpool/graph/badge.svg?token=4dxDuntYgf)](https://codecov.io/gh/alesr/workerpool)
44
[![Go Report Card](https://goreportcard.com/badge/github.com/alesr/workerpool)](https://goreportcard.com/report/github.com/alesr/workerpool)
5-
[![Go Reference](https://pkg.go.dev/badge/github.com/alesr/workerpool.git.svg)](https://pkg.go.dev/github.com/alesr/workerpool.git)
5+
[![Go Reference](https://pkg.go.dev/badge/github.com/alesr/workerpool.git.svg)](https://pkg.go.dev/github.com/alesr/workerpool)
66

77
Generic, type-safe handy worker pool in Go.
88

adapters/redisstream/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/alesr/workerpool/adapters/redisstream
2+
3+
go 1.26
4+
5+
require github.com/redis/go-redis/v9 v9.19.0
6+
7+
require (
8+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
9+
go.uber.org/atomic v1.11.0 // indirect
10+
)

adapters/redisstream/go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
2+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
3+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
4+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
5+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
6+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
7+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
10+
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
14+
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
15+
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
16+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
17+
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
18+
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
19+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
20+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
21+
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
22+
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package redisstream
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"strings"
9+
"time"
10+
11+
"github.com/redis/go-redis/v9"
12+
)
13+
14+
type redisClient interface {
15+
XGroupCreateMkStream(ctx context.Context, stream string, group string, start string) *redis.StatusCmd
16+
XReadGroup(ctx context.Context, a *redis.XReadGroupArgs) *redis.XStreamSliceCmd
17+
XAck(ctx context.Context, stream string, group string, ids ...string) *redis.IntCmd
18+
}
19+
20+
type TaskHandler interface {
21+
Submit(ctx context.Context, id string, payload map[string]string, ack func() error) error
22+
}
23+
24+
type StreamOptions struct {
25+
BatchSize int64
26+
BlockTimeout time.Duration
27+
ErrHandler func(err error)
28+
}
29+
30+
func DefaultStreamOptions() StreamOptions {
31+
return StreamOptions{
32+
BatchSize: 10,
33+
BlockTimeout: 2 * time.Second,
34+
ErrHandler: func(err error) {
35+
log.Printf("[redispool] stream error: %v", err)
36+
},
37+
}
38+
}
39+
40+
type StreamAdapter struct {
41+
rdb redisClient
42+
streamName string
43+
groupName string
44+
consumerID string
45+
opts StreamOptions
46+
}
47+
48+
func NewStreamAdapter(rdb redisClient, stream, group, consumer string, opts StreamOptions) *StreamAdapter {
49+
if opts.BatchSize <= 0 {
50+
opts.BatchSize = DefaultStreamOptions().BatchSize
51+
}
52+
if opts.BlockTimeout <= 0 {
53+
opts.BlockTimeout = DefaultStreamOptions().BlockTimeout
54+
}
55+
if opts.ErrHandler == nil {
56+
opts.ErrHandler = DefaultStreamOptions().ErrHandler
57+
}
58+
59+
return &StreamAdapter{
60+
rdb: rdb,
61+
streamName: stream,
62+
groupName: group,
63+
consumerID: consumer,
64+
opts: opts,
65+
}
66+
}
67+
68+
func (a *StreamAdapter) Initialize(ctx context.Context, offset string) error {
69+
if err := a.rdb.XGroupCreateMkStream(ctx, a.streamName, a.groupName, offset).Err(); err != nil {
70+
if strings.HasPrefix(err.Error(), "BUSYGROUP") {
71+
return nil
72+
}
73+
return fmt.Errorf("could not create group stream: %w", err)
74+
}
75+
return nil
76+
}
77+
78+
// Consume replaces FetchTasks injecting tasks directly into the handler.
79+
func (a *StreamAdapter) Consume(ctx context.Context, handler TaskHandler) {
80+
for {
81+
select {
82+
case <-ctx.Done():
83+
return
84+
default:
85+
streams, err := a.rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
86+
Group: a.groupName,
87+
Consumer: a.consumerID,
88+
Streams: []string{a.streamName, ">"},
89+
Count: a.opts.BatchSize,
90+
Block: a.opts.BlockTimeout,
91+
}).Result()
92+
if err != nil {
93+
if errors.Is(err, redis.Nil) {
94+
continue
95+
}
96+
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
97+
return
98+
}
99+
a.opts.ErrHandler(err)
100+
101+
select {
102+
case <-ctx.Done():
103+
return
104+
case <-time.After(time.Second):
105+
}
106+
continue
107+
}
108+
109+
for _, stream := range streams {
110+
for _, msg := range stream.Messages {
111+
msgID := msg.ID
112+
113+
payload := make(map[string]string)
114+
for k, v := range msg.Values {
115+
if str, ok := v.(string); ok {
116+
payload[k] = str
117+
}
118+
}
119+
120+
if err := handler.Submit(ctx, msg.ID, payload, func() error {
121+
ackCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
122+
defer cancel()
123+
return a.rdb.XAck(ackCtx, a.streamName, a.groupName, msgID).Err()
124+
}); err != nil {
125+
a.opts.ErrHandler(fmt.Errorf("could not submit task to handler: %w", err))
126+
}
127+
}
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)