@@ -18,6 +18,8 @@ package sqs
1818
1919import (
2020 "context"
21+ "errors"
22+ "fmt"
2123 "math/rand"
2224 "time"
2325
@@ -27,29 +29,75 @@ import (
2729 "github.com/aws/aws-sdk-go-v2/service/sqs/types"
2830)
2931
32+ const (
33+ defaultMinBackoff = 1 * time .Second
34+ defaultMaxBackoff = 60 * time .Second
35+ )
36+
3037type SQSClient struct {
31- service * sqs.Client
32- QueueURL string
38+ service * sqs.Client
39+ config * SQSConfig
40+ currentBackoff time.Duration
41+ errHandle func (error )
3342}
3443
35- func NewClient (region , queueURL string ) (* SQSClient , error ) {
36- cfg , err := config .LoadDefaultConfig (context .TODO (), config .WithRegion (region ))
44+ type SQSConfig struct {
45+ QueueURL string
46+ AWSRegion string
47+ MaxBackoff time.Duration
48+ MinBackoff time.Duration
49+ ErrHandle func (error )
50+ }
51+
52+ func NewClient (sqsCfg * SQSConfig ) (* SQSClient , error ) {
53+ err := validateSQSConfig (sqsCfg )
54+ if err != nil {
55+ return nil , err
56+ }
57+ cfg , err := config .LoadDefaultConfig (context .TODO (),
58+ config .WithRegion (sqsCfg .AWSRegion ),
59+ config .WithRetryMaxAttempts (3 ),
60+ config .WithRetryMode (aws .RetryModeStandard ),
61+ )
3762 if err != nil {
3863 return nil , err
3964 }
4065
4166 svc := sqs .NewFromConfig (cfg )
4267
4368 return & SQSClient {
44- service : svc ,
45- QueueURL : queueURL ,
69+ service : svc ,
70+ config : sqsCfg ,
71+ currentBackoff : sqsCfg .MinBackoff ,
72+ errHandle : sqsCfg .ErrHandle ,
4673 }, nil
4774}
4875
76+ func validateSQSConfig (cfg * SQSConfig ) error {
77+ if cfg .QueueURL == "" {
78+ return errors .New ("queueURL is required" )
79+ }
80+ if cfg .AWSRegion == "" {
81+ return errors .New ("region is required" )
82+ }
83+ if cfg .MaxBackoff <= 0 {
84+ cfg .MaxBackoff = defaultMaxBackoff
85+ }
86+ if cfg .MinBackoff <= 0 {
87+ cfg .MinBackoff = defaultMinBackoff
88+ }
89+ if cfg .ErrHandle == nil {
90+ cfg .ErrHandle = func (err error ) {
91+ return
92+ }
93+ }
94+ return nil
95+ }
96+
4997func (c * SQSClient ) SendMessage (ctx context.Context , messageBody string ) (* sqs.SendMessageOutput , error ) {
5098 result , err := c .service .SendMessage (ctx , & sqs.SendMessageInput {
5199 MessageBody : aws .String (messageBody ),
52- QueueUrl : aws .String (c .QueueURL ),
100+ QueueUrl : aws .String (c .config . QueueURL ),
53101 })
54102
55103 if err != nil {
@@ -61,7 +109,7 @@ func (c *SQSClient) SendMessage(ctx context.Context, messageBody string) (*sqs.S
61109
62110func (c * SQSClient ) DeleteMessage (ctx context.Context , receiptHandle string ) (* sqs.DeleteMessageOutput , error ) {
63111 result , err := c .service .DeleteMessage (ctx , & sqs.DeleteMessageInput {
64- QueueUrl : aws .String (c .QueueURL ),
112+ QueueUrl : aws .String (c .config . QueueURL ),
65113 ReceiptHandle : aws .String (receiptHandle ),
66114 })
67115
@@ -74,7 +122,7 @@ func (c *SQSClient) DeleteMessage(ctx context.Context, receiptHandle string) (*s
74122
75123func (c * SQSClient ) ReceiveMessages (ctx context.Context , maxMessages int32 , waitTimeSeconds int32 ) ([]types.Message , error ) {
76124 result , err := c .service .ReceiveMessage (ctx , & sqs.ReceiveMessageInput {
77- QueueUrl : aws .String (c .QueueURL ),
125+ QueueUrl : aws .String (c .config . QueueURL ),
78126 MaxNumberOfMessages : maxMessages ,
79127 WaitTimeSeconds : waitTimeSeconds ,
80128 })
@@ -89,10 +137,6 @@ func (c *SQSClient) ReceiveMessages(ctx context.Context, maxMessages int32, wait
89137// CalculateNextBackoff computes the next backoff duration using an exponential
90138// strategy with jitter to prevent synchronized polling.
91139//
92- // Parameters:
93- // - current: The current backoff duration.
94- // - max: The maximum allowable backoff duration.
95- //
96140// Returns:
97141//
98142// A time.Duration representing the next backoff period.
@@ -101,18 +145,91 @@ func (c *SQSClient) ReceiveMessages(ctx context.Context, maxMessages int32, wait
101145// (subtracting up to 25% of the doubled value) to prevent multiple instances
102146// from synchronizing their polling cycles. The result is capped at the specified
103147// maximum duration.
104- func (c * SQSClient ) CalculateNextBackoff ( current , maxDuration time. Duration ) time.Duration {
148+ func (c * SQSClient ) calculateNextBackoff ( ) time.Duration {
105149 // Double the current backoff
106- next := current * 2
150+ next := c . currentBackoff * 2
107151
108152 // Apply jitter (randomness) to prevent synchronized polling
109153 jitter := time .Duration (rand .Int63n (int64 (next / 4 )))
110154 next -= jitter
111155
112156 // Ensure we don't exceed the maximum
113- if next > maxDuration {
114- return maxDuration
157+ if next > c .config .MaxBackoff {
158+ c .currentBackoff = c .config .MaxBackoff
159+ } else {
160+ c .currentBackoff = next
115161 }
116162
117- return next
163+ return c .currentBackoff
164+ }
165+
166+ func (c * SQSClient ) resetBackoff () {
167+ c .currentBackoff = c .config .MinBackoff
168+ }
169+
170+ func (c * SQSClient ) PollForMessages (ctx context.Context , handler func (message * types.Message ) error ) {
171+ errChan := make (chan error , 100 )
172+ pollCtx , cancel := context .WithCancel (ctx )
173+ defer cancel ()
174+ go func () {
175+ defer close (errChan )
176+ for {
177+ select {
178+ case <- pollCtx .Done ():
179+ return
180+ case err := <- errChan :
181+ if err != nil && c .errHandle != nil {
182+ c .errHandle (err )
183+ }
184+ }
185+ }
186+ }()
187+
188+ var backoff time.Duration
189+
190+ for {
191+ select {
192+ case <- ctx .Done ():
193+ // Context was canceled - exit gracefully without error
194+ return
195+ default :
196+ messages , err := c .ReceiveMessages (ctx , 10 , 20 )
197+ if err != nil {
198+ // Check if the error is due to context cancellation
199+ if errors .Is (err , context .Canceled ) || errors .Is (err , context .DeadlineExceeded ) {
200+ // Context canceled while receiving messages, stopping polling
201+ return
202+ }
203+ errChan <- fmt .Errorf ("failed to receive messages: %w" , err )
204+
205+ // Increase backoff on errors
206+ backoff = c .calculateNextBackoff ()
207+ time .Sleep (backoff )
208+ continue
209+ }
210+
211+ if len (messages ) == 0 {
212+ // No messages found, increase the backoff
213+ backoff = c .calculateNextBackoff ()
214+ time .Sleep (backoff )
215+ } else {
216+ // Messages found, reset backoff
217+ c .resetBackoff ()
218+
219+ for _ , msg := range messages {
220+ err = handler (& msg )
221+ if err != nil {
222+ continue
223+ }
224+
225+ _ , err = c .DeleteMessage (ctx , * msg .ReceiptHandle )
226+ if err != nil {
227+ errChan <- fmt .Errorf ("failed to delete message %s from queue %s: %w" , * msg .MessageId ,
228+ c .config .QueueURL , err )
229+ continue
230+ }
231+ }
232+ }
233+ }
234+ }
118235}
0 commit comments