chore-core 1.10.0 → 4.0.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.
Files changed (60) hide show
  1. checksums.yaml +5 -13
  2. data/LICENSE.txt +1 -1
  3. data/README.md +172 -153
  4. data/chore-core.gemspec +3 -3
  5. data/lib/chore.rb +29 -5
  6. data/lib/chore/cli.rb +22 -4
  7. data/lib/chore/configuration.rb +1 -1
  8. data/lib/chore/consumer.rb +54 -12
  9. data/lib/chore/fetcher.rb +12 -7
  10. data/lib/chore/hooks.rb +2 -1
  11. data/lib/chore/job.rb +19 -0
  12. data/lib/chore/manager.rb +17 -2
  13. data/lib/chore/publisher.rb +18 -2
  14. data/lib/chore/queues/filesystem/consumer.rb +126 -64
  15. data/lib/chore/queues/filesystem/filesystem_queue.rb +19 -0
  16. data/lib/chore/queues/filesystem/publisher.rb +10 -16
  17. data/lib/chore/queues/sqs.rb +22 -13
  18. data/lib/chore/queues/sqs/consumer.rb +64 -51
  19. data/lib/chore/queues/sqs/publisher.rb +26 -17
  20. data/lib/chore/strategies/consumer/batcher.rb +6 -6
  21. data/lib/chore/strategies/consumer/single_consumer_strategy.rb +5 -5
  22. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +7 -6
  23. data/lib/chore/strategies/consumer/throttled_consumer_strategy.rb +120 -0
  24. data/lib/chore/strategies/worker/forked_worker_strategy.rb +5 -6
  25. data/lib/chore/strategies/worker/helpers/ipc.rb +87 -0
  26. data/lib/chore/strategies/worker/helpers/preforked_worker.rb +163 -0
  27. data/lib/chore/strategies/worker/helpers/work_distributor.rb +65 -0
  28. data/lib/chore/strategies/worker/helpers/worker_info.rb +13 -0
  29. data/lib/chore/strategies/worker/helpers/worker_killer.rb +40 -0
  30. data/lib/chore/strategies/worker/helpers/worker_manager.rb +183 -0
  31. data/lib/chore/strategies/worker/preforked_worker_strategy.rb +150 -0
  32. data/lib/chore/unit_of_work.rb +2 -1
  33. data/lib/chore/util.rb +5 -1
  34. data/lib/chore/version.rb +2 -2
  35. data/lib/chore/worker.rb +30 -3
  36. data/spec/chore/cli_spec.rb +2 -2
  37. data/spec/chore/consumer_spec.rb +1 -5
  38. data/spec/chore/duplicate_detector_spec.rb +17 -5
  39. data/spec/chore/fetcher_spec.rb +0 -11
  40. data/spec/chore/manager_spec.rb +7 -0
  41. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +74 -16
  42. data/spec/chore/queues/sqs/consumer_spec.rb +117 -78
  43. data/spec/chore/queues/sqs/publisher_spec.rb +49 -60
  44. data/spec/chore/queues/sqs_spec.rb +32 -41
  45. data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +3 -3
  46. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +6 -6
  47. data/spec/chore/strategies/consumer/throttled_consumer_strategy_spec.rb +165 -0
  48. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +6 -1
  49. data/spec/chore/strategies/worker/helpers/ipc_spec.rb +127 -0
  50. data/spec/chore/strategies/worker/helpers/preforked_worker_spec.rb +236 -0
  51. data/spec/chore/strategies/worker/helpers/work_distributor_spec.rb +131 -0
  52. data/spec/chore/strategies/worker/helpers/worker_info_spec.rb +14 -0
  53. data/spec/chore/strategies/worker/helpers/worker_killer_spec.rb +97 -0
  54. data/spec/chore/strategies/worker/helpers/worker_manager_spec.rb +304 -0
  55. data/spec/chore/strategies/worker/preforked_worker_strategy_spec.rb +183 -0
  56. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +1 -1
  57. data/spec/chore/worker_spec.rb +70 -15
  58. data/spec/spec_helper.rb +1 -1
  59. data/spec/support/queues/sqs/fake_objects.rb +18 -0
  60. metadata +53 -29
