Skip to content

Commit f147728

Browse files
committed
feat: update DynamoMQClient to store visibility timeout as datetime in invisible_until_at and add retryInterval parameter to Consumer
1 parent 6aa0b46 commit f147728

9 files changed

+160
-126
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,14 @@ The DynamoDB table for the DynamoMQ message queue system is designed to efficien
337337
|-------|--------------------|--------|-------------------------------------|
338338
| PK | id | string | A-101 |
339339
| | data | any | any |
340-
| | visibility_timeout | number | 10 |
341340
| | receive_count | number | 1 |
342341
| GSIPK | queue_type | string | STANDARD or DLQ |
343342
| | version | number | 1 |
344343
| | created_at | string | 2006-01-02T15:04:05.999999999Z07:00 |
345344
| | updated_at | string | 2006-01-02T15:04:05.999999999Z07:00 |
346345
| GSISK | sent_at | string | 2006-01-02T15:04:05.999999999Z07:00 |
347346
| | received_at | string | 2006-01-02T15:04:05.999999999Z07:00 |
347+
| | invisible_until_at | string | 2006-01-02T15:04:05.999999999Z07:00 |
348348

349349
**PK (Primary Key)** `ID`: A unique identifier for each message, such as 'A-101'. This is a string value that facilitates the retrieval and management of messages.
350350

@@ -354,12 +354,12 @@ The DynamoDB table for the DynamoMQ message queue system is designed to efficien
354354

355355
**Attributes**: These are the various properties associated with each message:
356356
- `data`: This attribute holds the content of the message and can be of any type.
357-
- `isibility_timeout`: The new value for the message's visibility timeout (in seconds).
358357
- `receive_count`: A numerical count of how many times the message has been retrieved from the queue.
359358
- `version`: A number that can be used for optimistic locking and to ensure that the message is not being concurrently modified.
360359
- `created_at`: The date and time when the message was created. ISO 8601 format.
361360
- `updated_at`: The date and time when the message was last updated. ISO 8601 format.
362361
- `received_at`: The timestamp when the message was last viewed without being altered. ISO 8601 format.
362+
- `invisible_until_at`: The timestamp indicating when the message becomes visible in the queue for processing. ISO 8601 format.
363363

364364
### Data Transition
365365

client.go

