chore-core 1.10.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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