@@ -15,61 +15,111 @@ module Chore
15
15
  # Once complete job files are deleted.
16
16
  # If rejected they are moved back into new and will be processed again. This may not be the
17
17
  # desired behavior long term and we may want to add configuration to this class to allow more
18
- # creating failure handling and retrying.
18
+ # creating failure handling and retrying.
19
19
  class Consumer < Chore::Consumer
20
20
  extend FilesystemQueue
21
21
 
22
22
  Chore::CLI.register_option 'fs_queue_root', '--fs-queue-root DIRECTORY', 'Root directory for fs based queue'
23
-
24
- FILE_QUEUE_MUTEXES = {}
25
23
 
26
24
  class << self
27
- # Cleans up the in-progress files by making them new again. This should only
28
- # happen once per process.
29
- def cleanup(queue)
30
- new_dir = self.new_dir(queue)
31
- in_progress_dir = self.in_progress_dir(queue)
32
-
33
- job_files(in_progress_dir).each do |file|
34
- make_new_again(file, new_dir, in_progress_dir)
25
+ # Cleans up expired in-progress files by making them new again.
26
+ def cleanup(expiration_time, new_dir, in_progress_dir)
27
+ each_file(in_progress_dir) do |job_file|
28
+ id, previous_attempts, timestamp = file_info(job_file)
29
+ next if timestamp > expiration_time
30
+
31
+ begin
32
+ make_new_again(job_file, new_dir, in_progress_dir)
33
+ rescue Errno::ENOENT
34
+ # File no longer exists; skip since it's been recovered by another
35
+ # consumer
36
+ rescue ArgumentError
37
+ # Move operation was attempted at same time as another consumer;
38
+ # skip since the other process succeeded where this one didn't
39
+ end
35
40
  end
36
41
  end
37
42
 
38
- def make_in_progress(job, new_dir, in_progress_dir)
39
- move_job(File.join(new_dir, job), File.join(in_progress_dir, job))
43
+ # Moves job file to inprogress directory and returns the full path
44
+ # if the job was successfully locked by this consumer
45
+ def make_in_progress(job, new_dir, in_progress_dir, queue_timeout)
46
+ basename, previous_attempts, * = file_info(job)
47
+
48
+ from = File.join(new_dir, job)
49
+ # Add a timestamp to mark when the job was started
50
+ to = File.join(in_progress_dir, "#{basename}.#{previous_attempts}.#{Time.now.to_i}.job")
51
+
52
+ # If the file is non-zero, this means it was successfully written to
53
+ # by a publisher and we can attempt to move it to "in progress".
54
+ #
55
+ # There is a small window of time where the file can be zero, but
56
+ # the publisher hasn't finished writing to the file yet.
57
+ if !File.zero?(from)
58
+ File.open(from, "r") do |f|
59
+ # If the lock can't be obtained, that means it's been locked
60
+ # by another consumer or the publisher of the file) -- don't
61
+ # block and skip it
62
+ if f.flock(File::LOCK_EX | File::LOCK_NB)
63
+ FileUtils.mv(from, to)
64
+ to
65
+ end
66
+ end
67
+ elsif (Time.now - File.ctime(from)) >= queue_timeout
68
+ # The file is empty (zero bytes) and enough time has passed since
69
+ # the file was written that we can safely assume it will never
70
+ # get written to be the publisher.
71
+ #
72
+ # The scenario where this happens is when the publisher created
73
+ # the file, but the process was killed before it had a chance to
74
+ # actually write the data.
75
+ File.delete(from)
76
+ nil
77
+ end
78
+ rescue Errno::ENOENT
79
+ # File no longer exists; skip it since it's been picked up by
80
+ # another consumer
40
81
  end
41
82
 
83
+ # Moves job file to new directory and returns the full path
42
84
  def make_new_again(job, new_dir, in_progress_dir)
43
85
  basename, previous_attempts = file_info(job)
44
- move_job(File.join(in_progress_dir, job), File.join(new_dir, "#{basename}.#{previous_attempts + 1}.job"))
45
- end
46
86
 
