sqewer 6.3.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2286f82682e74eb695d111974d88a034471ad1d6de26b17198f8ff24c86ea697
4
- data.tar.gz: d9d79b6d8c47701959da1763df30de7b3ea9183c03be613d5cc0289b393d50be
3
+ metadata.gz: acb88799499e49e2390422fd05b7c7a160bfec1f441347947941a8f15f62e3f1
4
+ data.tar.gz: d009682a2cf36c0a27daf683b6110a1a0ce6b3f1b35ba111ad391d57281aa4d1
5
5
  SHA512:
6
- metadata.gz: c3c49d0252a1a0fd8508491e3bd4d77558d2ee8e4b8def314899add6fa0619333914589ea80acbbbb12d9398e03ecea8e13b68747875d2af1a5b30d7c7512e42
7
- data.tar.gz: 2f732dca2866deb88d262aeabeca1109ed07bc9eaa553e1e95b120d884f675da6536364a00f95649aa2140cdc8498c3acecc2fa3f2949e94fbb7af92263e6d75
6
+ metadata.gz: 262f94efde1b86cbce03be08ce9a9c4cdd11de0bb95354c1f7a0adc29bdd4dc08c5d3d82d3377c5d9825dde7bbe1a7323214ed6d89c82d4ed97a56ffd27e0f98
7
+ data.tar.gz: 5375e768f53ad132fd5a6460dbe5bbf6b1457eaa514c54a1d445fb0f9b7ac30a4c852157877894766ae69da82c2e4efb76c42c4a0689f759ee9b83d909869752
data/.gitignore CHANGED
@@ -1,54 +1,56 @@
1
- .rspec
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
- # rcov generated
5
- coverage
6
- coverage.data
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
7
18
 
8
- # rdoc generated
9
- rdoc
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
10
26
 
11
- # yard generated
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
- # * Create a file at ~/.gitignore
26
- # * Include files you want ignored
27
- # * Run: git config --global core.excludesfile ~/.gitignore
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
- # After doing this, these files will be ignored in all your git projects,
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
- # For TextMate
39
- #*.tmproj
40
- #tmtags
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
41
40
 
42
- # For emacs:
43
- #*~
44
- #\#*
45
- #.\#*
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
46
45
 
47
- # For vim:
48
- #*.swp
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
- # For redcar:
51
- #.redcar
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
52
54
 
53
- # For rubinius:
54
- #*.rbc
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
data/.travis.yml CHANGED
@@ -1,11 +1,7 @@
1
1
  gemfile:
2
- - gemfiles/Gemfile.rails-4.2.x
3
2
  - gemfiles/Gemfile.rails-5.0.x
4
3
  - gemfiles/Gemfile.rails-5.1.x
5
4
  rvm:
6
- - 2.3.7
7
- - 2.4.5
8
- - 2.5.1
9
5
  - 2.6.5
10
6
  - 2.7.0
11
7
  cache: bundler
@@ -13,15 +9,3 @@ sudo: false
13
9
  env:
14
10
  global:
15
11
  - AWS_REGION=eu-central-1
16
- matrix:
17
- exclude:
18
- - rvm: 2.3.7
19
- gemfile: gemfiles/Gemfile.rails-5.0.x
20
- - rvm: 2.3.7
21
- gemfile: gemfiles/Gemfile.rails-5.1.x
22
- - rvm: 2.5.1
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,25 @@
1
+ ### 7.0.0
2
+ - Remove support of Ruby 2.3, 2.4 and 2.5
3
+ - Remove support of Rails 4
4
+ - Change `Sqewer::Connection` to preferentially use a singleton instance of `Aws::SQS::Client`, which can be set using `Sqewer.client=`. This avoids many HTTP requests to the AWS metadata endpoint when getting credentials.
5
+
6
+ ### 6.5.1
7
+ - Also retry on `Aws::SQS::Errors::InternalError` exception when receiving/sending messages. This will make
8
+ the receiving thread more resilient to sudden SQS failures. By the time SQS recovers the receiving thread
9
+ should stay alive.
10
+
11
+ ### 6.5.0
12
+ - Adds `$stdout.sync = true` to CLI to flush the logs to STDOUT
13
+
14
+ ### 6.4.1
15
+ - Retrieve attributes when receiving messages from SQS
16
+
17
+ ### 6.4.0
18
+ - Raise an exception in submit! if the job serializes to a message that is
19
+ above the native SQS limit for message size.
20
+ - Ensure SendMessageBatch is only performed for batches totaling 256KB of message size or less.
21
+ - Insert Sqewer::Error between StandardError and our custom errors for easier rescuing
22
+
1
23
  ### 6.3.0