+18-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
DefaultQueueingIndexName = "dynamo-mq-index-queue_type-sent_at"
2020
DefaultRetryMaxAttempts = 10
2121
DefaultVisibilityTimeoutInSeconds = 30
22+
DefaultVisibilityTimeout = DefaultVisibilityTimeoutInSeconds * time.Second
2223
DefaultMaxListMessages = 10
2324
DefaultQueryLimit = 250
2425
)
@@ -206,6 +207,9 @@ func (c *client[T]) ReceiveMessage(ctx context.Context, params *ReceiveMessageIn
206207
if params.QueueType == "" {
207208
params.QueueType = QueueTypeStandard
208209
}
210+
if params.VisibilityTimeout <= 0 {
211+
params.VisibilityTimeout = DefaultVisibilityTimeoutInSeconds
212+
}
209213

210214
selected, err := c.selectMessage(ctx, params)
211215
if err != nil {
@@ -287,7 +291,7 @@ func (c *client[T]) processQueryResult(params *ReceiveMessageInput, queryResult
287291
return nil, UnmarshalingAttributeError{Cause: err}
288292
}
289293

290-
if err := message.markAsProcessing(c.clock.Now(), params.VisibilityTimeout); err == nil {
294+
if err := message.markAsProcessing(c.clock.Now(), secToDur(params.VisibilityTimeout)); err == nil {
291295
selected = &message
292296
break
293297
}
@@ -303,9 +307,9 @@ func (c *client[T]) processSelectedMessage(ctx context.Context, message *Message
303307
WithUpdate(expression.
304308
Add(expression.Name("version"), expression.Value(1)).
305309
Add(expression.Name("receive_count"), expression.Value(1)).
306-
Set(expression.Name("visibility_timeout"), expression.Value(message.VisibilityTimeout)).
307310
Set(expression.Name("updated_at"), expression.Value(message.UpdatedAt)).
308-
Set(expression.Name("received_at"), expression.Value(message.ReceivedAt))).
311+
Set(expression.Name("received_at"), expression.Value(message.ReceivedAt)).
312+
Set(expression.Name("invisible_until_at"), expression.Value(message.InvisibleUntilAt))).
309313
WithCondition(expression.Name("version").Equal(expression.Value(message.Version)))
310314
expr, err := c.buildExpression(builder)
311315
if err != nil {
@@ -343,12 +347,12 @@ func (c *client[T]) ChangeMessageVisibility(ctx context.Context, params *ChangeM
343347
return &ChangeMessageVisibilityOutput[T]{}, &IDNotFoundError{}
344348
}
345349
message := retrieved.Message
346-
message.changeVisibilityTimeout(c.clock.Now(), params.VisibilityTimeout)
350+
message.changeVisibility(c.clock.Now(), secToDur(params.VisibilityTimeout))
347351
builder := expression.NewBuilder().
348352
WithUpdate(expression.
349353
Add(expression.Name("version"), expression.Value(1)).
350354
Set(expression.Name("updated_at"), expression.Value(message.UpdatedAt)).
351-
Set(expression.Name("visibility_timeout"), expression.Value(message.VisibilityTimeout))).
355+
Set(expression.Name("invisible_until_at"), expression.Value(message.InvisibleUntilAt))).
352356
WithCondition(expression.Name("version").Equal(expression.Value(message.Version)))
353357
expr, err := c.buildExpression(builder)
354358
if err != nil {
@@ -434,12 +438,12 @@ func (c *client[T]) MoveMessageToDLQ(ctx context.Context, params *MoveMessageToD
434438
builder := expression.NewBuilder().
435439
WithUpdate(expression.
436440
Add(expression.Name("version"), expression.Value(1)).
437-
Set(expression.Name("visibility_timeout"), expression.Value(message.VisibilityTimeout)).
438441
Set(expression.Name("receive_count"), expression.Value(message.ReceiveCount)).
439442
Set(expression.Name("queue_type"), expression.Value(message.QueueType)).
440443
Set(expression.Name("updated_at"), expression.Value(message.UpdatedAt)).
441444
Set(expression.Name("sent_at"), expression.Value(message.SentAt)).
442-
Set(expression.Name("received_at"), expression.Value(message.SentAt))).
445+
Set(expression.Name("received_at"), expression.Value(message.SentAt)).
446+
Set(expression.Name("invisible_until_at"), expression.Value(message.InvisibleUntilAt))).
443447
WithCondition(expression.Name("version").Equal(expression.Value(message.Version)))
444448
expr, err := c.buildExpression(builder)
445449
if err != nil {
@@ -493,15 +497,15 @@ func (c *client[T]) RedriveMessage(ctx context.Context, params *RedriveMessageIn
493497
).Set(
494498
expression.Name("queue_type"),
495499
expression.Value(message.QueueType),
496-
).Set(
497-
expression.Name("visibility_timeout"),
498-
expression.Value(message.VisibilityTimeout),
499500
).Set(
500501
expression.Name("updated_at"),
501502
expression.Value(message.UpdatedAt),
502503
).Set(
503504
expression.Name("sent_at"),
504505
expression.Value(message.SentAt),
506+
).Set(
507+
expression.Name("invisible_until_at"),
508+
expression.Value(message.InvisibleUntilAt),
505509
)).
506510
WithCondition(expression.Name("version").
507511
Equal(expression.Value(message.Version)))
@@ -845,6 +849,10 @@ func handleDynamoDBError(err error) error {
845849
return DynamoDBAPIError{Cause: err}
846850
}
847851

852+
func secToDur(sec int) time.Duration {
853+
return time.Duration(sec) * time.Second
854+
}
855+
848856
type Status string
849857

850858
const (

client_test.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ func testDynamoMQClientReceiveMessageSequence(t *testing.T, useFIFO bool) {
347347

348348
for i, want := range wants {
349349
result, err := client.ReceiveMessage(ctx, &dynamomq.ReceiveMessageInput{
350+
QueueType: dynamomq.QueueTypeStandard,
350351
VisibilityTimeout: dynamomq.DefaultVisibilityTimeoutInSeconds,
351352
})
352353
test.AssertError(t, err, nil, fmt.Sprintf("ReceiveMessage() [%d-1]", i))
@@ -383,10 +384,11 @@ func TestDynamoMQClientReceiveMessageNotUseFIFO(t *testing.T) {
383384
testDynamoMQClientReceiveMessageSequence(t, false)
384385
}
385386

386-
func TestDynamoMQClientUpdateMessageAsVisible(t *testing.T) {
387+
func TestDynamoMQClientChangeMessageVisibility(t *testing.T) {
387388
t.Parallel()
388389
type args struct {
389-
id string
390+
id string
391+
visibilityTimeout int
390392
}
391393
now := test.DefaultTestDate.Add(10 * time.Second)
392394
tests := []ClientTestCase[args, *dynamomq.ChangeMessageVisibilityOutput[test.MessageData]]{
@@ -397,7 +399,8 @@ func TestDynamoMQClientUpdateMessageAsVisible(t *testing.T) {
397399
T: now,
398400
},
399401
args: args{
400-
id: "A-101",
402+
id: "A-101",
403+
visibilityTimeout: -1,
401404
},
402405
want: &dynamomq.ChangeMessageVisibilityOutput[test.MessageData]{
403406
Result: &dynamomq.Result{
@@ -408,7 +411,9 @@ func TestDynamoMQClientUpdateMessageAsVisible(t *testing.T) {
408411
},
409412
Message: func() *dynamomq.Message[test.MessageData] {
410413
m := NewTestMessageItemAsProcessing("A-101", now)
411-
MarkAsReady(m, now)
414+
ts := clock.FormatRFC3339Nano(now)
415+
m.UpdatedAt = ts
416+
m.InvisibleUntilAt = clock.FormatRFC3339Nano(now.Add(-1 * time.Second))
412417
m.Version = 2
413418
return m
414419
}(),
@@ -418,7 +423,8 @@ func TestDynamoMQClientUpdateMessageAsVisible(t *testing.T) {
418423
runTestsParallel[args, *dynamomq.ChangeMessageVisibilityOutput[test.MessageData]](t, "ChangeMessageVisibility()", tests,
419424
func(client dynamomq.Client[test.MessageData], args args) (*dynamomq.ChangeMessageVisibilityOutput[test.MessageData], error) {
420425
return client.ChangeMessageVisibility(context.Background(), &dynamomq.ChangeMessageVisibilityInput{
421-
ID: args.id,
426+
ID: args.id,
427+
VisibilityTimeout: args.visibilityTimeout,
422428
})
423429
})
424430
}

consumer.go

+60-35
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import (
1111
)
1212

1313
const (
14-
defaultPollingInterval = time.Second
15-
defaultMaximumReceives = 0 // unlimited
16-
defaultQueueType = QueueTypeStandard
17-
defaultConcurrency = 3
14+
defaultPollingInterval = time.Second
15+
defaultMaximumReceives = 0 // unlimited
16+
defaultRetryIntervalInSeconds = 1
17+
defaultQueueType = QueueTypeStandard
18+
defaultConcurrency = 3
1819
)
1920

2021
func WithPollingInterval(pollingInterval time.Duration) func(o *ConsumerOptions) {
@@ -35,6 +36,18 @@ func WithMaximumReceives(maximumReceives int) func(o *ConsumerOptions) {
3536
}
3637
}
3738

39+
func WithVisibilityTimeout(sec int) func(o *ConsumerOptions) {
40+
return func(o *ConsumerOptions) {
41+
o.VisibilityTimeout = sec
42+
}
43+
}
44+
45+
func WithRetryInterval(sec int) func(o *ConsumerOptions) {
46+
return func(o *ConsumerOptions) {
47+
o.RetryInterval = sec
48+
}
49+
}
50+
3851
func WithQueueType(queueType QueueType) func(o *ConsumerOptions) {
3952
return func(o *ConsumerOptions) {
4053
o.QueueType = queueType
@@ -54,10 +67,12 @@ func WithOnShutdown(onShutdown []func()) func(o *ConsumerOptions) {
5467
}
5568

5669
type ConsumerOptions struct {
57-
PollingInterval time.Duration
58-
Concurrency int
59-
MaximumReceives int
60-
QueueType QueueType
70+
PollingInterval time.Duration
71+
Concurrency int
72+
MaximumReceives int
73+
VisibilityTimeout int
74+
RetryInterval int
75+
QueueType QueueType
6176
// errorLog specifies an optional logger for errors accepting
6277
// connections, unexpected behavior from handlers, and
6378
// underlying FileSystem errors.
@@ -68,28 +83,32 @@ type ConsumerOptions struct {
6883

6984
func NewConsumer[T any](client Client[T], processor MessageProcessor[T], opts ...func(o *ConsumerOptions)) *Consumer[T] {
7085
o := &ConsumerOptions{
71-
PollingInterval: defaultPollingInterval,
72-
Concurrency: defaultConcurrency,
73-
MaximumReceives: defaultMaximumReceives,
74-
QueueType: defaultQueueType,
86+
PollingInterval: defaultPollingInterval,
87+
Concurrency: defaultConcurrency,
88+
MaximumReceives: defaultMaximumReceives,
89+
VisibilityTimeout: DefaultVisibilityTimeoutInSeconds,
90+
RetryInterval: defaultRetryIntervalInSeconds,
91+
QueueType: defaultQueueType,
7592
}
7693
for _, opt := range opts {
7794
opt(o)
7895
}
7996
return &Consumer[T]{
80-
client: client,
81-
messageProcessor: processor,
82-
pollingInterval: o.PollingInterval,
83-
concurrency: o.Concurrency,
84-
maximumReceives: o.MaximumReceives,
85-
queueType: o.QueueType,
86-
errorLog: o.ErrorLog,
87-
onShutdown: o.OnShutdown,
88-
inShutdown: 0,
89-
mu: sync.Mutex{},
90-
activeMessages: make(map[*Message[T]]struct{}),
91-
activeMessagesWG: sync.WaitGroup{},
92-
doneChan: make(chan struct{}),
97+
client: client,
98+
messageProcessor: processor,
99+
pollingInterval: o.PollingInterval,
100+
concurrency: o.Concurrency,
101+
maximumReceives: o.MaximumReceives,
102+
visibilityTimeout: o.VisibilityTimeout,
103+
retryInterval: o.RetryInterval,
104+
queueType: o.QueueType,
105+
errorLog: o.ErrorLog,
106+
onShutdown: o.OnShutdown,
107+
inShutdown: 0,
108+
mu: sync.Mutex{},
109+
activeMessages: make(map[*Message[T]]struct{}),
110+
activeMessagesWG: sync.WaitGroup{},
111+
doneChan: make(chan struct{}),
93112
}
94113
}
95114

@@ -104,14 +123,16 @@ func (f MessageProcessorFunc[T]) Process(msg *Message[T]) error {
104123
}
105124

106125
type Consumer[T any] struct {
107-
client Client[T]
108-
messageProcessor MessageProcessor[T]
109-
concurrency int
110-
pollingInterval time.Duration
111-
maximumReceives int
112-
queueType QueueType
113-
errorLog *log.Logger
114-
onShutdown []func()
126+
client Client[T]
127+
messageProcessor MessageProcessor[T]
128+
concurrency int
129+
pollingInterval time.Duration
130+
maximumReceives int
131+
visibilityTimeout int
132+
retryInterval int
133+
queueType QueueType
134+
errorLog *log.Logger
135+
onShutdown []func()
115136

116137
inShutdown int32
117138
mu sync.Mutex
@@ -138,7 +159,7 @@ func (c *Consumer[T]) StartConsuming() error {
138159
ctx := context.Background()
139160
r, err := c.client.ReceiveMessage(ctx, &ReceiveMessageInput{
140161
QueueType: c.queueType,
141-
VisibilityTimeout: DefaultVisibilityTimeoutInSeconds,
162+
VisibilityTimeout: c.visibilityTimeout,
142163
})
143164
if err != nil {
144165
if c.shuttingDown() {
@@ -187,7 +208,11 @@ func (c *Consumer[T]) shouldRetry(msg *Message[T]) bool {
187208
}
188209

189210
func (c *Consumer[T]) retryMessage(ctx context.Context, msg *Message[T]) {
190-
if _, err := c.client.ChangeMessageVisibility(ctx, &ChangeMessageVisibilityInput{ID: msg.ID}); err != nil {
211+
in := &ChangeMessageVisibilityInput{
212+
ID: msg.ID,
213+
VisibilityTimeout: c.retryInterval,
214+
}
215+
if _, err := c.client.ChangeMessageVisibility(ctx, in); err != nil {
191216
c.logf("DynamoMQ: Failed to update a message as visible. %s", err)
192217
}
193218
}

consumer_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ func TestConsumerStartConsuming(t *testing.T) {
127127
dynamomq.WithPollingInterval(0),
128128
dynamomq.WithConcurrency(3),
129129
dynamomq.WithMaximumReceives(tt.MaximumReceives),
130+
dynamomq.WithVisibilityTimeout(30),
131+
dynamomq.WithRetryInterval(1),
130132
dynamomq.WithQueueType(tt.QueueType),
131133
dynamomq.WithErrorLog(log.New(os.Stderr, "", 0)),
132134
dynamomq.WithOnShutdown([]func(){}))

dynamomq_test.go

+3-9
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,21 @@ import (
88
"github.com/vvatanabe/dynamomq/internal/test"
99
)
1010

11-
func MarkAsReady[T any](m *dynamomq.Message[T], now time.Time) {
12-
ts := clock.FormatRFC3339Nano(now)
13-
m.VisibilityTimeout = 0
14-
m.UpdatedAt = ts
15-
}
16-
1711
func MarkAsProcessing[T any](m *dynamomq.Message[T], now time.Time) {
1812
ts := clock.FormatRFC3339Nano(now)
19-
m.VisibilityTimeout = dynamomq.DefaultVisibilityTimeoutInSeconds
2013
m.UpdatedAt = ts
2114
m.ReceivedAt = ts
15+
m.InvisibleUntilAt = clock.FormatRFC3339Nano(now.Add(dynamomq.DefaultVisibilityTimeout))
2216
}
2317

2418
func MarkAsMovedToDLQ[T any](m *dynamomq.Message[T], now time.Time) {
2519
ts := clock.FormatRFC3339Nano(now)
2620
m.QueueType = dynamomq.QueueTypeDLQ
27-
m.VisibilityTimeout = 0
2821
m.ReceiveCount = 0
2922
m.UpdatedAt = ts
3023
m.SentAt = ts
3124
m.ReceivedAt = ""
25+
m.InvisibleUntilAt = ""
3226
}
3327

3428
func NewTestMessageItemAsReady(id string, now time.Time) *dynamomq.Message[test.MessageData] {
@@ -53,7 +47,7 @@ func NewMessageFromReadyToProcessing(id string,
5347
MarkAsProcessing(m, processingTime)
5448
m.Version = 2
5549
m.ReceiveCount = 1
56-
m.VisibilityTimeout = dynamomq.DefaultVisibilityTimeoutInSeconds
50+
m.InvisibleUntilAt = clock.FormatRFC3339Nano(processingTime.Add(dynamomq.DefaultVisibilityTimeout))
5751
r := &dynamomq.ReceiveMessageOutput[test.MessageData]{
5852
Result: &dynamomq.Result{
5953
ID: m.ID,

0 commit comments

Comments
 (0)