47
- # Moves job file to inprogress directory and returns the full path
48
- def move_job(from, to)
49
- f = File.open(from, "r")
50
- # wait on the lock a publisher in another process might have.
51
- # Once we get the lock the file is ours to move to mark it in progress
52
- f.flock(File::LOCK_EX)
53
- begin
54
- FileUtils.mv(f.path, to)
55
- ensure
56
- f.flock(File::LOCK_UN) # yes we can unlock it after its been moved, I checked
57
- end
87
+ from = File.join(in_progress_dir, job)
88
+ to = File.join(new_dir, "#{basename}.#{previous_attempts + 1}.job")
89
+ FileUtils.mv(from, to)
90
+
58
91
  to
59
92
  end
60
93
 
61
- def job_files(dir)
62
- Dir.entries(dir).select{|e| ! e.start_with?(".")}
94
+ def each_file(path, limit = nil)
95
+ count = 0
96
+
97
+ Dir.foreach(path) do |file|
98
+ next if file.start_with?('.')
99
+
100
+ yield file
101
+
102
+ count += 1
103
+ break if limit && count >= limit
104
+ end
63
105
  end
64
106
 
65
107
  # Grabs the unique identifier for the job filename and the number of times
66
108
  # it's been attempted (also based on the filename)
67
109
  def file_info(job_file)
68
- id, previous_attempts = File.basename(job_file, '.job').split('.')
69
- [id, previous_attempts.to_i]
110
+ id, previous_attempts, timestamp, * = job_file.split('.')
111
+ [id, previous_attempts.to_i, timestamp.to_i]
70
112
  end
71
113
  end
72
114
 
115
+ # The minimum number of seconds to allow to pass between checks for expired
116
+ # jobs on the filesystem.
117
+ #
118
+ # Since queue times are measured on the order of seconds, 1 second is the
119
+ # smallest duration. It also prevents us from burning a lot of CPU looking
120
+ # at expired jobs when the consumer sleep interval is less than 1 second.
121
+ EXPIRATION_CHECK_INTERVAL = 1
122
+
73
123
  # The amount of time units of work can run before the queue considers
74
124
  # them timed out. For filesystem queues, this is the global default.
75
125
  attr_reader :queue_timeout
@@ -77,66 +127,79 @@ module Chore
77
127
  def initialize(queue_name, opts={})
78
128
  super(queue_name, opts)
79
129
 
80
- # Even though putting these Mutexes in this hash is, by itself, not particularly threadsafe
81
- # as long as some Mutex ends up in the queue after all consumers are created we're good
82
- # as they are pulled from the queue and synchronized for file operations below
83
- FILE_QUEUE_MUTEXES[@queue_name] ||= Mutex.new
84
-
85
130
  @in_progress_dir = self.class.in_progress_dir(queue_name)
86
131
  @new_dir = self.class.new_dir(queue_name)
87
- @queue_timeout = Chore.config.default_queue_timeout
132
+ @queue_timeout = self.class.queue_timeout(queue_name)
88
133
  end
89
134
 
90
- def consume(&handler)
135
+ def consume
91
136
  Chore.logger.info "Starting consuming file system queue #{@queue_name} in #{self.class.queue_dir(queue_name)}"
92
137
  while running?
93
138
  begin
94
- #TODO move expired job files to new directory?
95
- handle_jobs(&handler)
139
+ # Move expired job files to new directory (so long as enough time has
140
+ # passed since we last did this check)
141
+ if !@last_cleaned_at || (Time.now - @last_cleaned_at).to_i >= EXPIRATION_CHECK_INTERVAL
142
+ self.class.cleanup(Time.now.to_i - @queue_timeout, @new_dir, @in_progress_dir)
143
+ @last_cleaned_at = Time.now
144
+ end
145
+
146
+ found_files = false
147
+ handle_messages do |*args|
148
+ found_files = true
149
+ yield(*args)
150
+ end
96
151
  rescue => e
97
152
  Chore.logger.error { "#{self.class}#consume: #{e} #{e.backtrace * "\n"}" }
98
153
  ensure