2
24
  - Add support for Ruby 2.7
3
25
 
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.run
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
@@ -48,7 +48,7 @@ The messages will only be deleted from SQS once the job execution completes with
48
48
 
49
49
  ## Requirements
50
50
 
51
- Ruby 2.1+, version 2 of the AWS SDK. You can also run Sqewer backed by a SQLite database file, which can be handy for development situations.
51
+ Ruby 2.6+, version 2 of the AWS SDK. You can also run Sqewer backed by a SQLite database file, which can be handy for development situations.
52
52
 
53
53
  ## Job storage
54
54
 
@@ -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.run
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.
@@ -284,7 +288,7 @@ and traceable (make good use of logging).
284
288
 
285
289
  # Usage with Rails via ActiveJob
286
290
 
287
- This gem includes a queue adapter for usage with ActiveJob in Rails 4.2+. The functionality
291
+ This gem includes a queue adapter for usage with ActiveJob in Rails 5+. The functionality
288
292
  is well-tested and should function for any well-conforming ActiveJob subclasses.
289
293
 
290
294
  To run the default `sqewer` worker setup against your Rails application, first set it as the
@@ -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.
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "sqewer"
6
+
7
+ require "irb"
8
+ IRB.start(__FILE__)
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|
@@ -8,6 +11,25 @@ module Sqewer
8
11
  end
9
12
  end
10
13
 
14
+ # Sets an instance of Aws::SQS::Client to be used as a singleton.
15
+ # We recommend setting the options instance_profile_credentials_timeout and
16
+ # instance_profile_credentials_retries, for example:
17
+ #
18
+ # sqs_client = Aws::SQS::Client.new(
19
+ # instance_profile_credentials_timeout: 1,
20
+ # instance_profile_credentials_retries: 5,
21
+ # )
22
+ # Storm.client = sqs_client
23
+ #
24
+ # @param client[Aws::SQS::Client] an instance of Aws::SQS::Client
25
+ def self.client=(client)
26
+ @client = client
27
+ end
28
+
29
+ def self.client
30
+ @client
31
+ end
32
+
11
33
  # Loads a particular Sqewer extension that is not loaded
12
34
  # automatically during the gem require.
13
35
  #
@@ -23,7 +45,7 @@ module Sqewer
23
45
  def self.submit!(*jobs, **options)
24
46
  Sqewer::Submitter.default.submit!(*jobs, **options)
25
47
  end
26
-
48
+
27
49
  # If we are within Rails, load the railtie
28
50
  require_relative 'sqewer/extensions/railtie' if defined?(Rails)
29
51
 
data/lib/sqewer/cli.rb CHANGED
@@ -1,3 +1,4 @@
1
+ $stdout.sync = true
1
2
  # Wraps a Worker object in a process-wide commanline handler. Once the `start` method is
2
3
  # called, signal handlers will be installed for the following signals:
3
4
  #
@@ -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(StandardError)
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: Seahorse::Client::NetworkingError, tries: MAX_RANDOM_RECEIVE_FAILURES do
58
- response = client.receive_message(queue_url: @queue_url,
59
- wait_time_seconds: DEFAULT_TIMEOUT_SECONDS, max_number_of_messages: BATCH_RECEIVE_SIZE)
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: [NotOurFaultAwsError, Seahorse::Client::NetworkingError], tries: MAX_RANDOM_FAILURES_PER_CALL do
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?
@@ -151,6 +211,11 @@ class Sqewer::Connection
151
211
  end
152
212
 
153
213
  def client
