chore-core 1.5.2

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 (59) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +260 -0
  4. data/Rakefile +32 -0
  5. data/bin/chore +34 -0
  6. data/chore-core.gemspec +46 -0
  7. data/lib/chore/cli.rb +232 -0
  8. data/lib/chore/configuration.rb +13 -0
  9. data/lib/chore/consumer.rb +52 -0
  10. data/lib/chore/duplicate_detector.rb +56 -0
  11. data/lib/chore/fetcher.rb +31 -0
  12. data/lib/chore/hooks.rb +25 -0
  13. data/lib/chore/job.rb +103 -0
  14. data/lib/chore/json_encoder.rb +18 -0
  15. data/lib/chore/manager.rb +47 -0
  16. data/lib/chore/publisher.rb +29 -0
  17. data/lib/chore/queues/filesystem/consumer.rb +128 -0
  18. data/lib/chore/queues/filesystem/filesystem_queue.rb +49 -0
  19. data/lib/chore/queues/filesystem/publisher.rb +45 -0
  20. data/lib/chore/queues/sqs/consumer.rb +121 -0
  21. data/lib/chore/queues/sqs/publisher.rb +55 -0
  22. data/lib/chore/queues/sqs.rb +38 -0
  23. data/lib/chore/railtie.rb +18 -0
  24. data/lib/chore/signal.rb +175 -0
  25. data/lib/chore/strategies/consumer/batcher.rb +76 -0
  26. data/lib/chore/strategies/consumer/single_consumer_strategy.rb +34 -0
  27. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +81 -0
  28. data/lib/chore/strategies/worker/forked_worker_strategy.rb +221 -0
  29. data/lib/chore/strategies/worker/single_worker_strategy.rb +39 -0
  30. data/lib/chore/tasks/queues.task +11 -0
  31. data/lib/chore/unit_of_work.rb +17 -0
  32. data/lib/chore/util.rb +18 -0
  33. data/lib/chore/version.rb +9 -0
  34. data/lib/chore/worker.rb +117 -0
  35. data/lib/chore-core.rb +1 -0
  36. data/lib/chore.rb +218 -0
  37. data/spec/chore/cli_spec.rb +182 -0
  38. data/spec/chore/consumer_spec.rb +36 -0
  39. data/spec/chore/duplicate_detector_spec.rb +62 -0
  40. data/spec/chore/fetcher_spec.rb +38 -0
  41. data/spec/chore/hooks_spec.rb +44 -0
  42. data/spec/chore/job_spec.rb +80 -0
  43. data/spec/chore/json_encoder_spec.rb +11 -0
  44. data/spec/chore/manager_spec.rb +39 -0
  45. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -0
  46. data/spec/chore/queues/sqs/consumer_spec.rb +136 -0
  47. data/spec/chore/queues/sqs/publisher_spec.rb +74 -0
  48. data/spec/chore/queues/sqs_spec.rb +37 -0
  49. data/spec/chore/signal_spec.rb +244 -0
  50. data/spec/chore/strategies/consumer/batcher_spec.rb +93 -0
  51. data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +23 -0
  52. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +105 -0
  53. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +281 -0
  54. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +36 -0
  55. data/spec/chore/worker_spec.rb +134 -0
  56. data/spec/chore_spec.rb +108 -0
  57. data/spec/spec_helper.rb +58 -0
  58. data/spec/test_job.rb +7 -0
  59. metadata +194 -0
