sqewer 6.3.0 → 6.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2286f82682e74eb695d111974d88a034471ad1d6de26b17198f8ff24c86ea697
4
- data.tar.gz: d9d79b6d8c47701959da1763df30de7b3ea9183c03be613d5cc0289b393d50be
3
+ metadata.gz: b0251b1700672c6dc8722779c0865d3757aff9de9ea747d4739863521931cc0c
4
+ data.tar.gz: 0533f556c4cad614cf7973818126996c8a8bea51d1791ca22df059ab65a5202a
5
5
  SHA512:
6
- metadata.gz: c3c49d0252a1a0fd8508491e3bd4d77558d2ee8e4b8def314899add6fa0619333914589ea80acbbbb12d9398e03ecea8e13b68747875d2af1a5b30d7c7512e42
7
- data.tar.gz: 2f732dca2866deb88d262aeabeca1109ed07bc9eaa553e1e95b120d884f675da6536364a00f95649aa2140cdc8498c3acecc2fa3f2949e94fbb7af92263e6d75
6
+ metadata.gz: 7d9d8f211697dc585517fd6c431e32f7500711888a3b97cf94aa7d4afbd043da2790407242a81a1ba37bca905cf6b454f73ee98a5015550768c948fe53eaa315
7
+ data.tar.gz: 38369a4c3a74e3bdb5616705744ad732a363757c3eeadb04dde327f77d9251633047851e446454a0126895acbaf194d44081c3afd0192a509992d592e164a2a5
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?--*
@@ -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
 
@@ -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
 
@@ -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)
@@ -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
@@ -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
@@ -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(StandardError)
9
- MissingAttribute = Class.new(StandardError)
8
+ UnknownJobAttribute = Class.new(Sqewer::Error)
9
+ MissingAttribute = Class.new(Sqewer::Error)
10
10
 
11
11
  EQ_END = /(\w+)(\=)$/
12
12
 
@@ -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 = '6.4.0'
3
3
  end
@@ -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 = 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
@@ -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
@@ -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.3.0
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-01-08 00:00:00.000000000 Z
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.1.2
288
+ rubygems_version: 3.0.3
287
289
  signing_key:
288
290
  specification_version: 4
289
291
  summary: Process jobs from SQS