Skip to content

Commit 67783fa

Browse files
authored
Merge pull request #41 from hellofresh/feature/delayed-retries
delayed notification retries
2 parents 7c77ce0 + d4c2109 commit 67783fa

File tree

10 files changed

+399
-42
lines changed

10 files changed

+399
-42
lines changed

driver/sql/postgres/projector_aggregate_bench_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func setup(
182182
},
183183
goengine.NopLogger,
184184
driverSQL.NopMetrics,
185+
0,
185186
)
186187
require.NoError(b, err, "failed to create aggregate projector")
187188

driver/sql/projection.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sql
33
import (
44
"context"
55
"database/sql"
6+
"time"
67

78
"github.com/hellofresh/goengine"
89
"github.com/mailru/easyjson/jlexer"
@@ -12,8 +13,9 @@ import (
1213
type (
1314
// ProjectionNotification is a representation of the data provided by database notify
1415
ProjectionNotification struct {
15-
No int64 `json:"no"`
16-
AggregateID string `json:"aggregate_id"`
16+
No int64 `json:"no"`
17+
AggregateID string `json:"aggregate_id"`
18+
ValidAfter time.Time `json:"valid_after"`
1719
}
1820

1921
// ProjectionTrigger triggers the notification for processing

driver/sql/projection_notification_processor.go

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,37 @@ import (
44
"context"
55
"runtime"
66
"sync"
7+
"time"
78

89
"github.com/hellofresh/goengine"
910
"github.com/pkg/errors"
1011
)
1112

12-
// Ensure the projectionNotificationProcessor.Queue is a ProjectionTrigger
13-
var _ ProjectionTrigger = (&projectionNotificationProcessor{}).Queue
14-
1513
type (
16-
// projectionNotificationProcessor provides a way to Trigger a notification using a set of background processes.
17-
projectionNotificationProcessor struct {
14+
// ProjectionNotificationProcessor provides a way to Trigger a notification using a set of background processes.
15+
ProjectionNotificationProcessor struct {
1816
done chan struct{}
19-
queue chan *ProjectionNotification
2017
queueProcessors int
21-
queueBuffer int
2218

2319
logger goengine.Logger
2420
metrics Metrics
21+
22+
notificationQueue NotificationQueuer
2523
}
2624

2725
// ProcessHandler is a func used to trigger a notification but with the addition of providing a Trigger func so
2826
// the original notification can trigger other notifications
2927
ProcessHandler func(context.Context, *ProjectionNotification, ProjectionTrigger) error
3028
)
3129

32-
// newBackgroundProcessor create a new projectionNotificationProcessor
33-
func newBackgroundProcessor(queueProcessors, queueBuffer int, logger goengine.Logger, metrics Metrics) (*projectionNotificationProcessor, error) {
30+
// NewBackgroundProcessor create a new projectionNotificationProcessor
31+
func NewBackgroundProcessor(
32+
queueProcessors,
33+
queueBuffer int,
34+
logger goengine.Logger,
35+
metrics Metrics,
36+
notificationQueue NotificationQueuer,
37+
) (*ProjectionNotificationProcessor, error) {
3438
if queueProcessors <= 0 {
3539
return nil, errors.New("queueProcessors must be greater then zero")
3640
}
@@ -43,17 +47,20 @@ func newBackgroundProcessor(queueProcessors, queueBuffer int, logger goengine.Lo
4347
if metrics == nil {
4448
metrics = NopMetrics
4549
}
50+
if notificationQueue == nil {
51+
notificationQueue = newNotificationQueue(queueBuffer, 0, metrics)
52+
}
4653

47-
return &projectionNotificationProcessor{
48-
queueProcessors: queueProcessors,
49-
queueBuffer: queueBuffer,
50-
logger: logger,
51-
metrics: metrics,
54+
return &ProjectionNotificationProcessor{
55+
queueProcessors: queueProcessors,
56+
logger: logger,
57+
metrics: metrics,
58+
notificationQueue: notificationQueue,
5259
}, nil
5360
}
5461

5562
// Execute starts the background worker and wait for the notification to be executed
56-
func (b *projectionNotificationProcessor) Execute(ctx context.Context, handler ProcessHandler, notification *ProjectionNotification) error {
63+
func (b *ProjectionNotificationProcessor) Execute(ctx context.Context, handler ProcessHandler, notification *ProjectionNotification) error {
5764
// Wrap the processNotification in order to know that the first trigger finished
5865
handler, handlerDone := b.wrapProcessHandlerForSingleRun(handler)
5966

@@ -62,7 +69,7 @@ func (b *projectionNotificationProcessor) Execute(ctx context.Context, handler P
6269
defer stopExecutor()
6370

6471
// Execute a run of the internal.
65-
if err := b.Queue(ctx, nil); err != nil {
72+
if err := b.notificationQueue.Queue(ctx, nil); err != nil {
6673
return err
6774
}
6875

@@ -76,9 +83,8 @@ func (b *projectionNotificationProcessor) Execute(ctx context.Context, handler P
7683
}
7784

7885
// Start starts the background processes that will call the ProcessHandler based on the notification queued by Exec
79-
func (b *projectionNotificationProcessor) Start(ctx context.Context, handler ProcessHandler) func() {
80-
b.done = make(chan struct{})
81-
b.queue = make(chan *ProjectionNotification, b.queueBuffer)
86+
func (b *ProjectionNotificationProcessor) Start(ctx context.Context, handler ProcessHandler) func() {
87+
b.done = b.notificationQueue.Open()
8288

8389
var wg sync.WaitGroup
8490
wg.Add(b.queueProcessors)
@@ -95,37 +101,39 @@ func (b *projectionNotificationProcessor) Start(ctx context.Context, handler Pro
95101
return func() {
96102
close(b.done)
97103
wg.Wait()
98-
close(b.queue)
104+
b.notificationQueue.Close()
99105
}
100106
}
101107

102108
// Queue puts the notification on the queue to be processed
103-
func (b *projectionNotificationProcessor) Queue(ctx context.Context, notification *ProjectionNotification) error {
104-
select {
105-
default:
106-
case <-ctx.Done():
107-
return context.Canceled
108-
case <-b.done:
109-
return errors.New("goengine: unable to queue notification because the processor was stopped")
110-
}
111-
112-
b.metrics.QueueNotification(notification)
113-
114-
b.queue <- notification
115-
return nil
109+
func (b *ProjectionNotificationProcessor) Queue(ctx context.Context, notification *ProjectionNotification) error {
110+
return b.notificationQueue.Queue(ctx, notification)
116111
}
117112

118-
func (b *projectionNotificationProcessor) startProcessor(ctx context.Context, handler ProcessHandler) {
113+
func (b *ProjectionNotificationProcessor) startProcessor(ctx context.Context, handler ProcessHandler) {
114+
ProcessorLoop:
119115
for {
120116
select {
121117
case <-b.done:
122118
return
123119
case <-ctx.Done():
124120
return
125-
case notification := <-b.queue:
121+
case notification := <-b.notificationQueue.Channel():
122+
var queueFunc ProjectionTrigger
123+
if notification == nil {
124+
queueFunc = b.notificationQueue.Queue
125+
} else {
126+
queueFunc = b.notificationQueue.ReQueue
127+
128+
if notification.ValidAfter.After(time.Now()) {
129+
b.notificationQueue.PutBack(notification)
130+
continue ProcessorLoop
131+
}
132+
}
133+
126134
// Execute the notification
127135
b.metrics.StartNotificationProcessing(notification)
128-
if err := handler(ctx, notification, b.Queue); err != nil {
136+
if err := handler(ctx, notification, queueFunc); err != nil {
129137
b.logger.Error("the ProcessHandler produced an error", func(e goengine.LoggerEntry) {
130138
e.Error(err)
131139
e.Any("notification", notification)
@@ -142,7 +150,7 @@ func (b *projectionNotificationProcessor) startProcessor(ctx context.Context, ha
142150

143151
// wrapProcessHandlerForSingleRun returns a wrapped ProcessHandler with a done channel that is closed after the
144152
// provided ProcessHandler it's first call and related messages are finished or when the context is done.
145-
func (b *projectionNotificationProcessor) wrapProcessHandlerForSingleRun(handler ProcessHandler) (ProcessHandler, chan struct{}) {
153+
func (b *ProjectionNotificationProcessor) wrapProcessHandlerForSingleRun(handler ProcessHandler) (ProcessHandler, chan struct{}) {
146154
done := make(chan struct{})
147155

148156
var m sync.Mutex
@@ -169,7 +177,7 @@ func (b *projectionNotificationProcessor) wrapProcessHandlerForSingleRun(handler
169177
close(done)
170178
default:
171179
// No more queued messages to close the run
172-
if len(b.queue) == 0 {
180+
if b.notificationQueue.Empty() {
173181
close(done)
174182
}
175183
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package sql_test
2+
3+
import (
4+
"context"
5+
"sync"
6+
"testing"
7+
"time"
8+
9+
"github.com/golang/mock/gomock"
10+
"github.com/hellofresh/goengine/driver/sql"
11+
mocks "github.com/hellofresh/goengine/mocks/driver/sql"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestStartProcessor(t *testing.T) {
16+
testCases := []struct {
17+
title string
18+
notification func() *sql.ProjectionNotification
19+
queueFunc string
20+
}{
21+
{
22+
"Handle nil notification",
23+
func() *sql.ProjectionNotification {
24+
return nil
25+
},
26+
"Queue",
27+
},
28+
{
29+
"Handle new notification",
30+
func() *sql.ProjectionNotification {
31+
return &sql.ProjectionNotification{
32+
No: 1,
33+
AggregateID: "abc",
34+
}
35+
},
36+
"ReQueue",
37+
},
38+
{
39+
"Handle retried notification",
40+
func() *sql.ProjectionNotification {
41+
return &sql.ProjectionNotification{
42+
No: 1,
43+
AggregateID: "abc",
44+
ValidAfter: time.Now().Add(time.Millisecond * 200),
45+
}
46+
},
47+
"ReQueue",
48+
},
49+
}
50+
51+
for _, testCase := range testCases {
52+
t.Run(testCase.title, func(t *testing.T) {
53+
ctrl := gomock.NewController(t)
54+
55+
nqMock := mocks.NewNotificationQueuer(ctrl)
56+
ctx := context.Background()
57+
notification := testCase.notification()
58+
59+
e := nqMock.EXPECT()
60+
queueCallCount := 0
61+
reQueueCallCount := 0
62+
switch testCase.queueFunc {
63+
case "Queue":
64+
queueCallCount++
65+
case "ReQueue":
66+
reQueueCallCount++
67+
}
68+
69+
e.Queue(gomock.Eq(ctx), gomock.Eq(notification)).Times(queueCallCount)
70+
e.ReQueue(gomock.Eq(ctx), gomock.Eq(notification)).Times(reQueueCallCount)
71+
done := make(chan struct{})
72+
e.Open().DoAndReturn(func() chan struct{} {
73+
return done
74+
}).AnyTimes()
75+
channel := make(chan *sql.ProjectionNotification, 1)
76+
channel <- notification
77+
e.Channel().Return(channel).AnyTimes()
78+
e.PutBack(gomock.Eq(notification)).Do(func(notification *sql.ProjectionNotification) {
79+
channel <- notification
80+
}).AnyTimes()
81+
e.Close().Do(func() {
82+
close(channel)
83+
})
84+
85+
bufferSize := 1
86+
queueProcessorsCount := 1
87+
processor, err := sql.NewBackgroundProcessor(queueProcessorsCount, bufferSize, nil, nil, nqMock)
88+
require.NoError(t, err)
89+
90+
var wg sync.WaitGroup
91+
wg.Add(1)
92+
93+
handler := func(ctx context.Context, notification *sql.ProjectionNotification, queue sql.ProjectionTrigger) error {
94+
err := queue(ctx, notification)
95+
require.NoError(t, err)
96+
97+
wg.Done()
98+
return nil
99+
}
100+
101+
killer := processor.Start(ctx, handler)
102+
103+
defer func() {
104+
wg.Wait()
105+
killer()
106+
ctrl.Finish()
107+
}()
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)