sqewer 6.2.2 → 6.5.1
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/.travis.yml +6 -0
- data/CHANGELOG.md +20 -0
- data/README.md +15 -11
- data/gemfiles/Gemfile.rails-4.2.x +2 -2
- data/gemfiles/Gemfile.rails-5.0.x +2 -2
- data/gemfiles/Gemfile.rails-5.1.x +3 -3
- data/lib/sqewer.rb +4 -1
- data/lib/sqewer/cli.rb +1 -0
- data/lib/sqewer/connection.rb +67 -7
- data/lib/sqewer/extensions/appsignal_wrapper.rb +0 -1
- data/lib/sqewer/serializer.rb +6 -6
- data/lib/sqewer/simple_job.rb +10 -3
- data/lib/sqewer/submitter.rb +22 -4
- data/lib/sqewer/version.rb +1 -1
- data/lib/sqewer/worker.rb +17 -41
- data/sqewer.gemspec +3 -3
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf1c2d6717fda50ce8a72721d00ffcfd2fae867f40f40a07a0cf48cbe94fe17b
|
4
|
+
data.tar.gz: fb1622a03a4f540aaa96d24dc3d5d39c5d400b658f371c88dc682844ac784ea5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd4e73838959531d646d0ed69fd8316e85075d3abe96b5362c58ff5de9af27b26cec142273bce674aaadf571275f1f91cf055c305be4b9797edd75eb89b74222
|
7
|
+
data.tar.gz: 9f4b9006f504bd4a0a85abea9d3c82bf2c70ccdfecf59095df58a1e6b1b8b3ed0b6ba819778f828c48e5dd1865c1f312c5feda78485e9ceab95565e9559cc870
|
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/.travis.yml
CHANGED
@@ -6,6 +6,8 @@ rvm:
|
|
6
6
|
- 2.3.7
|
7
7
|
- 2.4.5
|
8
8
|
- 2.5.1
|
9
|
+
- 2.6.5
|
10
|
+
- 2.7.0
|
9
11
|
cache: bundler
|
10
12
|
sudo: false
|
11
13
|
env:
|
@@ -19,3 +21,7 @@ matrix:
|
|
19
21
|
gemfile: gemfiles/Gemfile.rails-5.1.x
|
20
22
|
- rvm: 2.5.1
|
21
23
|
gemfile: gemfiles/Gemfile.rails-4.2.x
|
24
|
+
- rvm: 2.6.5
|
25
|
+
gemfile: gemfiles/Gemfile.rails-4.2.x
|
26
|
+
- rvm: 2.7.0
|
27
|
+
gemfile: gemfiles/Gemfile.rails-4.2.x
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
### 6.5.1
|
2
|
+
- Also retry on `Aws::SQS::Errors::InternalError` exception when receiving/sending messages. This will make
|
3
|
+
the receiving thread more resilient to sudden SQS failures. By the time SQS recovers the receiving thread
|
4
|
+
should stay alive.
|
5
|
+
|
6
|
+
### 6.5.0
|
7
|
+
- Adds `$stdout.sync = true` to CLI to flush the logs to STDOUT
|
8
|
+
|
9
|
+
### 6.4.1
|
10
|
+
- Retrieve attributes when receiving messages from SQS
|
11
|
+
|
12
|
+
### 6.4.0
|
13
|
+
- Raise an exception in submit! if the job serializes to a message that is
|
14
|
+
above the native SQS limit for message size.
|
15
|
+
- Ensure SendMessageBatch is only performed for batches totaling 256KB of message size or less.
|
16
|
+
- Insert Sqewer::Error between StandardError and our custom errors for easier rescuing
|
17
|
+
|
18
|
+
### 6.3.0
|
19
|
+
- Add support for Ruby 2.7
|
20
|
+
|
1
21
|
### 6.2.2
|
2
22
|
- Test the Appsignal integration using actual Appsignal libraries
|
3
23
|
- In the Appsignal integration, replace a call of `set_queue_start=` with `set_queue_start`
|
data/README.md
CHANGED
@@ -20,14 +20,14 @@ and to start processing, in your commandline handler:
|
|
20
20
|
|
21
21
|
#!/usr/bin/env ruby
|
22
22
|
require 'my_applicaion'
|
23
|
-
Sqewer::CLI.
|
23
|
+
Sqewer::CLI.start
|
24
24
|
|
25
25
|
To add arguments to the job
|
26
26
|
|
27
27
|
class JobWithArgs
|
28
28
|
include Sqewer::SimpleJob
|
29
29
|
attr_accessor :times
|
30
|
-
|
30
|
+
|
31
31
|
def run
|
32
32
|
...
|
33
33
|
end
|
@@ -88,6 +88,10 @@ Note that at this point only arguments that are raw JSON types are supported:
|
|
88
88
|
|
89
89
|
If you need marshalable Ruby types there instead, you might need to implement a custom `Serializer.`
|
90
90
|
|
91
|
+
### Sqewer::SimpleJob
|
92
|
+
|
93
|
+
The module `Sqewer::SimpleJob` can be included to a job class to add some features, specially dealing with attributes, see more details [here](https://github.com/WeTransfer/sqewer/blob/master/lib/sqewer/simple_job.rb).
|
94
|
+
|
91
95
|
## Jobs spawning dependent jobs
|
92
96
|
|
93
97
|
If your `run` method on the job object accepts arguments (has non-zero `arity` ) the `ExecutionContext` will
|
@@ -119,11 +123,11 @@ include all the keyword arguments needed to instantiate the job when executing.
|
|
119
123
|
def initialize(to:, body:)
|
120
124
|
...
|
121
125
|
end
|
122
|
-
|
126
|
+
|
123
127
|
def run()
|
124
128
|
...
|
125
129
|
end
|
126
|
-
|
130
|
+
|
127
131
|
def to_h
|
128
132
|
{to: @to, body: @body}
|
129
133
|
end
|
@@ -151,7 +155,7 @@ conform to the job serialization format used internally. For example, you can ha
|
|
151
155
|
message = JSON.load(message_blob)
|
152
156
|
return if message['Service'] # AWS test
|
153
157
|
return HandleS3Notification.new(message) if message['Records']
|
154
|
-
|
158
|
+
|
155
159
|
super # as default
|
156
160
|
end
|
157
161
|
end
|
@@ -176,7 +180,7 @@ The very minimal executable for running jobs would be this:
|
|
176
180
|
|
177
181
|
#!/usr/bin/env ruby
|
178
182
|
require 'my_applicaion'
|
179
|
-
Sqewer::CLI.
|
183
|
+
Sqewer::CLI.start
|
180
184
|
|
181
185
|
This will connect to the queue at the URL set in the `SQS_QUEUE_URL` environment variable, and
|
182
186
|
use all the default parameters. The `CLI` module will also set up a signal handler to terminate
|
@@ -233,9 +237,9 @@ you generate. For example, you could use a pipe. But in a more general case some
|
|
233
237
|
ActiveRAMGobbler.fetch_stupendously_many_things.each do |...|
|
234
238
|
end
|
235
239
|
end
|
236
|
-
|
240
|
+
|
237
241
|
_, status = Process.wait2(pid)
|
238
|
-
|
242
|
+
|
239
243
|
# Raise an error in the parent process to signal Sqewer that the job failed
|
240
244
|
# if the child exited with a non-0 status
|
241
245
|
raise "Child process crashed" unless status.exitstatus && status.exitstatus.zero?
|
@@ -252,7 +256,7 @@ You can wrap job processing in middleware. A full-featured middleware class look
|
|
252
256
|
# msg_id is the receipt handle, msg_payload is the message body string, msg_attributes are the message's attributes
|
253
257
|
yield
|
254
258
|
end
|
255
|
-
|
259
|
+
|
256
260
|
# Surrounds the actual job execution
|
257
261
|
def around_execution(job, context)
|
258
262
|
# job is the actual job you will be running, context is the ExecutionContext.
|
@@ -378,7 +382,7 @@ products using this library, was a very bad idea (more workload for deployment).
|
|
378
382
|
|
379
383
|
## Why so many configurable components?
|
380
384
|
|
381
|
-
Because sometimes your requirements differ just-a-little-bit from what is provided, and you have to swap your
|
385
|
+
Because sometimes your requirements differ just-a-little-bit from what is provided, and you have to swap your
|
382
386
|
implementation in instead. One product needs foreign-submitted SQS jobs (S3 notifications). Another product
|
383
387
|
needs a custom Logger subclass. Yet another product needs process-based concurrency on top of threads.
|
384
388
|
Yet another process needs to manage database connections when running the jobs. Have 3-4 of those, and a
|
@@ -398,7 +402,7 @@ Because I found that a producer-consumer model with a thread pool works quite we
|
|
398
402
|
the Ruby standard library alone.
|
399
403
|
|
400
404
|
## Contributing to the library
|
401
|
-
|
405
|
+
|
402
406
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
403
407
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
404
408
|
* Fork the project.
|
@@ -3,6 +3,6 @@ source "http://rubygems.org"
|
|
3
3
|
# Gemspec as base dependency set
|
4
4
|
gemspec path: __dir__ + '/..'
|
5
5
|
|
6
|
-
gem 'sqlite3',
|
7
|
-
gem 'activejob', "~> 5.1
|
8
|
-
gem 'activerecord', "~> 5.1
|
6
|
+
gem 'sqlite3', "~> 1.3", ">= 1.3.6"
|
7
|
+
gem 'activejob', "~> 5.1.0"
|
8
|
+
gem 'activerecord', "~> 5.1.0"
|
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/cli.rb
CHANGED
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)
|
@@ -52,11 +52,15 @@ class Sqewer::Connection
|
|
52
52
|
# even after 15 minutes it is either down or the server is misconfigured. Either way it makes no sense to
|
53
53
|
# continue.
|
54
54
|
#
|
55
|
-
# @return [Array<Message>] an array of Message objects
|
55
|
+
# @return [Array<Message>] an array of Message objects
|
56
56
|
def receive_messages
|
57
|
-
Retriable.retriable on:
|
58
|
-
response = client.receive_message(
|
59
|
-
|
57
|
+
Retriable.retriable on: network_and_aws_sdk_errors, tries: MAX_RANDOM_RECEIVE_FAILURES do
|
58
|
+
response = client.receive_message(
|
59
|
+
queue_url: @queue_url,
|
60
|
+
attribute_names: ['All'],
|
61
|
+
wait_time_seconds: DEFAULT_TIMEOUT_SECONDS,
|
62
|
+
max_number_of_messages: BATCH_RECEIVE_SIZE
|
63
|
+
)
|
60
64
|
response.messages.map {|message| Message.new(message.receipt_handle, message.body, message.attributes) }
|
61
65
|
end
|
62
66
|
end
|
@@ -65,7 +69,7 @@ class Sqewer::Connection
|
|
65
69
|
#
|
66
70
|
# @param message_body[String] the message to send
|
67
71
|
# @param kwargs_for_send[Hash] additional arguments for the submit (such as `delay_seconds`).
|
68
|
-
# Passes the arguments to the AWS SDK.
|
72
|
+
# Passes the arguments to the AWS SDK.
|
69
73
|
# @return [void]
|
70
74
|
def send_message(message_body, **kwargs_for_send)
|
71
75
|
send_multiple_messages {|via| via.send_message(message_body, **kwargs_for_send) }
|
@@ -91,6 +95,58 @@ class Sqewer::Connection
|
|
91
95
|
m[:delay_seconds] = kwargs_for_send[:delay_seconds] if kwargs_for_send[:delay_seconds]
|
92
96
|
messages << m
|
93
97
|
end
|
98
|
+
|
99
|
+
# each_batch here also needs to ensure that the sum of payload lengths does not exceed 256kb
|
100
|
+
def each_batch
|
101
|
+
regrouped = pack_into_batches(messages, weight_limit: 256 * 1024, batch_length_limit: 10) do |message|
|
102
|
+
message.fetch(:message_body).bytesize
|
103
|
+
end
|
104
|
+
regrouped.each { |b| yield(b) }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Optimizes a large list of items into batches of 10 items
|
108
|
+
# or less and with the sum of item lengths being below 256KB
|
109
|
+
# The block given to the method should return the weight of the given item
|
110
|
+
def pack_into_batches(items, weight_limit:, batch_length_limit:)
|
111
|
+
batches = []
|
112
|
+
current_batch = []
|
113
|
+
current_batch_weight = 0
|
114
|
+
|
115
|
+
# Sort the items by their weight (length of the body).
|
116
|
+
sorted_items = items.sort_by { |item| yield(item) }
|
117
|
+
|
118
|
+
# and then take 1 item from the list and append it to the batch if it fits.
|
119
|
+
# If it doesn't fit, no item after it will fit into this batch either (!)
|
120
|
+
# which is how we can optimize
|
121
|
+
sorted_items.each_with_index do |item|
|
122
|
+
weight_of_this_item = yield(item)
|
123
|
+
|
124
|
+
# First protect from invalid input
|
125
|
+
if weight_of_this_item > weight_limit
|
126
|
+
raise "#{item.inspect} was larger than the permissible limit"
|
127
|
+
# The first limit is on the item count per batch -
|
128
|
+
# if we are limited on that the batch needs to be closed
|
129
|
+
elsif current_batch.length == batch_length_limit
|
130
|
+
batches << current_batch
|
131
|
+
current_batch = []
|
132
|
+
current_batch_weight = 0
|
133
|
+
# If placing this item in the batch would make the batch overweight
|
134
|
+
# we need to close the batch, because all the items which come after
|
135
|
+
# this one will be same size or larger. This is the key part of the optimization.
|
136
|
+
elsif (current_batch_weight + weight_of_this_item) > weight_limit
|
137
|
+
batches << current_batch
|
138
|
+
current_batch = []
|
139
|
+
current_batch_weight = 0
|
140
|
+
end
|
141
|
+
|
142
|
+
# and then append the item to the current batch
|
143
|
+
current_batch_weight += weight_of_this_item
|
144
|
+
current_batch << item
|
145
|
+
end
|
146
|
+
batches << current_batch unless current_batch.empty?
|
147
|
+
|
148
|
+
batches
|
149
|
+
end
|
94
150
|
end
|
95
151
|
|
96
152
|
# Saves the receipt handles to batch-delete from the SQS queue
|
@@ -135,8 +191,12 @@ class Sqewer::Connection
|
|
135
191
|
|
136
192
|
private
|
137
193
|
|
194
|
+
def network_and_aws_sdk_errors
|
195
|
+
[NotOurFaultAwsError, Seahorse::Client::NetworkingError, Aws::SQS::Errors::InternalError]
|
196
|
+
end
|
197
|
+
|
138
198
|
def handle_batch_with_retries(method, batch)
|
139
|
-
Retriable.retriable on:
|
199
|
+
Retriable.retriable on: network_and_aws_sdk_errors, tries: MAX_RANDOM_FAILURES_PER_CALL do
|
140
200
|
resp = client.send(method, queue_url: @queue_url, entries: batch)
|
141
201
|
wrong_messages, aws_failures = resp.failed.partition {|m| m.sender_fault }
|
142
202
|
if wrong_messages.any?
|
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
@@ -4,9 +4,16 @@
|
|
4
4
|
# * initialize() will have keyword access to all accessors, and will ensure you have called each one of them
|
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
|
+
#
|
8
|
+
# This module validates if the attributes defined in the job class are the same as
|
9
|
+
# those persisted in the queue. More details on `Sqewer::SimpleJob#initialize`.
|
10
|
+
# Because of this, it's required to create a new job class when adding or removing
|
11
|
+
# an attribute.
|
12
|
+
# This mechanism guarantees strong consistency. Without it, a new deployed job class
|
13
|
+
# could process old incompatible payloads.
|
7
14
|
module Sqewer::SimpleJob
|
8
|
-
UnknownJobAttribute = Class.new(
|
9
|
-
MissingAttribute = Class.new(
|
15
|
+
UnknownJobAttribute = Class.new(Sqewer::Error)
|
16
|
+
MissingAttribute = Class.new(Sqewer::Error)
|
10
17
|
|
11
18
|
EQ_END = /(\w+)(\=)$/
|
12
19
|
|
@@ -53,7 +60,7 @@ module Sqewer::SimpleJob
|
|
53
60
|
accessor = "#{k}="
|
54
61
|
touched_attributes << k
|
55
62
|
unless respond_to?(accessor)
|
56
|
-
raise UnknownJobAttribute, "Unknown attribute #{k.inspect} for #{self.class}"
|
63
|
+
raise UnknownJobAttribute, "Unknown attribute #{k.inspect} for #{self.class}"
|
57
64
|
end
|
58
65
|
|
59
66
|
send("#{k}=", v)
|
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
|
@@ -91,7 +91,7 @@ class Sqewer::Worker
|
|
91
91
|
|
92
92
|
consumers = (1..@num_threads).map do
|
93
93
|
Thread.new do
|
94
|
-
|
94
|
+
loop { take_and_execute }
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
@@ -212,58 +212,34 @@ 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
|
229
233
|
message = @execution_queue.pop(nonblock=true)
|
230
234
|
handle_message(message)
|
231
235
|
rescue ThreadError # Queue is empty
|
232
|
-
|
233
|
-
|
236
|
+
if stopping?
|
237
|
+
Thread.current.exit
|
238
|
+
else
|
239
|
+
sleep SLEEP_SECONDS_ON_EMPTY_QUEUE
|
240
|
+
end
|
234
241
|
rescue => e # anything else, at or below StandardError that does not need us to quit
|
235
242
|
@logger.error { '[worker] Failed "%s..." with %s: %s' % [message.inspect[0..64], e.class, e.message] }
|
236
243
|
e.backtrace.each { |s| @logger.debug{"\t#{s}"} }
|
237
244
|
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
245
|
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}
|
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_runtime_dependency "retriable"
|
37
37
|
|
38
38
|
spec.add_development_dependency "bundler"
|
39
|
-
spec.add_development_dependency "rake", "~>
|
39
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
40
40
|
spec.add_development_dependency "rspec", "~> 3.0"
|
41
41
|
|
42
42
|
# The Rails deps can be relaxed, they are specified more exactly in the gemfiles/
|
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.5.1
|
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:
|
12
|
+
date: 2021-02-18 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: aws-sdk-sqs
|
@@ -100,14 +101,14 @@ dependencies:
|
|
100
101
|
requirements:
|
101
102
|
- - "~>"
|
102
103
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
104
|
+
version: '13.0'
|
104
105
|
type: :development
|
105
106
|
prerelease: false
|
106
107
|
version_requirements: !ruby/object:Gem::Requirement
|
107
108
|
requirements:
|
108
109
|
- - "~>"
|
109
110
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
111
|
+
version: '13.0'
|
111
112
|
- !ruby/object:Gem::Dependency
|
112
113
|
name: rspec
|
113
114
|
requirement: !ruby/object:Gem::Requirement
|
@@ -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
|