99
- sleep 5
154
+ sleep(Chore.config.consumer_sleep_interval) unless found_files
100
155
  end
101
156
  end
102
157
  end
103
158
 
159
+ # Rejects the given message from the filesystem by +id+. Currently a noop
104
160
  def reject(id)
105
- Chore.logger.debug "Rejecting: #{id}"
106
- make_new_again(id)
161
+
107
162
  end
108
163
 
109
- def complete(id)
110
- Chore.logger.debug "Completing (deleting): #{id}"
111
- FileUtils.rm_f(File.join(@in_progress_dir, id))
164
+ # Deletes the given message from filesystem queue. Since the filesystem is not a remote API, there is no
165
+ # notion of a "receipt handle".
166
+ #
167
+ # @param [String] message_id Unique ID of the message
168
+ # @param [Hash] receipt_handle Receipt handle of the message. Always nil for the filesystem consumer
169
+ def complete(message_id, receipt_handle = nil)
170
+ Chore.logger.debug "Completing (deleting): #{message_id}"
171
+ File.delete(File.join(@in_progress_dir, message_id))
172
+ rescue Errno::ENOENT
173
+ # The job took too long to complete, was deemed expired, and moved
174
+ # back into "new". Ignore.
112
175
  end
113
176
 
114
177
  private
115
178
 
116
179
  # finds all new job files, moves them to in progress and starts the job
117
180
  # Returns a list of the job files processed
118
- def handle_jobs(&block)
119
- # all consumers on a single queue share a lock on handling files.
120
- # Each consumer comes along, processes all present files and release the lock.
121
- # This isn't particularly useful but is here to allow the configuration of
122
- # ThreadedConsumerStrategy with mutiple threads on a queue safely although you
123
- # probably wouldn't want to do that.
124
- FILE_QUEUE_MUTEXES[@queue_name].synchronize do
125
- self.class.job_files(@new_dir).each do |job_file|
126
- Chore.logger.debug "Found a new job #{job_file}"
127
-
128
- job_json = File.read(make_in_progress(job_file))
129
- basename, previous_attempts = self.class.file_info(job_file)
130
-
131
- # job_file is just the name which is the job id
132
- block.call(job_file, queue_name, queue_timeout, job_json, previous_attempts)
133
- Chore.run_hooks_for(:on_fetch, job_file, job_json)
134
- end
181
+ def handle_messages(&block)
182
+ self.class.each_file(@new_dir, Chore.config.queue_polling_size) do |job_file|
183
+ Chore.logger.debug "Found a new job #{job_file}"
184
+
185
+ in_progress_path = make_in_progress(job_file)
186
+ next unless in_progress_path
187
+
188
+ # The job filename may have changed, so update it to reflect the in progress path
189
+ job_file = File.basename(in_progress_path)
190
+
191
+ job_json = File.read(in_progress_path)
192
+ basename, previous_attempts, * = self.class.file_info(job_file)
193
+
194
+ # job_file is just the name which is the job id. 2nd argument (:receipt_handle) is nil because the
195
+ # filesystem is dealt with directly, as opposed to being an external API
196
+ block.call(job_file, nil, queue_name, queue_timeout, job_json, previous_attempts)
197
+ Chore.run_hooks_for(:on_fetch, job_file, job_json)
135
198
  end
136
199
  end
137
200
 
138
201
  def make_in_progress(job)
139
- self.class.make_in_progress(job, @new_dir, @in_progress_dir)
202
+ self.class.make_in_progress(job, @new_dir, @in_progress_dir, @queue_timeout)
140
203
  end
141
204
 
142
205
  def make_new_again(job)
@@ -146,4 +209,3 @@ module Chore
146
209
  end
147
210
  end
148
211
  end
149
-
@@ -6,6 +6,8 @@ module Chore::FilesystemQueue
6
6
  NEW_JOB_DIR = "new"
7
7
  # Local directory for jobs currently in-process to be moved
8
8
  IN_PROGRESS_DIR = "inprogress"
9
+ # Local directory for configuration info
10
+ CONFIG_DIR = "config"
9
11
 
