github.com/Jeffail/benthos/v3@v3.65.0/lib/input/reader/amazon_sqs.go (about) 1 package reader 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/Jeffail/benthos/v3/lib/log" 9 "github.com/Jeffail/benthos/v3/lib/message" 10 "github.com/Jeffail/benthos/v3/lib/metrics" 11 "github.com/Jeffail/benthos/v3/lib/types" 12 sess "github.com/Jeffail/benthos/v3/lib/util/aws/session" 13 "github.com/aws/aws-sdk-go/aws" 14 "github.com/aws/aws-sdk-go/aws/awserr" 15 "github.com/aws/aws-sdk-go/aws/request" 16 "github.com/aws/aws-sdk-go/aws/session" 17 "github.com/aws/aws-sdk-go/service/sqs" 18 ) 19 20 //------------------------------------------------------------------------------ 21 22 // AmazonSQSConfig contains configuration values for the input type. 23 type AmazonSQSConfig struct { 24 sess.Config `json:",inline" yaml:",inline"` 25 URL string `json:"url" yaml:"url"` 26 Timeout string `json:"timeout" yaml:"timeout"` 27 MaxNumberOfMessages int64 `json:"max_number_of_messages" yaml:"max_number_of_messages"` 28 DeleteMessage bool `json:"delete_message" yaml:"delete_message"` 29 } 30 31 // NewAmazonSQSConfig creates a new Config with default values. 32 func NewAmazonSQSConfig() AmazonSQSConfig { 33 return AmazonSQSConfig{ 34 Config: sess.NewConfig(), 35 URL: "", 36 Timeout: "5s", 37 MaxNumberOfMessages: 1, 38 DeleteMessage: true, 39 } 40 } 41 42 //------------------------------------------------------------------------------ 43 44 // AmazonSQS is a benthos reader.Type implementation that reads messages from an 45 // Amazon SQS queue. 46 type AmazonSQS struct { 47 conf AmazonSQSConfig 48 49 pendingHandles map[string]string 50 51 session *session.Session 52 sqs *sqs.SQS 53 timeout time.Duration 54 55 log log.Modular 56 stats metrics.Type 57 } 58 59 // NewAmazonSQS creates a new Amazon SQS reader.Type. 60 func NewAmazonSQS( 61 conf AmazonSQSConfig, 62 log log.Modular, 63 stats metrics.Type, 64 ) (*AmazonSQS, error) { 65 var timeout time.Duration 66 if tout := conf.Timeout; len(tout) > 0 { 67 var err error 68 if timeout, err = time.ParseDuration(tout); err != nil { 69 return nil, fmt.Errorf("failed to parse timeout string: %v", err) 70 } 71 } 72 return &AmazonSQS{ 73 conf: conf, 74 log: log, 75 stats: stats, 76 timeout: timeout, 77 pendingHandles: map[string]string{}, 78 }, nil 79 } 80 81 // Connect attempts to establish a connection to the target SQS queue. 82 func (a *AmazonSQS) Connect() error { 83 return a.ConnectWithContext(context.Background()) 84 } 85 86 // ConnectWithContext attempts to establish a connection to the target SQS 87 // queue. 88 func (a *AmazonSQS) ConnectWithContext(ctx context.Context) error { 89 if a.session != nil { 90 return nil 91 } 92 93 sess, err := a.conf.GetSession() 94 if err != nil { 95 return err 96 } 97 98 a.sqs = sqs.New(sess) 99 a.session = sess 100 101 a.log.Infof("Receiving Amazon SQS messages from URL: %v\n", a.conf.URL) 102 return nil 103 } 104 105 func addSQSMetadata(p types.Part, sqsMsg *sqs.Message) { 106 meta := p.Metadata() 107 meta.Set("sqs_message_id", *sqsMsg.MessageId) 108 meta.Set("sqs_receipt_handle", *sqsMsg.ReceiptHandle) 109 if rCountStr := sqsMsg.Attributes["ApproximateReceiveCount"]; rCountStr != nil { 110 meta.Set("sqs_approximate_receive_count", *rCountStr) 111 } 112 for k, v := range sqsMsg.MessageAttributes { 113 if v.StringValue != nil { 114 meta.Set(k, *v.StringValue) 115 } 116 } 117 } 118 119 // ReadWithContext attempts to read a new message from the target SQS. 120 func (a *AmazonSQS) ReadWithContext(ctx context.Context) (types.Message, AsyncAckFn, error) { 121 if a.session == nil { 122 return nil, nil, types.ErrNotConnected 123 } 124 125 msg := message.New(nil) 126 pendingHandles := map[string]string{} 127 128 output, err := a.sqs.ReceiveMessageWithContext(ctx, &sqs.ReceiveMessageInput{ 129 QueueUrl: aws.String(a.conf.URL), 130 MaxNumberOfMessages: aws.Int64(a.conf.MaxNumberOfMessages), 131 WaitTimeSeconds: aws.Int64(int64(a.timeout.Seconds())), 132 AttributeNames: []*string{aws.String("All")}, 133 MessageAttributeNames: []*string{aws.String("All")}, 134 }) 135 if err != nil { 136 if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode { 137 return nil, nil, types.ErrTimeout 138 } 139 return nil, nil, err 140 } 141 for _, sqsMsg := range output.Messages { 142 if sqsMsg.ReceiptHandle != nil { 143 pendingHandles[*sqsMsg.MessageId] = *sqsMsg.ReceiptHandle 144 } 145 146 if sqsMsg.Body != nil { 147 part := message.NewPart([]byte(*sqsMsg.Body)) 148 addSQSMetadata(part, sqsMsg) 149 msg.Append(part) 150 } 151 } 152 if msg.Len() == 0 { 153 return nil, nil, types.ErrTimeout 154 } 155 156 return msg, func(rctx context.Context, res types.Response) error { 157 // TODO: Replace this with a background process for batching these 158 // requests up more. 159 if res.Error() == nil { 160 if !a.conf.DeleteMessage { 161 return nil 162 } 163 for len(pendingHandles) > 0 { 164 input := sqs.DeleteMessageBatchInput{ 165 QueueUrl: aws.String(a.conf.URL), 166 } 167 168 for k, v := range pendingHandles { 169 input.Entries = append(input.Entries, &sqs.DeleteMessageBatchRequestEntry{ 170 Id: aws.String(k), 171 ReceiptHandle: aws.String(v), 172 }) 173 delete(pendingHandles, k) 174 if len(input.Entries) == 10 { 175 break 176 } 177 } 178 179 response, serr := a.sqs.DeleteMessageBatchWithContext(rctx, &input) 180 if serr != nil { 181 a.log.Errorf("Failed to delete consumed SQS messages: %v\n", serr) 182 return serr 183 } 184 for _, fail := range response.Failed { 185 a.log.Errorf("Failed to delete consumed SQS message '%v', response code: %v\n", *fail.Id, *fail.Code) 186 } 187 } 188 } else { 189 for len(pendingHandles) > 0 { 190 input := sqs.ChangeMessageVisibilityBatchInput{ 191 QueueUrl: aws.String(a.conf.URL), 192 } 193 194 for k, v := range pendingHandles { 195 input.Entries = append(input.Entries, &sqs.ChangeMessageVisibilityBatchRequestEntry{ 196 Id: aws.String(k), 197 ReceiptHandle: aws.String(v), 198 VisibilityTimeout: aws.Int64(0), 199 }) 200 delete(pendingHandles, k) 201 if len(input.Entries) == 10 { 202 break 203 } 204 } 205 206 response, serr := a.sqs.ChangeMessageVisibilityBatchWithContext(rctx, &input) 207 if serr != nil { 208 a.log.Errorf("Failed to change consumed SQS message visibility: %v\n", serr) 209 return serr 210 } 211 for _, fail := range response.Failed { 212 a.log.Errorf("Failed to change consumed SQS message '%v' visibility, response code: %v\n", *fail.Id, *fail.Code) 213 } 214 } 215 } 216 return nil 217 }, nil 218 } 219 220 // Read attempts to read a new message from the target SQS. 221 func (a *AmazonSQS) Read() (types.Message, error) { 222 if a.session == nil { 223 return nil, types.ErrNotConnected 224 } 225 226 output, err := a.sqs.ReceiveMessage(&sqs.ReceiveMessageInput{ 227 QueueUrl: aws.String(a.conf.URL), 228 MaxNumberOfMessages: aws.Int64(a.conf.MaxNumberOfMessages), 229 WaitTimeSeconds: aws.Int64(int64(a.timeout.Seconds())), 230 AttributeNames: []*string{aws.String("All")}, 231 MessageAttributeNames: []*string{aws.String("All")}, 232 }) 233 if err != nil { 234 return nil, err 235 } 236 237 msg := message.New(nil) 238 239 if len(output.Messages) == 0 { 240 return nil, types.ErrTimeout 241 } 242 243 for _, sqsMsg := range output.Messages { 244 if sqsMsg.ReceiptHandle != nil { 245 a.pendingHandles[*sqsMsg.MessageId] = *sqsMsg.ReceiptHandle 246 } 247 248 if sqsMsg.Body != nil { 249 part := message.NewPart([]byte(*sqsMsg.Body)) 250 addSQSMetadata(part, sqsMsg) 251 msg.Append(part) 252 } 253 } 254 255 if msg.Len() == 0 { 256 return nil, types.ErrTimeout 257 } 258 259 return msg, nil 260 } 261 262 // Acknowledge confirms whether or not our unacknowledged messages have been 263 // successfully propagated or not. 264 func (a *AmazonSQS) Acknowledge(err error) error { 265 if err == nil { 266 for len(a.pendingHandles) > 0 { 267 if !a.conf.DeleteMessage { 268 // If we aren't deleting the source here then simply remove the 269 // handles. 270 for k := range a.pendingHandles { 271 delete(a.pendingHandles, k) 272 } 273 return nil 274 } 275 276 input := sqs.DeleteMessageBatchInput{ 277 QueueUrl: aws.String(a.conf.URL), 278 } 279 280 for k, v := range a.pendingHandles { 281 input.Entries = append(input.Entries, &sqs.DeleteMessageBatchRequestEntry{ 282 Id: aws.String(k), 283 ReceiptHandle: aws.String(v), 284 }) 285 delete(a.pendingHandles, k) 286 if len(input.Entries) == 10 { 287 break 288 } 289 } 290 291 if res, serr := a.sqs.DeleteMessageBatch(&input); serr != nil { 292 a.log.Errorf("Failed to delete consumed SQS messages: %v\n", serr) 293 } else { 294 for _, fail := range res.Failed { 295 a.log.Errorf("Failed to delete consumed SQS message '%v', response code: %v\n", *fail.Id, *fail.Code) 296 } 297 } 298 } 299 } else { 300 for len(a.pendingHandles) > 0 { 301 input := sqs.ChangeMessageVisibilityBatchInput{ 302 QueueUrl: aws.String(a.conf.URL), 303 } 304 305 for k, v := range a.pendingHandles { 306 input.Entries = append(input.Entries, &sqs.ChangeMessageVisibilityBatchRequestEntry{ 307 Id: aws.String(k), 308 ReceiptHandle: aws.String(v), 309 VisibilityTimeout: aws.Int64(0), 310 }) 311 delete(a.pendingHandles, k) 312 if len(input.Entries) == 10 { 313 break 314 } 315 } 316 317 if res, serr := a.sqs.ChangeMessageVisibilityBatch(&input); serr != nil { 318 a.log.Errorf("Failed to change consumed SQS message visibility: %v\n", serr) 319 } else { 320 for _, fail := range res.Failed { 321 a.log.Errorf("Failed to change consumed SQS message '%v' visibility, response code: %v\n", *fail.Id, *fail.Code) 322 } 323 } 324 } 325 } 326 return nil 327 } 328 329 // CloseAsync begins cleaning up resources used by this reader asynchronously. 330 func (a *AmazonSQS) CloseAsync() { 331 } 332 333 // WaitForClose will block until either the reader is closed or a specified 334 // timeout occurs. 335 func (a *AmazonSQS) WaitForClose(time.Duration) error { 336 return nil 337 } 338 339 //------------------------------------------------------------------------------