|
@@ -3,6 +3,7 @@ package redriver
|
|
import (
|
|
import (
|
|
"context"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/json"
|
|
|
|
+ "errors"
|
|
"fmt"
|
|
"fmt"
|
|
"io"
|
|
"io"
|
|
"log"
|
|
"log"
|
|
@@ -19,11 +20,18 @@ import (
|
|
)
|
|
)
|
|
|
|
|
|
const (
|
|
const (
|
|
|
|
+ // BatchMax defines the maximum number of message usable in a batch operation, including redriving.
|
|
|
|
+ BatchMax = 10
|
|
|
|
+
|
|
// MessageSystemAttributeNameDeadLetterQueueSourceArn is an undocumented
|
|
// MessageSystemAttributeNameDeadLetterQueueSourceArn is an undocumented
|
|
// types.Message attribute, used by the SQS console to support redrive.
|
|
// types.Message attribute, used by the SQS console to support redrive.
|
|
MessageSystemAttributeNameDeadLetterQueueSourceArn types.MessageSystemAttributeName = "DeadLetterQueueSourceArn"
|
|
MessageSystemAttributeNameDeadLetterQueueSourceArn types.MessageSystemAttributeName = "DeadLetterQueueSourceArn"
|
|
)
|
|
)
|
|
|
|
|
|
|
|
+var (
|
|
|
|
+ ErrBatchTooBig = fmt.Errorf("operation requested on more than %d items", BatchMax)
|
|
|
|
+)
|
|
|
|
+
|
|
type ItemsKeys struct {
|
|
type ItemsKeys struct {
|
|
MessageID string
|
|
MessageID string
|
|
ReceiptHandle string
|
|
ReceiptHandle string
|
|
@@ -46,6 +54,7 @@ type Redriver interface {
|
|
}
|
|
}
|
|
|
|
|
|
type redriver struct {
|
|
type redriver struct {
|
|
|
|
+ VTO int32
|
|
Wait int32
|
|
Wait int32
|
|
|
|
|
|
io.Writer
|
|
io.Writer
|
|
@@ -393,30 +402,69 @@ func (r *redriver) Purge(ctx context.Context, qName string) error {
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
-// RedriveItems sends the selected message back to their respective source queue.
|
|
|
|
|
|
+// RedriveItems redrives the selected message back to their respective source queue,
|
|
|
|
+// removing them from the DLQ once they have been sent.
|
|
//
|
|
//
|
|
// Since a queue can act as a DLQ for more than one source queue, the messages
|
|
// Since a queue can act as a DLQ for more than one source queue, the messages
|
|
// sends are grouped by source queue.
|
|
// sends are grouped by source queue.
|
|
-func (r *redriver) RedriveItems(ctx context.Context, qName string, messages []Message) error {
|
|
|
|
- qURLs := make(map[string][]Message, 1) // In most cases, only a single queue will be used.
|
|
|
|
|
|
+func (r *redriver) RedriveItems(ctx context.Context, dlqName string, messages []Message) error {
|
|
|
|
+ sqURLs := make(map[string][]Message, 1) // In most cases, only a single queue will be used.
|
|
for _, message := range messages {
|
|
for _, message := range messages {
|
|
sARN := message.Attributes.DeadLetterQueueSourceARN
|
|
sARN := message.Attributes.DeadLetterQueueSourceARN
|
|
sURL, err := URLFromARNString(sARN)
|
|
sURL, err := URLFromARNString(sARN)
|
|
if err != nil {
|
|
if err != nil {
|
|
return fmt.Errorf("failed resolving source ARN %q to URL: %v", sARN, err)
|
|
return fmt.Errorf("failed resolving source ARN %q to URL: %v", sARN, err)
|
|
}
|
|
}
|
|
- qURLs[sURL] = append(qURLs[sURL], message)
|
|
|
|
|
|
+ sqURLs[sURL] = append(sqURLs[sURL], message)
|
|
}
|
|
}
|
|
|
|
|
|
- for qURL, messages := range qURLs {
|
|
|
|
- if err := r.redriveQueueMessages(ctx, qURL, messages); err != nil {
|
|
|
|
|
|
+ for qURL, messages := range sqURLs {
|
|
|
|
+ if err := r.redriveQueueMessages(ctx, dlqName, qURL, messages); err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
-func (r *redriver) redriveQueueMessages(ctx context.Context, qURL string, messages []Message) error {
|
|
|
|
|
|
+// redriveQueueMessages handles message redriving for messages in a single queue.
|
|
|
|
+func (r *redriver) redriveQueueMessages(ctx context.Context, dlqName string, qURL string, messages []Message) error {
|
|
|
|
+ if len(messages) > BatchMax {
|
|
|
|
+ return ErrBatchTooBig
|
|
|
|
+ }
|
|
|
|
+ qui := &sqs.GetQueueUrlInput{QueueName: &dlqName}
|
|
|
|
+ dlqURL, err := r.GetQueueUrl(ctx, qui)
|
|
|
|
+ if err != nil || dlqURL == nil {
|
|
|
|
+ return fmt.Errorf("failed getting URL for queue %q: %w", dlqName, err)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Hide messages to prevent other consumers from seeing them and generating duplicates.
|
|
|
|
+ fatal, nontafal := r.hideQueueMessages(ctx, "", *dlqURL.QueueUrl, messages)
|
|
|
|
+ if fatal != nil {
|
|
|
|
+ return fmt.Errorf("failed hiding messages during redrive towards queue %q: %w", qURL, fatal)
|
|
|
|
+ }
|
|
|
|
+ if nontafal != nil {
|
|
|
|
+ log.Printf("Redrive nonfatal error hiding messages on queue %q: %v", dlqName, nontafal)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Send the messages back to their source queue.
|
|
|
|
+ if err := r.resendQueueMessages(ctx, qURL, messages); err != nil {
|
|
|
|
+ return fmt.Errorf("failed sending messages back to queue %q: %w", qURL, err)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Delete them from the DLQ.
|
|
|
|
+ keys := make([]ItemsKeys, len(messages))
|
|
|
|
+ for i, m := range messages {
|
|
|
|
+ keys[i] = m.Keys()
|
|
|
|
+ }
|
|
|
|
+ if err := r.DeleteItems(ctx, dlqName, keys); err != nil {
|
|
|
|
+ return fmt.Errorf("failed deleting messages already redriven from DLQ %s to queue %q: beware of duplicates: %w",
|
|
|
|
+ dlqName, qURL, err)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (r *redriver) resendQueueMessages(ctx context.Context, qURL string, messages []Message) error {
|
|
smbre := make([]types.SendMessageBatchRequestEntry, len(messages))
|
|
smbre := make([]types.SendMessageBatchRequestEntry, len(messages))
|
|
for i, m := range messages {
|
|
for i, m := range messages {
|
|
m.MessageAttributes["previous-message-id"] = m.MessageId
|
|
m.MessageAttributes["previous-message-id"] = m.MessageId
|
|
@@ -425,14 +473,11 @@ func (r *redriver) redriveQueueMessages(ctx context.Context, qURL string, messag
|
|
return fmt.Errorf("failed converting message attributes for message %s on queue %q: %v",
|
|
return fmt.Errorf("failed converting message attributes for message %s on queue %q: %v",
|
|
m.MessageId, qURL, err)
|
|
m.MessageId, qURL, err)
|
|
}
|
|
}
|
|
|
|
+
|
|
smbre[i] = types.SendMessageBatchRequestEntry{
|
|
smbre[i] = types.SendMessageBatchRequestEntry{
|
|
- Id: aws.String(strconv.Itoa(i)),
|
|
|
|
- MessageBody: &m.Body,
|
|
|
|
- DelaySeconds: 0,
|
|
|
|
- MessageAttributes: mav,
|
|
|
|
- MessageDeduplicationId: nil,
|
|
|
|
- MessageGroupId: nil,
|
|
|
|
- MessageSystemAttributes: nil,
|
|
|
|
|
|
+ Id: aws.String(strconv.Itoa(i)),
|
|
|
|
+ MessageBody: &m.Body,
|
|
|
|
+ MessageAttributes: mav,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
smbi := sqs.SendMessageBatchInput{
|
|
smbi := sqs.SendMessageBatchInput{
|
|
@@ -444,16 +489,71 @@ func (r *redriver) redriveQueueMessages(ctx context.Context, qURL string, messag
|
|
return fmt.Errorf("failed sending messages to queue %q: %v",
|
|
return fmt.Errorf("failed sending messages to queue %q: %v",
|
|
qURL, err)
|
|
qURL, err)
|
|
}
|
|
}
|
|
- log.Printf("%#v", smbo)
|
|
|
|
- return nil
|
|
|
|
|
|
+ if len(smbo.Failed) == 0 {
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ errs := make([]error, len(smbo.Failed))
|
|
|
|
+ for _, err := range smbo.Failed {
|
|
|
|
+ msg := fmt.Sprintf("ID: %s, Code: %s, Message: %s", *err.Id, *err.Code, *err.Message)
|
|
|
|
+ if err.SenderFault {
|
|
|
|
+ msg += " (sender fault)"
|
|
|
|
+ }
|
|
|
|
+ errs = append(errs, errors.New(msg))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return fmt.Errorf("partial redrive: failed re-sending %d/%d messages, %v",
|
|
|
|
+ len(smbo.Failed), len(smbi.Entries), errs)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (r *redriver) hideQueueMessages(ctx context.Context, dlqName string, qURL string, messages []Message) (fatal, nonfatal error) {
|
|
|
|
+ cmvbre := make([]types.ChangeMessageVisibilityBatchRequestEntry, len(messages))
|
|
|
|
+ for i, m := range messages {
|
|
|
|
+ cmvbre[i] = types.ChangeMessageVisibilityBatchRequestEntry{
|
|
|
|
+ Id: aws.String(strconv.Itoa(i)),
|
|
|
|
+ ReceiptHandle: aws.String(m.ReceiptHandle),
|
|
|
|
+ VisibilityTimeout: r.VTO,
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ cmvbi := sqs.ChangeMessageVisibilityBatchInput{
|
|
|
|
+ Entries: cmvbre,
|
|
|
|
+ QueueUrl: aws.String(qURL),
|
|
|
|
+ }
|
|
|
|
+ cmvbo, err := r.ChangeMessageVisibilityBatch(ctx, &cmvbi)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return fmt.Errorf("failed hiding request on DLQ %q: %w", dlqName, err), nil
|
|
|
|
+ }
|
|
|
|
+ switch len(cmvbo.Failed) {
|
|
|
|
+ case len(cmvbi.Entries):
|
|
|
|
+ // No message made it: abort.
|
|
|
|
+ return fmt.Errorf("failed hiding all %d messages on DLQ %q", len(cmvbi.Entries), dlqName), nil
|
|
|
|
+ case 0:
|
|
|
|
+ return nil, nil // All well
|
|
|
|
+ default:
|
|
|
|
+ errs := make([]error, len(cmvbo.Failed))
|
|
|
|
+ for _, err := range cmvbo.Failed {
|
|
|
|
+ msg := fmt.Sprintf("ID: %s, Code: %s, Message: %s", *err.Id, *err.Code, *err.Message)
|
|
|
|
+ if err.SenderFault {
|
|
|
|
+ msg += " (sender fault)"
|
|
|
|
+ }
|
|
|
|
+ errs = append(errs, errors.New(msg))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Some message made it, crossing fingers.
|
|
|
|
+ return nil, fmt.Errorf("failed hiding %d/%d messages, %v",
|
|
|
|
+ len(cmvbo.Failed), len(cmvbi.Entries), errs)
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
func RedriverService(dic *izidic.Container) (any, error) {
|
|
func RedriverService(dic *izidic.Container) (any, error) {
|
|
cli := dic.MustService(services.SvcClient).(*sqs.Client)
|
|
cli := dic.MustService(services.SvcClient).(*sqs.Client)
|
|
w := dic.MustParam(services.PWriter).(io.Writer)
|
|
w := dic.MustParam(services.PWriter).(io.Writer)
|
|
|
|
+ vto := dic.MustParam(services.PVTO).(time.Duration)
|
|
|
|
+
|
|
wait := int32(dic.MustParam(services.PWait).(int))
|
|
wait := int32(dic.MustParam(services.PWait).(int))
|
|
return &redriver{
|
|
return &redriver{
|
|
Client: cli,
|
|
Client: cli,
|
|
|
|
+ VTO: int32(vto.Seconds()),
|
|
Wait: wait,
|
|
Wait: wait,
|
|
Writer: w,
|
|
Writer: w,
|
|
}, nil
|
|
}, nil
|