@@ -0,0 +1,121 @@
1
+ require 'aws/sqs'
2
+ require 'chore/duplicate_detector'
3
+
4
+ AWS.eager_autoload! AWS::Core
5
+ AWS.eager_autoload! AWS::SQS
6
+
7
+ module Chore
8
+ module Queues
9
+ module SQS
10
+ # SQS Consumer for Chore. Requests messages from SQS and passes them to be worked on. Also controls
11
+ # deleting completed messages within SQS.
12
+ class Consumer < Chore::Consumer
13
+ # Initialize the reset at on class load
14
+ @@reset_at = Time.now
15
+
16
+ Chore::CLI.register_option 'aws_access_key', '--aws-access-key KEY', 'Valid AWS Access Key'
17
+ Chore::CLI.register_option 'aws_secret_key', '--aws-secret-key KEY', 'Valid AWS Secret Key'
18
+ 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
+
23
+ def initialize(queue_name, opts={})
24
+ super(queue_name, opts)
25
+ end
26
+
27
+ # Sets a flag that instructs the publisher to reset the connection the next time it's used
28
+ def self.reset_connection!
29
+ @@reset_at = Time.now
30
+ end
31
+
32
+ # Begins requesting messages from SQS, which will invoke the +&handler+ over each message
33
+ def consume(&handler)
34
+ while running?
35
+ begin
36
+ messages = handle_messages(&handler)
37
+ sleep 1 if messages.empty?
38
+ rescue AWS::SQS::Errors::NonExistentQueue => e
39
+ Chore.logger.error "You specified a queue that does not exist. You must create the queue before starting Chore. Shutting down..."
40
+ raise Chore::TerribleMistake
41
+ rescue => e
42
+ Chore.logger.error { "SQSConsumer#Consume: #{e.inspect} #{e.backtrace * "\n"}" }
43
+ end
44
+ end
45
+ end
46
+
47
+ # Rejects the given message from SQS by +id+. Currently a noop
48
+ def reject(id)
49
+
50
+ end
51
+
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
+ end
57
+
58
+ private
59
+
60
+ # Requests messages from SQS, and invokes the provided +&block+ over each one. Afterwards, the :on_fetch
61
+ # hook will be invoked, per message
62
+ def handle_messages(&block)
63
+ msg = queue.receive_messages(:limit => sqs_polling_amount, :attributes => [:receive_count])
64
+
65
+ messages = *msg
66
+ messages.each do |message|
67
+ block.call(message.handle, queue_name, queue_timeout, message.body, message.receive_count - 1) unless duplicate_message?(message)
68
+ Chore.run_hooks_for(:on_fetch, message.handle, message.body)
69
+ end
70
+ messages
71
+ end
72
+
73
+ # Checks if the given message has already been received within the timeout window for this queue
74
+ def duplicate_message?(message)
75
+ dupe_detector.found_duplicate?(message)
76
+ end
77
+
78
+ # Returns the instance of the DuplicateDetector used to ensure unique messages.
79
+ # Will create one if one doesn't already exist
80
+ def dupe_detector
81
+ @dupes ||= DuplicateDetector.new({:servers => Chore.config.dedupe_servers,
82
+ :dupe_on_cache_failure => Chore.config.dupe_on_cache_failure})
83
+ end
84
+
85
+ # Retrieves the SQS queue with the given +name+. The method will cache the results to prevent round trips on
86
+ # subsequent calls. If <tt>reset_connection!</tt> has been called, this will result in the connection being
87
+ # re-initialized, as well as clear any cached results from prior calls
88
+ def queue
89
+ if !@sqs_last_connected || (@@reset_at && @@reset_at >= @sqs_last_connected)
90
+ AWS::Core::Http::ConnectionPool.pools.each do |p|
91
+ p.empty!
92
+ end
93
+ @sqs = nil
94
+ @sqs_last_connected = Time.now
95
+ @queue = nil
96
+ end
97
+ @queue_url ||= sqs.queues.url_for(@queue_name)
98
+ @queue ||= sqs.queues[@queue_url]
99
+ end
100
+
101
+ # The visibility timeout of the queue for this consumer
102
+ def queue_timeout
103
+ @queue_timeout ||= queue.visibility_timeout
104
+ end
105
+
106
+ # Access to the configured SQS connection object
107
+ def sqs
108
+ @sqs ||= AWS::SQS.new(
109
+ :access_key_id => Chore.config.aws_access_key,
110
+ :secret_access_key => Chore.config.aws_secret_key,
111
+ :logger => Chore.logger,
112
+ :log_level => :debug)
113
+ end
114
+
115
+ def sqs_polling_amount
116
+ Chore.config.queue_polling_size || 10
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,55 @@
1
+ require 'chore/publisher'
2
+
3
+ module Chore
4
+ module Queues
5
+ module SQS
6
+
7
+ # SQS Publisher, for writing messages to SQS from Chore
8
+ class Publisher < Chore::Publisher
9
+ @@reset_next = true
10
+
11
+ def initialize(opts={})
12
+ super
13
+ @sqs_queues = {}
14
+ @sqs_queue_urls = {}
15
+ end
16
+
17
+ # Takes a given Chore::Job instance +job+, and publishes it by looking up the +queue_name+.
18
+ def publish(queue_name,job)
19
+ queue = self.queue(queue_name)
20
+ queue.send_message(encode_job(job))
21
+ end
22
+
23
+ # Sets a flag that instructs the publisher to reset the connection the next time it's used
24
+ def self.reset_connection!
25
+ @@reset_next = true
26
+ end
27
+
28
+ # Access to the configured SQS connection object
29
+ def sqs
30
+ @sqs ||= AWS::SQS.new(
31
+ :access_key_id => Chore.config.aws_access_key,
32
+ :secret_access_key => Chore.config.aws_secret_key,
33
+ :logger => Chore.logger,
34
+ :log_level => :debug)
35
+ end
36
+
37
+ # Retrieves the SQS queue with the given +name+. The method will cache the results to prevent round trips on subsequent calls
38
+ # If <tt>reset_connection!</tt> has been called, this will result in the connection being re-initialized,
39
+ # as well as clear any cached results from prior calls
40
+ def queue(name)
41
+ if @@reset_next
42
+ AWS::Core::Http::ConnectionPool.pools.each do |p|
43
+ p.empty!
44
+ end
45
+ @sqs = nil
46
+ @@reset_next = false
47
+ @sqs_queues = {}
48
+ end
49
+ @sqs_queue_urls[name] ||= self.sqs.queues.url_for(name)
50
+ @sqs_queues[name] ||= self.sqs.queues[@sqs_queue_urls[name]]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ module Chore
2
+ module Queues
3
+ module SQS
4
+ # Helper method to create queues based on the currently known list as provided by your configured Chore::Jobs
5
+ # This is meant to be invoked from a rake task, and not directly.
6
+ # These queues will be created with the default settings, which may not be ideal.
7
+ # This is meant only as a convenience helper for testing, and not as a way to create production quality queues in SQS
8
+ def self.create_queues!
9
+ raise 'You must have atleast one Chore Job configured and loaded before attempting to create queues' unless Chore.prefixed_queue_names.length > 0
10
+ #This will raise an exception if AWS has not been configured by the project making use of Chore
11
+ sqs_queues = AWS::SQS.new.queues
12
+ Chore.prefixed_queue_names.each do |queue_name|
13
+ Chore.logger.info "Chore Creating Queue: #{queue_name}"
14
+ begin
15
+ sqs_queues.create(queue_name)
16
+ rescue AWS::SQS::Errors::QueueAlreadyExists
17
+ Chore.logger.info "exists with different config"
18
+ end
19
+ end
20
+ Chore.prefixed_queue_names
21
+ end
22
+
23
+ # Helper method to delete all known queues based on the list as provided by your configured Chore::Jobs
24
+ # This is meant to be invoked from a rake task, and not directly.
25
+ def self.delete_queues!
26
+ raise 'You must have atleast one Chore Job configured and loaded before attempting to create queues' unless Chore.prefixed_queue_names.length > 0
27
+ #This will raise an exception if AWS has not been configured by the project making use of Chore
28
+ sqs_queues = AWS::SQS.new.queues
29
+ Chore.prefixed_queue_names.each do |queue_name|
30
+ Chore.logger.info "Chore Deleting Queue: #{queue_name}"
31
+ url = sqs_queues.url_for(queue_name)
32
+ sqs_queues[url].delete
33
+ end
34
+ Chore.prefixed_queue_names
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ module Chore
2
+ # Railtie for incorporating chores rake tasks into your Rails application
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.task')].each { |f| load f }
6
+ end
7
+
8
+ config.after_initialize do
9
+ if Chore.configuring?
10
+ # Reset the logger on forks to avoid deadlocks
11
+ Rails.logger = Chore.logger
12
+ Chore.add_hook(:after_fork) do |worker|
13
+ Rails.logger = Chore.logger
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,175 @@
1
+ module Chore
2
+ # Provides smarter signal handling capabilities than Ruby's built-in
3
+ # Signal class. Specifically it runs callbacks in a separate thread since:
4
+ # (1) Ruby 2.0 cannot obtain locks in the main Signal thread
5
+ # (2) Doing so can result in deadlocks in Ruby 1.9.x.
6
+ #
7
+ # Ruby's core implementation can be found at: http://ruby-doc.org/core-1.9.3/Signal.html
8
+ #
9
+ # == Differences
10
+ #
11
+ # There are a few important differences with the way signals trapped through
12
+ # this class behave than through Ruby's Signal class.
13
+ #
14
+ # === Sequential processing
15
+ #
16
+ # In Ruby, signals are interrupt-driven -- the thread is executing at the time
17
+ # will be interrupted at that point in the call stack and start executing the
18
+ # signal handler. This increases the potential for deadlocks if mutexes are
19
+ # in use by both the thread and the signal handler.
20
+ #
21
+ # In Chore, signal handlers are executed sequentially. When a handler is
22
+ # started, it must complete before the next signal is processed. These
23
+ # handlers are also executed in their own thread and, therefore, will compete
24
+ # for resources with the rest of the application.
25
+ #
26
+ # === Forking
27
+ #
28
+ # In Ruby, forking does not disrupt the ability to process signals. Signals
29
+ # trapped in the master process will continue to be trapped in forked child
30
+ # processes.
31
+ #
32
+ # In Chore, this is not the case. When a process is forked, any trapped
33
+ # signals will no longer get processed. This is because the thread that
34
+ # processes those incoming signals gets killed.
35
+ #
36
+ # In order to process these signals, `Chore::Signal.reset` must be called,
37
+ # followed by additional calls to re-register those signal handlers.
38
+ #
39
+ # == Signal ordering
40
+ #
41
+ # It is important to note that in Ruby, signals are essentially processed
42
+ # as LIFO (Last-In, First-Out) since they are interrupt driven. Similar
43
+ # behaviors is present in Chore's implementation.
44
+ #
45
+ # Having LIFO behavior is the reason why this class uses a queue for
46
+ # tracking the list of incoming signals, instead of writing them out to a
47
+ # pipe.
48
+ class Signal
49
+ # The handlers registered for trapping certain signals. Maps signal => handler.
50
+ @handlers = {}
51
+
52
+ # The set of incoming, unprocessed high-priority signals (such as QUIT / INT)
53
+ @primary_signals = []
54
+
55
+ # The set of incoming, unprocessed low-priority signals (such as CHLD)
56
+ @secondary_signals = []
57
+
58
+ # The priorities of signals to handle. If not defined, the signal is
59
+ # considered high-priority.
60
+ PRIORITIES = {
61
+ 'CHLD' => :secondary
62
+ }
63
+
64
+ # Stream used to track when signals are ready to be processed
65
+ @wake_in, @wake_out = IO.pipe
66
+
67
+ class << self
68
+ # Traps the given signal and runs the block when the signal is sent to
69
+ # this process. This will run the block outside of the trap thread.
70
+ #
71
+ # Only a single handler can be registered for a signal at any point. If
72
+ # a signal has already been trapped, a warning will be generated and the
73
+ # previous handler for the signal will be returned.
74
+ #
75
+ # See ::Signal#trap @ http://ruby-doc.org/core-1.9.3/Signal.html#method-c-trap
76
+ # for more information.
77
+ def trap(signal, command = nil, &block)
78
+ if command
79
+ # Command given for Ruby to interpret -- pass it directly onto Signal
80
+ @handlers.delete(signal)
81
+ ::Signal.trap(signal, command)
82
+ else
83
+ # Ensure we're listening for signals
84
+ listen
85
+
86
+ if @handlers[signal]
87
+ Chore.logger.debug "#{signal} signal has been overwritten:\n#{caller * "\n"}"
88
+ end
89
+
90
+ # Wrap handlers so they run in the listener thread
91
+ signals = PRIORITIES[signal] == :secondary ? @secondary_signals : @primary_signals
92
+ @handlers[signal] = block
93
+ ::Signal.trap(signal) do
94
+ signals << signal
95
+ wakeup
96
+ end
97
+ end
98
+ end
99
+
100
+ # Resets signals and handlers back to their defaults. Any unprocessed
101
+ # signals will be discarded.
102
+ #
103
+ # This should be called after forking a processing in order to ensure
104
+ # that signals continue to get processed. *Note*, however, that new
105
+ # handlers must get registered after forking.
106
+ def reset
107
+ # Reset traps back to their default behavior. Note that this *must*
108
+ # be done first in order to prevent trap handlers from being called
109
+ # while the wake pipe / listener are being reset. If this is run
110
+ # out of order, then it's possible for those callbacks to hit errors.
111
+ @handlers.keys.each {|signal| trap(signal, 'DEFAULT')}
112
+
113
+ # Reset signals back to their empty state
114
+ @listener = nil
115
+ @primary_signals.clear
116
+ @secondary_signals.clear
117
+ @wake_out.close
118
+ @wake_in.close
119
+ @wake_in, @wake_out = IO.pipe
120
+ end
121
+
122
+ private
123
+ # Starts the thread that processes incoming signals
124
+ def listen
125
+ @listener ||= Thread.new do
126
+ on_wakeup do
127
+ while signal = next_signal
128
+ process(signal)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ # Looks up what the next signal is to process. Signals are typically
135
+ # processed LIFO (Last In, First Out), though primary signals are
136
+ # prioritized over secondary signals.
137
+ def next_signal
138
+ @primary_signals.pop || @secondary_signals.pop
139
+ end
140
+
141
+ # Waits until a wakeup signal is received. When it is received, the
142
+ # provided block will be yielded to.
143
+ def on_wakeup
144
+ begin
145
+ while @wake_in.getc
146
+ yield
147
+ end
148
+ rescue IOError => e
149
+ # Ignore: listener has been stopped
150
+ Chore.logger.debug "Signal stream closed: #{e}\n#{e.backtrace * "\n"}"
151
+ end
152
+ end
153
+
154
+ # Wakes up the listener thread to indicate that signals are ready to be
155
+ # processed
156
+ def wakeup
157
+ @wake_out.write('.')
158
+ end
159
+
160
+ # Processes the given signal by running the handler in a separate
161
+ # thread.
162
+ def process(signal)
163
+ handler = @handlers[signal]
164
+ if handler
165
+ begin
166
+ handler.call
167
+ rescue => e
168
+ # Prevent signal handlers from killing the listener thread
169
+ Chore.logger.error "Failed to run #{signal} signal handler: #{e}\n#{e.backtrace * "\n"}"
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,76 @@
1
+ module Chore
2
+ module Strategy
3
+
4
+ # Handles holding jobs in memory until such time as the batch has become full, per the developers configured threshold,
5
+ # or enough time elapses that Chore determines to not wait any longer (20 seconds by default)
6
+ class Batcher
7
+ attr_accessor :callback
8
+ attr_accessor :batch
9
+
10
+ def initialize(size)
11
+ @size = size
12
+ @batch = []
13
+ @mutex = Mutex.new
14
+ @last_message = nil
15
+ @callback = nil
16
+ @running = true
17
+ end
18
+
19
+ # The main entry point of the Batcher, <tt>schedule</tt> begins a thread with the provided +batch_timeout+
20
+ # as the only argument. While the Batcher is running, it will attempt to check if either the batch is full,
21
+ # or if the +batch_timeout+ has elapsed since the last batch was executed. If the batch is full, it will be executed.
22
+ # If the +batch_timeout+ has elapsed, as soon as the next message enters the batch, it will be executed.
23
+ #
24
+ # Calling <tt>stop</tt> will cause the thread to finish it's current check, and exit
25
+ def schedule(batch_timeout=20)
26
+ @thread = Thread.new(batch_timeout) do |timeout|
27
+ Chore.logger.info "Batching timeout thread starting"
28
+ while @running do
29
+ begin
30
+ Chore.logger.debug "Last message added to batch: #{@last_message}: #{@batch.size}"
31
+ if @last_message && Time.now > (@last_message + timeout)
32
+ Chore.logger.debug "Batching timeout reached (#{@last_message + timeout}), current size: #{@batch.size}"
33
+ self.execute(true)
34
+ @last_message = nil
35
+ end
36
+ sleep(1)
37
+ rescue => e
38
+ Chore.logger.error "Batcher#schedule raised an exception: #{e.inspect}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Adds the +item+ to the current batch
45
+ def add(item)
46
+ @batch << item
47
+ @last_message = Time.now
48
+ execute if ready?
49
+ end
50
+
51
+ # Calls for the batch to be executed. If +force+ is set to true, the batch will execute even if it is not full yet
52
+ def execute(force = false)
53
+ batch = nil
54
+ @mutex.synchronize do
55
+ if force || ready?
56
+ batch = @batch.slice!(0...@size)
57
+ end
58
+ end
59
+
60
+ if batch && !batch.empty?
61
+ @callback.call(batch)
62
+ end
63
+ end
64
+
65
+ # Determines if the batch is ready to fire, by comparing it's size to the configured batch_size
66
+ def ready?
67
+ @batch.size >= @size
68
+ end
69
+
70
+ # Sets a flag which will begin shutting down the Batcher
71
+ def stop
72
+ @running = false
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,34 @@
1
+ module Chore
2
+ module Strategy
3
+
4
+ # Consumer strategy for requesting batches of work in a linear fashion. Ideally used for running
5
+ # a single Chore job locally in a development environment where performance or throughput may not matter.
6
+ # <tt>SingleConsumerStrategy</tt> will raise an exception if you're configured to listen for more than 1 queue
7
+ class SingleConsumerStrategy
8
+ def initialize(fetcher, opts={})
9
+ @fetcher = fetcher
10
+ end
11
+
12
+ # Begins fetching from the configured queue by way of the configured Consumer. This can only be used if you have a
13
+ # single queue which can be kept up with at a relatively low volume. If you have more than a single queue configured,
14
+ # it will raise an exception.
15
+ def fetch
16
+ Chore.logger.debug "Starting up consumer strategy: #{self.class.name}"
17
+ queues = Chore.config.queues
18
+ raise "When using SingleConsumerStrategy only one queue can be defined. Queues: #{queues}" unless queues.size == 1
19
+
20
+ @consumer = Chore.config.consumer.new(queues.first)
21
+ @consumer.consume do |id,queue_name,queue_timeout,body,previous_attempts|
22
+ work = UnitOfWork.new(id, queue_name, queue_timeout, body, previous_attempts, @consumer)
23
+ @fetcher.manager.assign(work)
24
+ end
25
+ end
26
+
27
+ # Stops consuming messages from the queue
28
+ def stop!
29
+ Chore.logger.info "Shutting down fetcher: #{self.class.name.to_s}"
30
+ @consumer.stop
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,81 @@
1
+ require 'chore/strategies/consumer/batcher'
2
+ module Chore
3
+ module Strategy
4
+ class ThreadedConsumerStrategy #:nodoc
5
+ attr_accessor :batcher
6
+
7
+ Chore::CLI.register_option 'batch_size', '--batch-size SIZE', Integer, 'Number of items to collect for a single worker to process'
8
+ Chore::CLI.register_option 'threads_per_queue', '--threads-per-queue NUM_THREADS', Integer, 'Number of threads to create for each named queue'
9
+
10
+ def initialize(fetcher)
11
+ @fetcher = fetcher
12
+ @batcher = Batcher.new(Chore.config.batch_size)
13
+ @batcher.callback = lambda { |batch| @fetcher.manager.assign(batch) }
14
+ @batcher.schedule
15
+ @running = true
16
+ end
17
+
18
+ # Begins fetching from queues by spinning up the configured +:threads_per_queue:+ count of threads
19
+ # for each queue you're consuming from.
20
+ # Once all threads are spun up and running, the threads are then joined.
21
+ def fetch
22
+ Chore.logger.debug "Starting up consumer strategy: #{self.class.name}"
23
+ threads = []
24
+ Chore.config.queues.each do |queue|
25
+ Chore.config.threads_per_queue.times do
26
+ if running?
27
+ threads << start_consumer_thread(queue)
28
+ end
29
+ end
30
+ end
31
+
32
+ threads.each(&:join)
33
+ end
34
+
35
+ # If the ThreadedConsumerStrategy is currently running <tt>stop!</tt> will begin signalling it to stop
36
+ # It will stop the batcher from forking more work, as well as set a flag which will disable it's own consuming
37
+ # threads once they finish with their current work.
38
+ def stop!
39
+ if running?
40
+ Chore.logger.info "Shutting down fetcher: #{self.class.name.to_s}"
41
+ @batcher.stop
42
+ @running = false
43
+ end
44
+ end
45
+
46
+ # Returns whether or not the ThreadedConsumerStrategy is running or not
47
+ def running?
48
+ @running
49
+ end
50
+
51
+ private
52
+ # Starts a consumer thread for polling the given +queue+.
53
+ # If <tt>stop!<tt> is called, the threads will shut themsevles down.
54
+ def start_consumer_thread(queue)
55
+ t = Thread.new(queue) do |tQueue|
56
+ begin
57
+ consumer = Chore.config.consumer.new(tQueue)
58
+ consumer.consume do |id, queue_name, queue_timeout, body, previous_attempts|
59
+ # Quick hack to force this thread to end it's work
60
+ # if we're shutting down. Could be delayed due to the
61
+ # weird sometimes-blocking nature of SQS.
62
+ consumer.stop if !running?
63
+ Chore.logger.debug { "Got message: #{id}"}
64
+
65
+ work = UnitOfWork.new(id, queue_name, queue_timeout, body, previous_attempts, consumer)
66
+ @batcher.add(work)
67
+ end
68
+ rescue Chore::TerribleMistake
69
+ Chore.logger.error "I've made a terrible mistake... shutting down Chore"
70
+ self.stop!
71
+ @fetcher.manager.shutdown!
72
+ rescue => e
73
+ Chore.logger.error "ThreadedConsumerStrategy#consumer thread raised an exception: #{e.inspect} at #{e.backtrace}"
74
+ end
75
+ end
76
+ t
77
+ end
78
+
79
+ end #ThreadedConsumerStrategy
80
+ end
81
+ end #Chore