10
12
  # Retrieves the directory for in-process messages to go. If the directory for the +queue_name+ doesn't exist,
11
13
  # it will be created for you. If the directory cannot be created, an IOError will be raised
@@ -29,6 +31,23 @@ module Chore::FilesystemQueue
29
31
  prepare_dir(File.join(root_dir, queue_name))
30
32
  end
31
33
 
34
+ # The configuration for the given queue
35
+ def config_dir(queue_name)
36
+ validate_dir(queue_name, CONFIG_DIR)
37
+ end
38
+
39
+ def config_value(queue_name, config_name)
40
+ config_file = File.join(config_dir(queue_name), config_name)
41
+ if File.exists?(config_file)
42
+ File.read(config_file).strip
43
+ end
44
+ end
45
+
46
+ # Returns the timeout for +queue_name+
47
+ def queue_timeout(queue_name)
48
+ (config_value(queue_name, 'timeout') || Chore.config.default_queue_timeout).to_i
49
+ end
50
+
32
51
  private
33
52
  # Returns the directory for the given +queue_name+ and +task_state+. If the directory doesn't exist, it will be
34
53
  # created for you. If the directory cannot be created, an IOError will be raised
@@ -10,27 +10,20 @@ module Chore
10
10
  # See the top of FilesystemConsumer for comments on how this works
11
11
  include FilesystemQueue
12
12
 
13
- # Mutex for holding a lock over the files for this queue while they are in process
14
- FILE_MUTEX = Mutex.new
15
-
16
13
  # use of mutex and file locking should make this both threadsafe and safe for multiple
17
- # processes to use the same queue directory simultaneously.
14
+ # processes to use the same queue directory simultaneously.
18
15
  def publish(queue_name,job)
19
16
  # First try encoding the job to avoid writing empty job files if this fails
20
17
  encoded_job = encode_job(job)
21
18
 
22
- FILE_MUTEX.synchronize do
23
- while true
24
- # keep trying to get a file with nothing in it meaning we just created it
25
- # as opposed to us getting someone else's file that hasn't been processed yet.
26
- f = File.open(filename(queue_name, job[:class].to_s), "w")
19
+ published = false
20
+ while !published
21
+ # keep trying to get a file with nothing in it meaning we just created it
22
+ # as opposed to us getting someone else's file that hasn't been processed yet.
23
+ File.open(filename(queue_name, job[:class].to_s), "a") do |f|
27
24
  if f.flock(File::LOCK_EX | File::LOCK_NB) && f.size == 0
28
- begin
29
- f.write(encoded_job)
30
- ensure
31
- f.flock(File::LOCK_UN)
32
- end
33
- break
25
+ f.write(encoded_job)
26
+ published = true
34
27
  end
35
28
  end
36
29
  end
@@ -40,7 +33,8 @@ module Chore
40
33
  def filename(queue_name, job_name)
41
34
  now = Time.now.strftime "%Y%m%d-%H%M%S-%6N"
42
35
  previous_attempts = 0
43
- File.join(new_dir(queue_name), "#{queue_name}-#{job_name}-#{now}.#{previous_attempts}.job")
36
+ pid = Process.pid
37
+ File.join(new_dir(queue_name), "#{queue_name}-#{job_name}-#{pid}-#{now}.#{previous_attempts}.job")
44
38
  end
45
39
  end
46
40
  end
@@ -1,10 +1,18 @@
1
1
  module Chore
2
2
  module Queues
3
3
  module SQS
4
+ def self.sqs_client
5
+ Aws::SQS::Client.new(logger: Chore.logger, log_level: Chore.log_level_to_sym)
6
+ end
7
+
4
8
  # Helper method to create queues based on the currently known list as provided by your configured Chore::Jobs
5
9
  # This is meant to be invoked from a rake task, and not directly.
6
10
  # These queues will be created with the default settings, which may not be ideal.
7
11
  # This is meant only as a convenience helper for testing, and not as a way to create production quality queues in SQS
12
+ #
13
+ # @param [TrueClass, FalseClass] halt_on_existing Raise an exception if the queue already exists
14
+ #
15
+ # @return [Array<String>]
8
16
  def self.create_queues!(halt_on_existing=false)
