aws_sqs_extended_client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/lib/aws_sqs_extended_client/client.rb +157 -0
- data/lib/aws_sqs_extended_client/constants.rb +7 -0
- data/lib/aws_sqs_extended_client/errors.rb +44 -0
- data/lib/aws_sqs_extended_client/validator.rb +39 -0
- data/lib/aws_sqs_extended_client.rb +4 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8b2bbebadb3a245517db4d6f6e898ac88e59fb3fab3ae565352a966dce65394f
|
4
|
+
data.tar.gz: fc70a193700aff4e6749aeedc04cc5941d5d8f9c27824a34512321edc5f9813d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 55544c1f6dd1469429c922e5a5659621fea80eb5340582014bac635d6efb930f35a51f004f7701973c10a7d0083513d896190fb304d53caff758f7a0dc6ad6a0
|
7
|
+
data.tar.gz: 8470b1aa27602466c76fedbd184a5461c1dbc662a1c744e1d488840082bfe3143e66ff46395adf339832e0b31a80008712b07cc19e819148f47e17e07e249645
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Hemaraj G
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# sqs_extended_client
|
2
|
+
|
3
|
+
A Ruby gem that provides support for sending and receiving large messages using Amazon SQS by offloading payloads to Amazon S3, similar to the AWS Java SDK's SQS Extended Client Library.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## ✨ Features
|
8
|
+
|
9
|
+
- ✅ Automatically offloads large messages to S3
|
10
|
+
- ✅ Configurable max message size threshold
|
11
|
+
- ✅ Optional compression support
|
12
|
+
- ✅ Automatic S3 payload cleanup after reading (configurable)
|
13
|
+
- ✅ JSON schema validation (planned)
|
14
|
+
- ✅ Error handling for missing or inaccessible buckets
|
15
|
+
- ✅ Ready for use as a drop-in extension of `Aws::SQS::Client`
|
16
|
+
|
17
|
+
---
|
18
|
+
|
19
|
+
## 🔧 Installation
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install sqs_extended_client
|
23
|
+
|
24
|
+
gem 'sqs_extended_client', path: 'path/to/your/local/gem'
|
25
|
+
|
26
|
+
```
|
27
|
+
|
28
|
+
Example:
|
29
|
+
|
30
|
+
|
31
|
+
```bash
|
32
|
+
require 'aws-sdk-sqs'
|
33
|
+
require 'aws-sdk-s3'
|
34
|
+
require 'sqs_extended_client'
|
35
|
+
|
36
|
+
sqs = Aws::SQS::Client.new(region: 'us-east-1')
|
37
|
+
s3 = Aws::S3::Client.new(region: 'us-east-1')
|
38
|
+
|
39
|
+
client = SqsExtendedClient::Client.new(
|
40
|
+
sqs: sqs,
|
41
|
+
s3: s3,
|
42
|
+
queue_url: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
|
43
|
+
bucket_name: 'my-sqs-large-message-bucket',
|
44
|
+
always_use_s3: false,
|
45
|
+
delete_s3_payload_after_read: true,
|
46
|
+
compress: true
|
47
|
+
)
|
48
|
+
|
49
|
+
client.send_message("Hello world!")
|
50
|
+
messages = client.receive_messages
|
51
|
+
```
|
@@ -0,0 +1,157 @@
|
|
1
|
+
|
2
|
+
require 'aws-sdk-s3'
|
3
|
+
require 'aws-sdk-sqs'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'json'
|
6
|
+
require 'zlib'
|
7
|
+
require 'logger'
|
8
|
+
require_relative 'errors'
|
9
|
+
require_relative 'validator'
|
10
|
+
require_relative 'constants'
|
11
|
+
|
12
|
+
module SqsExtendedClient
|
13
|
+
include Constant
|
14
|
+
class Client
|
15
|
+
|
16
|
+
def initialize(args = {})
|
17
|
+
|
18
|
+
Validator.validate_client_options!({
|
19
|
+
sqs: args[:sqs], queue_url: args[:queue_url]
|
20
|
+
})
|
21
|
+
|
22
|
+
@sqs = args[:sqs]
|
23
|
+
@s3 = args[:s3]
|
24
|
+
@queue_url = args[:queue_url]
|
25
|
+
@bucket_name = args[:bucket_name]
|
26
|
+
@always_use_s3 = args.key?(:always_use_s3) ? args[:always_use_s3] : false
|
27
|
+
@max_message_size = args[:max_message_size] || Constant::DEFAULT_MAX_SQS_MESSAGE_SIZE
|
28
|
+
@delete_s3_payload_after_read = args.key?(:delete_s3_payload_after_read) ? args[:delete_s3_payload_after_read] : false
|
29
|
+
@compress = args.key?(:compress) ? args[:compress] : false
|
30
|
+
@logger = args[:logger] || Logger.new($stdout)
|
31
|
+
end
|
32
|
+
|
33
|
+
def send_message(message_body = nil)
|
34
|
+
store_method = Validator.validate_send_message!({
|
35
|
+
message_body: message_body,
|
36
|
+
always_use_s3: @always_use_s3,
|
37
|
+
max_message_size: @max_message_size,
|
38
|
+
bucket_name: @bucket_name,
|
39
|
+
s3: @s3
|
40
|
+
})
|
41
|
+
|
42
|
+
if store_method == :use_s3
|
43
|
+
key = "sqs-large/#{SecureRandom.uuid}.json"
|
44
|
+
data = @compress ? Zlib::Deflate.deflate(message_body) : message_body
|
45
|
+
|
46
|
+
begin
|
47
|
+
@s3.put_object(bucket: @bucket_name, key: key, body: data)
|
48
|
+
@logger&.info("Uploaded large message to S3: #{key}")
|
49
|
+
rescue Aws::S3::Errors::NoSuchBucket => e
|
50
|
+
raise BucketNotFoundError.new(@bucket_name, cause: e)
|
51
|
+
rescue Aws::S3::Errors::AccessDenied => e
|
52
|
+
raise AccessDeniedError.new("Access denied to bucket: #{@bucket_name}", cause: e)
|
53
|
+
rescue Aws::S3::Errors::ServiceError => e
|
54
|
+
raise S3UploadError.new("Failed to upload to S3: #{e.message}", cause: e)
|
55
|
+
rescue StandardError => e
|
56
|
+
@logger&.error("Unexpected S3 error while uploading: #{e.class} - #{e.message}")
|
57
|
+
raise S3UploadError.new("Unexpected error during S3 upload", cause: e)
|
58
|
+
end
|
59
|
+
|
60
|
+
body = {
|
61
|
+
s3_pointer: { bucket: @bucket_name, key: key },
|
62
|
+
compressed: @compress
|
63
|
+
}.to_json
|
64
|
+
else
|
65
|
+
body = message_body
|
66
|
+
end
|
67
|
+
|
68
|
+
begin
|
69
|
+
@sqs.send_message(queue_url: @queue_url, message_body: body)
|
70
|
+
@logger&.info("Sent message to SQS#{' (S3 pointer)' if store_method == :use_s3}")
|
71
|
+
rescue Aws::SQS::Errors::ServiceError => e
|
72
|
+
@logger&.error("Failed to send message to SQS: #{e.class} - #{e.message}")
|
73
|
+
raise SqsSendError.new("SQS send failed: #{e.message}", cause: e)
|
74
|
+
rescue StandardError => e
|
75
|
+
@logger&.error("Unexpected error during SQS send: #{e.class} - #{e.message}")
|
76
|
+
raise SqsSendError.new("Unexpected error during SQS send", cause: e)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def receive_messages(args = {})
|
82
|
+
max_messages = args[:max_number] || Constant::MAX_MESSAGES
|
83
|
+
wait_time = args[:wait_time_seconds] || Constant::LONG_POLL_WAIT_TIME
|
84
|
+
auto_delete = args.key?(:auto_delete) ? args[:auto_delete] : true
|
85
|
+
|
86
|
+
resp = @sqs.receive_message(
|
87
|
+
queue_url: @queue_url,
|
88
|
+
max_number_of_messages: max_messages,
|
89
|
+
wait_time_seconds: wait_time,
|
90
|
+
message_attribute_names: ["All"]
|
91
|
+
)
|
92
|
+
|
93
|
+
return [] if resp.messages.empty?
|
94
|
+
|
95
|
+
resp.messages.map do |msg|
|
96
|
+
begin
|
97
|
+
decoded_body = parse_message_body(msg)
|
98
|
+
msg.define_singleton_method(:decoded_body) { decoded_body }
|
99
|
+
|
100
|
+
msg
|
101
|
+
rescue JSON::ParserError => e
|
102
|
+
@logger&.error("Failed to parse JSON from message #{msg.message_id}: #{e.message}")
|
103
|
+
nil
|
104
|
+
rescue Aws::S3::Errors::NoSuchKey => e
|
105
|
+
@logger&.error("S3 object not found for message #{msg.message_id}: #{e.message}")
|
106
|
+
nil
|
107
|
+
rescue Aws::S3::Errors::NoSuchBucket => e
|
108
|
+
@logger&.error("S3 bucket not found for message #{msg.message_id}: #{e.message}")
|
109
|
+
nil
|
110
|
+
rescue Aws::S3::Errors::AccessDenied => e
|
111
|
+
@logger&.error("Access denied for S3 object in message #{msg.message_id}: #{e.message}")
|
112
|
+
nil
|
113
|
+
rescue Aws::S3::Errors::ServiceError => e
|
114
|
+
@logger&.error("S3 service error while reading message #{msg.message_id}: #{e.message}")
|
115
|
+
nil
|
116
|
+
rescue Zlib::DataError => e
|
117
|
+
@logger&.error("Decompression failed for message #{msg.message_id}: #{e.message}")
|
118
|
+
nil
|
119
|
+
rescue StandardError => e
|
120
|
+
@logger&.error("Unexpected error in message #{msg.message_id}: #{e.class} - #{e.message}")
|
121
|
+
nil
|
122
|
+
ensure
|
123
|
+
if auto_delete
|
124
|
+
@sqs.delete_message(queue_url: @queue_url, receipt_handle: msg.receipt_handle)
|
125
|
+
@logger&.info("Deleted SQS message: #{msg.message_id}")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end.compact
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def parse_message_body(msg)
|
134
|
+
parsed = JSON.parse(msg.body)
|
135
|
+
|
136
|
+
if parsed && parsed["s3_pointer"]
|
137
|
+
bucket = parsed["s3_pointer"]["bucket"]
|
138
|
+
key = parsed["s3_pointer"]["key"]
|
139
|
+
compressed = parsed["compressed"]
|
140
|
+
|
141
|
+
s3_data = @s3.get_object(bucket: bucket, key: key).body.read
|
142
|
+
result = compressed ? Zlib::Inflate.inflate(s3_data) : s3_data
|
143
|
+
|
144
|
+
p "result #{result}"
|
145
|
+
|
146
|
+
if @delete_s3_payload_after_read
|
147
|
+
@s3.delete_object(bucket: bucket, key: key)
|
148
|
+
@logger&.info("Deleted S3 object after read: #{key}")
|
149
|
+
end
|
150
|
+
|
151
|
+
result
|
152
|
+
else
|
153
|
+
msg.body
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SqsExtendedClient
|
2
|
+
class Error < StandardError
|
3
|
+
attr_reader :cause
|
4
|
+
|
5
|
+
def initialize(msg = nil, cause: nil)
|
6
|
+
@cause = cause
|
7
|
+
super(msg)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class BucketNameMissingError < Error
|
12
|
+
def initialize
|
13
|
+
super("S3 bucket name must be provided but was nil or empty.")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BucketNotFoundError < Error
|
18
|
+
attr_reader :bucket_name
|
19
|
+
|
20
|
+
def initialize(bucket_name, cause: nil)
|
21
|
+
@bucket_name = bucket_name
|
22
|
+
msg = "S3 bucket not found or not accessible: #{bucket_name}"
|
23
|
+
super(msg, cause: cause)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class AccessDeniedError < Error
|
28
|
+
def initialize(msg = "Access denied to the S3 resource", cause: nil)
|
29
|
+
super(msg, cause: cause)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class S3UploadError < Error
|
34
|
+
def initialize(msg = "Unexpected error while uploading to S3", cause: nil)
|
35
|
+
super(msg, cause: cause)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class SqsSendError < Error
|
40
|
+
def initialize(msg = "SQS send_message failed", cause: nil)
|
41
|
+
super(msg, cause: cause)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require_relative 'errors'
|
3
|
+
|
4
|
+
module SqsExtendedClient
|
5
|
+
class Validator
|
6
|
+
REQUIRED_ATTRIBUTES = %i[sqs queue_url].freeze
|
7
|
+
S3_MESSASGE_ATTRIBUTES = %i[s3 bucket_name].freeze
|
8
|
+
# RECEIVE_MESSAGE_ATTRIBUTES =
|
9
|
+
|
10
|
+
def self.validate_client_options!(options)
|
11
|
+
missing = REQUIRED_ATTRIBUTES.select do |key|
|
12
|
+
options[key].nil? || options[key].to_s.strip.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
unless missing.empty?
|
16
|
+
raise ArgumentError, "Missing required attribute(s): #{missing.join(', ')}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.validate_send_message!(options)
|
21
|
+
raise ArgumentError, "Missing required attribute: message_body" if options[:message_body].nil?
|
22
|
+
|
23
|
+
raise ArgumentError, "max_message_size must be a positive integer" if options[:max_message_size] && options[:max_message_size].to_i <= 0
|
24
|
+
|
25
|
+
if options[:always_use_s3] || options[:message_body].bytesize > options[:max_message_size]
|
26
|
+
missing = S3_MESSASGE_ATTRIBUTES.select do |key|
|
27
|
+
options[key].nil? || options[key].to_s.strip.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
unless missing.empty?
|
31
|
+
raise ArgumentError, "Missing required attribute(s): #{missing.join(', ')}"
|
32
|
+
end
|
33
|
+
return :use_s3
|
34
|
+
else
|
35
|
+
return :use_sqs
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aws_sqs_extended_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hemaraj G
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk-sqs
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk-s3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Ruby gem that mimics AWS SQS Extended Client with features like S3 offloading,
|
56
|
+
compression, cleanup, validation, and logging.
|
57
|
+
email:
|
58
|
+
- hemarajg7@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- LICENSE.txt
|
64
|
+
- README.md
|
65
|
+
- lib/aws_sqs_extended_client.rb
|
66
|
+
- lib/aws_sqs_extended_client/client.rb
|
67
|
+
- lib/aws_sqs_extended_client/constants.rb
|
68
|
+
- lib/aws_sqs_extended_client/errors.rb
|
69
|
+
- lib/aws_sqs_extended_client/validator.rb
|
70
|
+
homepage: https://github.com/hemarajg/ruby_aws_extended_client
|
71
|
+
licenses:
|
72
|
+
- MIT
|
73
|
+
metadata: {}
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubygems_version: 3.2.3
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: Extended SQS client with S3 offloading and validation.
|
93
|
+
test_files: []
|