214
+ # It's better using a singleton client to prevent making a lot of HTTP
215
+ # requests to the AWS metadata endpoint when getting credentials.
216
+ # Maybe in the future, we can remove @client and use Storm.client only.
217
+ return Sqewer.client if Sqewer.client
218
+
154
219
  @client ||= Aws::SQS::Client.new(
155
220
  instance_profile_credentials_timeout: 1, # defaults to 1 second
156
221
  instance_profile_credentials_retries: 5, # defaults to 0 retries
@@ -4,7 +4,6 @@ module Sqewer
4
4
  # to Appsignal and to monitor performance. Will only activate
5
5
  # if the Appsignal gem is loaded within the current process and active.
6
6
  class AppsignalWrapper
7
-
8
7
  def self.new
9
8
  if defined?(Appsignal)
10
9
  super
@@ -13,7 +13,7 @@ class Sqewer::Serializer
13
13
  @instance ||= new
14
14
  end
15
15
 
16
- AnonymousJobClass = Class.new(StandardError)
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
@@ -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(StandardError)
9
- MissingAttribute = Class.new(StandardError)
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)
@@ -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
- NotSqewerJob = Class.new(StandardError)
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
- raise NotSqewerJob.new("Submitted object is not a valid job: #{job.inspect}") unless job.respond_to?(:run)
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
@@ -1,3 +1,3 @@
1
1
  module Sqewer
2
- VERSION = '6.3.0'
2
+ VERSION = '7.0.0'
3
3
  end
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
- catch(:goodbye) { loop {take_and_execute} }
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 = Time.now
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 = Time.now - t
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
- throw :goodbye if stopping?
233
- sleep SLEEP_SECONDS_ON_EMPTY_QUEUE
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,12 +6,13 @@ 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}
14
14
  spec.homepage = "https://github.com/WeTransfer/sqewer"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
15
16
 
16
17
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host"
17
18
  # to allow pushing to a single host or delete this section to allow pushing to any host.
@@ -36,7 +37,7 @@ Gem::Specification.new do |spec|
36
37
  spec.add_runtime_dependency "retriable"
37
38
 
38
39
  spec.add_development_dependency "bundler"
39
- spec.add_development_dependency "rake", "~> 12.3"
40
+ spec.add_development_dependency "rake", "~> 13.0"
40
41
  spec.add_development_dependency "rspec", "~> 3.0"
41
42
 
42
43
  # 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.3.0
4
+ version: 7.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
- autorequire:
8
+ - Andrei Horak
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2020-01-08 00:00:00.000000000 Z
12
+ date: 2021-04-08 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: '12.3'
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: '12.3'
111
+ version: '13.0'
111
112
  - !ruby/object:Gem::Dependency
112
113
  name: rspec
113
114
  requirement: !ruby/object:Gem::Requirement
@@ -223,7 +224,9 @@ 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:
229
+ - console
227
230
  - sqewer
228
231
  - sqewer_rails
229
232
  extensions: []
@@ -237,10 +240,10 @@ files:
237
240
  - Gemfile
238
241
  - README.md
239
242
  - Rakefile
243
+ - bin/console
240
244
  - bin/sqewer
241
245
  - bin/sqewer_rails
242
246
  - example.env
243
- - gemfiles/Gemfile.rails-4.2.x
244
247
  - gemfiles/Gemfile.rails-5.0.x
245
248
  - gemfiles/Gemfile.rails-5.1.x
246
249
  - lib/sqewer.rb
@@ -268,7 +271,7 @@ homepage: https://github.com/WeTransfer/sqewer
268
271
  licenses: []
269
272
  metadata:
270
273
  allowed_push_host: https://rubygems.org
271
- post_install_message:
274
+ post_install_message:
272
275
  rdoc_options: []
273
276
  require_paths:
274
277
  - lib
@@ -276,15 +279,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
276
279
  requirements:
277
280
  - - ">="
278
281
  - !ruby/object:Gem::Version
279
- version: '0'
282
+ version: 2.6.0
280
283
  required_rubygems_version: !ruby/object:Gem::Requirement
281
284
  requirements:
282
285
  - - ">="
283
286
  - !ruby/object:Gem::Version
284
287
  version: '0'
285
288
  requirements: []
286
- rubygems_version: 3.1.2
287
- signing_key:
289
+ rubygems_version: 3.0.3
290
+ signing_key:
288
291
  specification_version: 4
289
292
  summary: Process jobs from SQS
290
293
  test_files: []
@@ -1,8 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- # Gemspec as base dependency set
4
- gemspec path: __dir__ + '/..'
5
-
6
- gem 'sqlite3', '~> 1.3.6'
7
- gem 'activejob', "~> 4.2.0"
8
- gem 'activerecord', "~> 4.2.0"