9
17
  raise 'You must have atleast one Chore Job configured and loaded before attempting to create queues' unless Chore.prefixed_queue_names.length > 0
10
18
 
@@ -20,49 +28,50 @@ module Chore
20
28
  end
21
29
  end
22
30
 
23
- #This will raise an exception if AWS has not been configured by the project making use of Chore
24
- sqs_queues = AWS::SQS.new.queues
25
31
  Chore.prefixed_queue_names.each do |queue_name|
26
32
  Chore.logger.info "Chore Creating Queue: #{queue_name}"
27
33
  begin
28
- sqs_queues.create(queue_name)
29
- rescue AWS::SQS::Errors::QueueAlreadyExists
34
+ sqs_client.create_queue(queue_name: queue_name)
35
+ rescue Aws::SQS::Errors::QueueAlreadyExists
30
36
  Chore.logger.info "exists with different config"
31
37
  end
32
38
  end
39
+
33
40
  Chore.prefixed_queue_names
34
41
  end
35
42
 
36
43
  # Helper method to delete all known queues based on the list as provided by your configured Chore::Jobs
37
44
  # This is meant to be invoked from a rake task, and not directly.
45
+ #
46
+ # @return [Array<String>]
47
+
38
48
  def self.delete_queues!
39
49
  raise 'You must have atleast one Chore Job configured and loaded before attempting to create queues' unless Chore.prefixed_queue_names.length > 0
40
- #This will raise an exception if AWS has not been configured by the project making use of Chore
41
- sqs_queues = AWS::SQS.new.queues
50
+
42
51
  Chore.prefixed_queue_names.each do |queue_name|
43
52
  begin
44
53
  Chore.logger.info "Chore Deleting Queue: #{queue_name}"
45
- url = sqs_queues.url_for(queue_name)
46
- sqs_queues[url].delete
54
+ url = sqs_client.get_queue_url(queue_name: queue_name).queue_url
55
+ sqs_client.delete_queue(queue_url: url)
47
56
  rescue => e
48
57
  # This could fail for a few reasons - log out why it failed, then continue on
49
58
  Chore.logger.error "Deleting Queue: #{queue_name} failed because #{e}"
50
59
  end
51
60
  end
61
+
52
62
  Chore.prefixed_queue_names
53
63
  end
54
64
 
55
65
  # Collect a list of queues that already exist
66
+ #
67
+ # @return [Array<String>]
56
68
  def self.existing_queues
57
- #This will raise an exception if AWS has not been configured by the project making use of Chore
58
- sqs_queues = AWS::SQS.new.queues
59
-
60
69
  Chore.prefixed_queue_names.select do |queue_name|
61
70
  # If the NonExistentQueue exception is raised we do not care about that queue name.
62
71
  begin
63
- sqs_queues.named(queue_name)
72
+ sqs_client.get_queue_url(queue_name: queue_name)
64
73
  true
65
- rescue AWS::SQS::Errors::NonExistentQueue
74
+ rescue Aws::SQS::Errors::NonExistentQueue
66
75
  false
67
76
  end
68
77
  end
@@ -1,9 +1,6 @@
1
- require 'aws/sqs'
1
+ require 'aws-sdk-sqs'
2
2
  require 'chore/duplicate_detector'
3
3
 
4
- AWS.eager_autoload! AWS::Core
5
- AWS.eager_autoload! AWS::SQS
6
-
7
4
  module Chore
8
5
  module Queues
9
6
  module SQS
@@ -16,26 +13,30 @@ module Chore
16
13
  Chore::CLI.register_option 'aws_access_key', '--aws-access-key KEY', 'Valid AWS Access Key'
17
14
  Chore::CLI.register_option 'aws_secret_key', '--aws-secret-key KEY', 'Valid AWS Secret Key'
18
15
  Chore::CLI.register_option 'dedupe_servers', '--dedupe-servers SERVERS', 'List of mememcache compatible server(s) to use for storing SQS Message Dedupe cache'
