sqewer 6.3.0 → 6.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +45 -43
- data/CHANGELOG.md +6 -0
- data/lib/sqewer.rb +4 -1
- data/lib/sqewer/connection.rb +53 -1
- data/lib/sqewer/extensions/appsignal_wrapper.rb +0 -1
- data/lib/sqewer/serializer.rb +6 -6
- data/lib/sqewer/simple_job.rb +2 -2
- data/lib/sqewer/submitter.rb +22 -4
- data/lib/sqewer/version.rb +1 -1
- data/lib/sqewer/worker.rb +11 -38
- data/sqewer.gemspec +2 -2
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0251b1700672c6dc8722779c0865d3757aff9de9ea747d4739863521931cc0c
|
4
|
+
data.tar.gz: 0533f556c4cad614cf7973818126996c8a8bea51d1791ca22df059ab65a5202a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d9d8f211697dc585517fd6c431e32f7500711888a3b97cf94aa7d4afbd043da2790407242a81a1ba37bca905cf6b454f73ee98a5015550768c948fe53eaa315
|
7
|
+
data.tar.gz: 38369a4c3a74e3bdb5616705744ad732a363757c3eeadb04dde327f77d9251633047851e446454a0126895acbaf194d44081c3afd0192a509992d592e164a2a5
|
data/.gitignore
CHANGED
@@ -1,54 +1,56 @@
|
|
1
|
-
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
2
14
|
.env
|
3
15
|
|
4
|
-
#
|
5
|
-
|
6
|
-
coverage.data
|
16
|
+
# Ignore Byebug command history file.
|
17
|
+
.byebug_history
|
7
18
|
|
8
|
-
|
9
|
-
|
19
|
+
## Specific to RubyMotion:
|
20
|
+
.dat*
|
21
|
+
.repl_history
|
22
|
+
build/
|
23
|
+
*.bridgesupport
|
24
|
+
build-iPhoneOS/
|
25
|
+
build-iPhoneSimulator/
|
10
26
|
|
11
|
-
|
12
|
-
doc
|
13
|
-
.yardoc
|
14
|
-
|
15
|
-
# bundler
|
16
|
-
.bundle
|
17
|
-
Gemfile.lock
|
18
|
-
gemfiles/*.lock
|
19
|
-
|
20
|
-
# jeweler generated
|
21
|
-
pkg
|
22
|
-
|
23
|
-
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
27
|
+
## Specific to RubyMotion (use of CocoaPods):
|
24
28
|
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
29
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
30
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
31
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
28
32
|
#
|
29
|
-
#
|
30
|
-
# saving you from having to 'pollute' every project you touch with them
|
31
|
-
#
|
32
|
-
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
33
|
-
#
|
34
|
-
# For MacOS:
|
35
|
-
#
|
36
|
-
#.DS_Store
|
33
|
+
# vendor/Pods/
|
37
34
|
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
## Documentation cache and generated files:
|
36
|
+
/.yardoc/
|
37
|
+
/_yardoc/
|
38
|
+
/doc/
|
39
|
+
/rdoc/
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
## Environment normalization:
|
42
|
+
/.bundle/
|
43
|
+
/vendor/bundle
|
44
|
+
/lib/bundler/man/
|
46
45
|
|
47
|
-
#
|
48
|
-
|
46
|
+
# for a library or gem, you might want to ignore these files since the code is
|
47
|
+
# intended to run in multiple environments; otherwise, check them in:
|
48
|
+
Gemfile.lock
|
49
|
+
.ruby-version
|
50
|
+
.ruby-gemset
|
49
51
|
|
50
|
-
#
|
51
|
-
|
52
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
53
|
+
.rvmrc
|
52
54
|
|
53
|
-
#
|
54
|
-
|
55
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
56
|
+
# .rubocop-https?--*
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
### 6.4.0
|
2
|
+
- Raise an exception in submit! if the job serializes to a message that is
|
3
|
+
above the native SQS limit for message size.
|
4
|
+
- Ensure SendMessageBatch is only performed for batches totaling 256KB of message size or less.
|
5
|
+
- Insert Sqewer::Error between StandardError and our custom errors for easier rescuing
|
6
|
+
|
1
7
|
### 6.3.0
|
2
8
|
- Add support for Ruby 2.7
|
3
9
|
|
data/lib/sqewer.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# The enclosing module for the library
|
2
2
|
module Sqewer
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
|
3
6
|
# Eager-load everything except extensions. Sort to ensure
|
4
7
|
# the files load in the same order on all platforms.
|
5
8
|
Dir.glob(__dir__ + '/**/*.rb').sort.each do |path|
|
@@ -23,7 +26,7 @@ module Sqewer
|
|
23
26
|
def self.submit!(*jobs, **options)
|
24
27
|
Sqewer::Submitter.default.submit!(*jobs, **options)
|
25
28
|
end
|
26
|
-
|
29
|
+
|
27
30
|
# If we are within Rails, load the railtie
|
28
31
|
require_relative 'sqewer/extensions/railtie' if defined?(Rails)
|
29
32
|
|
data/lib/sqewer/connection.rb
CHANGED
@@ -11,7 +11,7 @@ class Sqewer::Connection
|
|
11
11
|
MAX_RANDOM_FAILURES_PER_CALL = 10
|
12
12
|
MAX_RANDOM_RECEIVE_FAILURES = 100 # sure to hit the max_elapsed_time of 900 seconds
|
13
13
|
|
14
|
-
NotOurFaultAwsError = Class.new(
|
14
|
+
NotOurFaultAwsError = Class.new(Sqewer::Error)
|
15
15
|
|
16
16
|
# A wrapper for most important properties of the received message
|
17
17
|
class Message < Struct.new(:receipt_handle, :body, :attributes)
|
@@ -91,6 +91,58 @@ class Sqewer::Connection
|
|
91
91
|
m[:delay_seconds] = kwargs_for_send[:delay_seconds] if kwargs_for_send[:delay_seconds]
|
92
92
|
messages << m
|
93
93
|
end
|
94
|
+
|
95
|
+
# each_batch here also needs to ensure that the sum of payload lengths does not exceed 256kb
|
96
|
+
def each_batch
|
97
|
+
regrouped = pack_into_batches(messages, weight_limit: 256 * 1024, batch_length_limit: 10) do |message|
|
98
|
+
message.fetch(:message_body).bytesize
|
99
|
+
end
|
100
|
+
regrouped.each { |b| yield(b) }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Optimizes a large list of items into batches of 10 items
|
104
|
+
# or less and with the sum of item lengths being below 256KB
|
105
|
+
# The block given to the method should return the weight of the given item
|
106
|
+
def pack_into_batches(items, weight_limit:, batch_length_limit:)
|
107
|
+
batches = []
|
108
|
+
current_batch = []
|
109
|
+
current_batch_weight = 0
|
110
|
+
|
111
|
+
# Sort the items by their weight (length of the body).
|
112
|
+
sorted_items = items.sort_by { |item| yield(item) }
|
113
|
+
|
114
|
+
# and then take 1 item from the list and append it to the batch if it fits.
|
115
|
+
# If it doesn't fit, no item after it will fit into this batch either (!)
|
116
|
+
# which is how we can optimize
|
117
|
+
sorted_items.each_with_index do |item|
|
118
|
+
weight_of_this_item = yield(item)
|
119
|
+
|
120
|
+
# First protect from invalid input
|
121
|
+
if weight_of_this_item > weight_limit
|
122
|
+
raise "#{item.inspect} was larger than the permissible limit"
|
123
|
+
# The first limit is on the item count per batch -
|
124
|
+
# if we are limited on that the batch needs to be closed
|
125
|
+
elsif current_batch.length == batch_length_limit
|
126
|
+
batches << current_batch
|
127
|
+
current_batch = []
|
128
|
+
current_batch_weight = 0
|
129
|
+
# If placing this item in the batch would make the batch overweight
|
130
|
+
# we need to close the batch, because all the items which come after
|
131
|
+
# this one will be same size or larger. This is the key part of the optimization.
|
132
|
+
elsif (current_batch_weight + weight_of_this_item) > weight_limit
|
133
|
+
batches << current_batch
|
134
|
+
current_batch = []
|
135
|
+
current_batch_weight = 0
|
136
|
+
end
|
137
|
+
|
138
|
+
# and then append the item to the current batch
|
139
|
+
current_batch_weight += weight_of_this_item
|
140
|
+
current_batch << item
|
141
|
+
end
|
142
|
+
batches << current_batch unless current_batch.empty?
|
143
|
+
|
144
|
+
batches
|
145
|
+
end
|
94
146
|
end
|
95
147
|
|
96
148
|
# Saves the receipt handles to batch-delete from the SQS queue
|
data/lib/sqewer/serializer.rb
CHANGED
@@ -13,7 +13,7 @@ class Sqewer::Serializer
|
|
13
13
|
@instance ||= new
|
14
14
|
end
|
15
15
|
|
16
|
-
AnonymousJobClass = Class.new(
|
16
|
+
AnonymousJobClass = Class.new(Sqewer::Error)
|
17
17
|
|
18
18
|
# Instantiate a Job object from a message body string. If the
|
19
19
|
# returned result is `nil`, the job will be skipped.
|
@@ -33,18 +33,18 @@ class Sqewer::Serializer
|
|
33
33
|
# use a default that will put us ahead of that execution deadline from the start.
|
34
34
|
t = Time.now.to_i
|
35
35
|
execute_after = job_ticket_hash.fetch(:_execute_after) { t - 5 }
|
36
|
-
|
36
|
+
|
37
37
|
job_params = job_ticket_hash.delete(:_job_params)
|
38
38
|
job = if job_params.nil? || job_params.empty?
|
39
39
|
job_class.new # no args
|
40
40
|
else
|
41
41
|
job_class.new(**job_params) # The rest of the message are keyword arguments for the job
|
42
42
|
end
|
43
|
-
|
44
|
-
# If the job is not up for execution now, wrap it with something that will
|
43
|
+
|
44
|
+
# If the job is not up for execution now, wrap it with something that will
|
45
45
|
# re-submit it for later execution when the run() method is called
|
46
46
|
return ::Sqewer::Resubmit.new(job, execute_after) if execute_after > t
|
47
|
-
|
47
|
+
|
48
48
|
job
|
49
49
|
end
|
50
50
|
|
@@ -65,7 +65,7 @@ class Sqewer::Serializer
|
|
65
65
|
job_params = job.respond_to?(:to_h) ? job.to_h : nil
|
66
66
|
job_ticket_hash = {_job_class: job_class_name, _job_params: job_params}
|
67
67
|
job_ticket_hash[:_execute_after] = execute_after_timestamp.to_i if execute_after_timestamp
|
68
|
-
|
68
|
+
|
69
69
|
JSON.dump(job_ticket_hash)
|
70
70
|
end
|
71
71
|
end
|
data/lib/sqewer/simple_job.rb
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
# * to_h() will produce a symbolized Hash with all the properties defined using attr_accessor, and the job_class_name
|
6
6
|
# * inspect() will provide a sensible default string representation for logging
|
7
7
|
module Sqewer::SimpleJob
|
8
|
-
UnknownJobAttribute = Class.new(
|
9
|
-
MissingAttribute = Class.new(
|
8
|
+
UnknownJobAttribute = Class.new(Sqewer::Error)
|
9
|
+
MissingAttribute = Class.new(Sqewer::Error)
|
10
10
|
|
11
11
|
EQ_END = /(\w+)(\=)$/
|
12
12
|
|
data/lib/sqewer/submitter.rb
CHANGED
@@ -3,7 +3,10 @@
|
|
3
3
|
# and the serializer (something that responds to `#serialize`) to
|
4
4
|
# convert the job into the string that will be put in the queue.
|
5
5
|
class Sqewer::Submitter < Struct.new(:connection, :serializer)
|
6
|
-
|
6
|
+
MAX_PERMITTED_MESSAGE_SIZE_BYTES = 256 * 1024
|
7
|
+
|
8
|
+
NotSqewerJob = Class.new(Sqewer::Error)
|
9
|
+
MessageTooLarge = Class.new(Sqewer::Error)
|
7
10
|
|
8
11
|
# Returns a default Submitter, configured with the default connection
|
9
12
|
# and the default serializer.
|
@@ -12,7 +15,7 @@ class Sqewer::Submitter < Struct.new(:connection, :serializer)
|
|
12
15
|
end
|
13
16
|
|
14
17
|
def submit!(job, **kwargs_for_send)
|
15
|
-
|
18
|
+
validate_job_responds_to_run!(job)
|
16
19
|
message_body = if delay_by_seconds = kwargs_for_send[:delay_seconds]
|
17
20
|
clamped_delay = clamp_delay(delay_by_seconds)
|
18
21
|
kwargs_for_send[:delay_seconds] = clamped_delay
|
@@ -21,11 +24,26 @@ class Sqewer::Submitter < Struct.new(:connection, :serializer)
|
|
21
24
|
else
|
22
25
|
serializer.serialize(job)
|
23
26
|
end
|
27
|
+
validate_message_for_size!(message_body, job)
|
28
|
+
|
24
29
|
connection.send_message(message_body, **kwargs_for_send)
|
25
30
|
end
|
26
|
-
|
31
|
+
|
27
32
|
private
|
28
|
-
|
33
|
+
|
34
|
+
def validate_job_responds_to_run!(job)
|
35
|
+
return if job.respond_to?(:run)
|
36
|
+
error_message = "Submitted object is not a valid job (does not respond to #run): #{job.inspect}"
|
37
|
+
raise NotSqewerJob.new(error_message)
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_message_for_size!(message_body, job)
|
41
|
+
actual_bytesize = message_body.bytesize
|
42
|
+
return if actual_bytesize <= MAX_PERMITTED_MESSAGE_SIZE_BYTES
|
43
|
+
error_message = "Job #{job.inspect} serialized to a message which was too large (#{actual_bytesize} bytes)"
|
44
|
+
raise MessageTooLarge.new(error_message)
|
45
|
+
end
|
46
|
+
|
29
47
|
def clamp_delay(delay)
|
30
48
|
[1, 899, delay].sort[1]
|
31
49
|
end
|
data/lib/sqewer/version.rb
CHANGED
data/lib/sqewer/worker.rb
CHANGED
@@ -48,7 +48,7 @@ class Sqewer::Worker
|
|
48
48
|
#
|
49
49
|
# @param connection[Sqewer::Connection] the object that handles polling and submitting
|
50
50
|
# @param serializer[#serialize, #unserialize] the serializer/unserializer for the jobs
|
51
|
-
# @param execution_context_class[Class] the class for the execution context (will be instantiated by
|
51
|
+
# @param execution_context_class[Class] the class for the execution context (will be instantiated by
|
52
52
|
# the worker for each job execution)
|
53
53
|
# @param submitter_class[Class] the class used for submitting jobs (will be instantiated by the worker for each job execution)
|
54
54
|
# @param middleware_stack[Sqewer::MiddlewareStack] the middleware stack that is going to be used
|
@@ -212,17 +212,21 @@ class Sqewer::Worker
|
|
212
212
|
submitter = submitter_class.new(box, serializer)
|
213
213
|
context = execution_context_class.new(submitter, {'logger' => logger})
|
214
214
|
|
215
|
-
t =
|
215
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
216
216
|
middleware_stack.around_execution(job, context) do
|
217
217
|
job.method(:run).arity.zero? ? job.run : job.run(context)
|
218
|
+
# delete_message will enqueue the message for deletion,
|
219
|
+
# but when flush! is called _first_ the messages pending will be
|
220
|
+
# delivered to the queue, THEN all the deletes are going to be performed.
|
221
|
+
# So it is safe to call delete here first - the delete won't get to the queue
|
222
|
+
# if flush! fails to spool pending messages.
|
223
|
+
box.delete_message(message.receipt_handle)
|
224
|
+
n_flushed = box.flush!
|
225
|
+
logger.debug { "[worker] Flushed %d connection commands" % n_flushed } if n_flushed > 0
|
218
226
|
end
|
219
|
-
box.delete_message(message.receipt_handle)
|
220
227
|
|
221
|
-
delta =
|
228
|
+
delta = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t
|
222
229
|
logger.info { "[worker] Finished %s in %0.2fs" % [job.inspect, delta] }
|
223
|
-
ensure
|
224
|
-
n_flushed = box.flush!
|
225
|
-
logger.debug { "[worker] Flushed %d connection commands" % n_flushed } if n_flushed.nonzero?
|
226
230
|
end
|
227
231
|
|
228
232
|
def take_and_execute
|
@@ -235,35 +239,4 @@ class Sqewer::Worker
|
|
235
239
|
@logger.error { '[worker] Failed "%s..." with %s: %s' % [message.inspect[0..64], e.class, e.message] }
|
236
240
|
e.backtrace.each { |s| @logger.debug{"\t#{s}"} }
|
237
241
|
end
|
238
|
-
|
239
|
-
def perform(message)
|
240
|
-
# Create a messagebox that buffers all the calls to Connection, so that
|
241
|
-
# we can send out those commands in one go (without interfering with senders
|
242
|
-
# on other threads, as it seems the Aws::SQS::Client is not entirely
|
243
|
-
# thread-safe - or at least not it's HTTP client part).
|
244
|
-
box = Sqewer::ConnectionMessagebox.new(connection)
|
245
|
-
|
246
|
-
job = middleware_stack.around_deserialization(serializer, message.receipt_handle, message.body, message.attributes) do
|
247
|
-
serializer.unserialize(message.body)
|
248
|
-
end
|
249
|
-
return unless job
|
250
|
-
|
251
|
-
submitter = submitter_class.new(box, serializer)
|
252
|
-
context = execution_context_class.new(submitter, {'logger' => logger})
|
253
|
-
|
254
|
-
t = Time.now
|
255
|
-
middleware_stack.around_execution(job, context) do
|
256
|
-
job.method(:run).arity.zero? ? job.run : job.run(context)
|
257
|
-
end
|
258
|
-
|
259
|
-
# Perform two flushes, one for any possible jobs the job has spawned,
|
260
|
-
# and one for the job delete afterwards
|
261
|
-
box.delete_message(message.receipt_handle)
|
262
|
-
|
263
|
-
delta = Time.now - t
|
264
|
-
logger.info { "[worker] Finished %s in %0.2fs" % [job.inspect, delta] }
|
265
|
-
ensure
|
266
|
-
n_flushed = box.flush!
|
267
|
-
logger.debug { "[worker] Flushed %d connection commands" % n_flushed } if n_flushed.nonzero?
|
268
|
-
end
|
269
242
|
end
|
data/sqewer.gemspec
CHANGED
@@ -6,8 +6,8 @@ require "sqewer/version"
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "sqewer"
|
8
8
|
spec.version = Sqewer::VERSION
|
9
|
-
spec.authors = ["Julik Tarkhanov"]
|
10
|
-
spec.email = ["me@julik.nl"]
|
9
|
+
spec.authors = ["Julik Tarkhanov", "Andrei Horak"]
|
10
|
+
spec.email = ["me@julik.nl", "linkyndy@gmail.com"]
|
11
11
|
|
12
12
|
spec.summary = %q{Process jobs from SQS}
|
13
13
|
spec.description = %q{A full-featured library for all them SQS worker needs}
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sqewer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
|
+
- Andrei Horak
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2020-
|
12
|
+
date: 2020-05-14 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: aws-sdk-sqs
|
@@ -223,6 +224,7 @@ dependencies:
|
|
223
224
|
description: A full-featured library for all them SQS worker needs
|
224
225
|
email:
|
225
226
|
- me@julik.nl
|
227
|
+
- linkyndy@gmail.com
|
226
228
|
executables:
|
227
229
|
- sqewer
|
228
230
|
- sqewer_rails
|
@@ -283,7 +285,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
283
285
|
- !ruby/object:Gem::Version
|
284
286
|
version: '0'
|
285
287
|
requirements: []
|
286
|
-
rubygems_version: 3.
|
288
|
+
rubygems_version: 3.0.3
|
287
289
|
signing_key:
|
288
290
|
specification_version: 4
|
289
291
|
summary: Process jobs from SQS
|