19
- Chore::CLI.register_option 'queue_polling_size', '--queue_polling_size NUM', Integer, 'Amount of messages to grab on each request' do |arg|
20
- raise ArgumentError, "Cannot specify a queue polling size greater than 10" if arg > 10
21
- end
22
16
 
17
+ # @param [String] queue_name Name of SQS queue
18
+ # @param [Hash] opts Options
23
19
  def initialize(queue_name, opts={})
24
20
  super(queue_name, opts)
21
+ raise Chore::TerribleMistake, "Cannot specify a queue polling size greater than 10" if sqs_polling_amount > 10
25
22
  end
26
23
 
27
- # Sets a flag that instructs the publisher to reset the connection the next time it's used
24
+ # Resets the API client connection and provides @@reset_at so we know when the last time that was done
28
25
  def self.reset_connection!
29
26
  @@reset_at = Time.now
30
27
  end
31
28
 
32
29
  # Begins requesting messages from SQS, which will invoke the +&handler+ over each message
30
+ #
31
+ # @param [Block] &handler Message handler, used by the calling context (worker) to create & assigns a UnitOfWork
32
+ #
33
+ # @return [Array<Aws::SQS::Message>]
33
34
  def consume(&handler)
34
35
  while running?
35
36
  begin
36
37
  messages = handle_messages(&handler)
37
- sleep (Chore.config.consumer_sleep_interval || 1) if messages.empty?
38
- rescue AWS::SQS::Errors::NonExistentQueue => e
38
+ sleep (Chore.config.consumer_sleep_interval) if messages.empty?
39
+ rescue Aws::SQS::Errors::NonExistentQueue => e
39
40
  Chore.logger.error "You specified a queue '#{queue_name}' that does not exist. You must create the queue before starting Chore. Shutting down..."
40
41
  raise Chore::TerribleMistake
41
42
  rescue => e
@@ -44,21 +45,34 @@ module Chore
44
45
  end
45
46
  end
46
47
 
47
- # Rejects the given message from SQS by +id+. Currently a noop
48
- def reject(id)
49
-
48
+ # Unimplemented. Rejects the given message from SQS.
49
+ #
50
+ # @param [String] message_id Unique ID of the SQS message
51
+ #
52
+ # @return nil
53
+ def reject(message_id)
50
54
  end
51
55
 
52
- # Deletes the given message from SQS by +id+
53
- def complete(id)
54
- Chore.logger.debug "Completing (deleting): #{id}"
55
- queue.batch_delete([id])
56
+ # Deletes the given message from the SQS queue
57
+ #
58
+ # @param [String] message_id Unique ID of the SQS message
59
+ # @param [Hash] receipt_handle Receipt handle (unique per consume request) of the SQS message
60
+ def complete(message_id, receipt_handle)
61
+ Chore.logger.debug "Completing (deleting): #{message_id}"
62
+ queue.delete_messages(entries: [{ id: message_id, receipt_handle: receipt_handle }])
56
63
  end
57
64
 
65
+ # Delays retry of a job by +backoff_calc+ seconds.
66
+ #
67
+ # @param [UnitOfWork] item Item to be delayed
68
+ # @param [Block] backoff_calc Code that determines the backoff.
58
69
  def delay(item, backoff_calc)
59
70
  delay = backoff_calc.call(item)
60
71
  Chore.logger.debug "Delaying #{item.id} by #{delay} seconds"
61
- queue.batch_change_visibility(delay, [item.id])
72
+
73
+ queue.change_message_visibility_batch(entries: [
74
+ { id: item.id, receipt_handle: item.receipt_handle, visibility_timeout: delay },
75
+ ])
62
76
 
63
77
  return delay
64
78
  end
@@ -67,62 +81,61 @@ module Chore
67
81
 
68
82
  # Requests messages from SQS, and invokes the provided +&block+ over each one. Afterwards, the :on_fetch
69
83
  # hook will be invoked, per message
84
+ #
85
+ # @param [Block] &handler Message handler, passed along by #consume
86
+ #
87
+ # @return [Array<Aws::SQS::Message>]
70
88
  def handle_messages(&block)
71
- msg = queue.receive_messages(:limit => sqs_polling_amount, :attributes => [:receive_count])
89
+ msg = queue.receive_messages(:max_number_of_messages => sqs_polling_amount, :attribute_names => ['ApproximateReceiveCount'])
72
90
  messages = *msg
91
+
73
92
  messages.each do |message|
74
- unless duplicate_message?(message)
75
- block.call(message.handle, queue_name, queue_timeout, message.body, message.receive_count - 1)
93
+ unless duplicate_message?(message.message_id, message.queue_url, queue_timeout)
94
+ block.call(message.message_id, message.receipt_handle, queue_name, queue_timeout, message.body, message.attributes['ApproximateReceiveCount'].to_i - 1)
76
95
  end
77
- Chore.run_hooks_for(:on_fetch, message.handle, message.body)
96
+ Chore.run_hooks_for(:on_fetch, message.receipt_handle, message.body)
78
97
  end
79
- messages
80
- end
81
-
82
- # Checks if the given message has already been received within the timeout window for this queue
83
- def duplicate_message?(message)
84
- dupe_detector.found_duplicate?(:id=>message.id, :queue=>message.queue.url, :visibility_timeout=>message.queue.visibility_timeout)
85
- end
86
98
 
87
- # Returns the instance of the DuplicateDetector used to ensure unique messages.
88
- # Will create one if one doesn't already exist
89
- def dupe_detector
90
- @dupes ||= DuplicateDetector.new({:servers => Chore.config.dedupe_servers,
91
- :dupe_on_cache_failure => Chore.config.dupe_on_cache_failure})
99
+ messages
92
100
  end
93
101
 
94
- # Retrieves the SQS queue with the given +name+. The method will cache the results to prevent round trips on
95
- # subsequent calls. If <tt>reset_connection!</tt> has been called, this will result in the connection being
96
- # re-initialized, as well as clear any cached results from prior calls
102
+ # Retrieves the SQS queue object. The method will cache the results to prevent round trips on subsequent calls
103
+ #
104
+ # If <tt>reset_connection!</tt> has been called, this will result in the connection being re-initialized,
105
+ # as well as clear any cached results from prior calls
106
+ #
107
+ # @return [Aws::SQS::Queue]
97
108
  def queue
98
109
  if !@sqs_last_connected || (@@reset_at && @@reset_at >= @sqs_last_connected)
99
- AWS::Core::Http::ConnectionPool.pools.each do |p|
100
- p.empty!
101
- end
110
+ Aws.empty_connection_pools!
102
111
  @sqs = nil
103
112
  @sqs_last_connected = Time.now
104
113
  @queue = nil
105
114
  end
106
- @queue_url ||= sqs.queues.url_for(@queue_name)
107
- @queue ||= sqs.queues[@queue_url]
115
+
116
+ @queue_url ||= sqs.get_queue_url(queue_name: @queue_name).queue_url
117
+ @queue ||= Aws::SQS::Queue.new(url: @queue_url, client: sqs)
108
118
  end
109
119
 
110
- # The visibility timeout of the queue for this consumer
120
+ # The visibility timeout (in seconds) of the queue
121
+ #
122
+ # @return [Integer]
111
123
  def queue_timeout
112
- @queue_timeout ||= queue.visibility_timeout
124
+ @queue_timeout ||= queue.attributes['VisibilityTimeout'].to_i
113
125
  end
114
126
 
115
- # Access to the configured SQS connection object
127
+ # SQS API client object
128
+ #
129
+ # @return [Aws::SQS::Client]
116
130
  def sqs
117
- @sqs ||= AWS::SQS.new(
118
- :access_key_id => Chore.config.aws_access_key,
119
- :secret_access_key => Chore.config.aws_secret_key,
120
- :logger => Chore.logger,
121
- :log_level => :debug)
131
+ @sqs ||= Chore::Queues::SQS.sqs_client
122
132
  end
123
133
 
134
+ # Maximum number of messages to retrieve on each request
135
+ #
136
+ # @return [Integer]
124
137
  def sqs_polling_amount
125
- Chore.config.queue_polling_size || 10
138
+ Chore.config.queue_polling_size
126
139
  end
127
140
  end
128
141
  end