chore-core 1.8.2 → 3.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +6 -0
- data/chore-core.gemspec +1 -0
- data/lib/chore.rb +11 -5
- data/lib/chore/cli.rb +21 -2
- data/lib/chore/consumer.rb +15 -5
- data/lib/chore/fetcher.rb +12 -7
- data/lib/chore/hooks.rb +2 -1
- data/lib/chore/job.rb +17 -0
- data/lib/chore/manager.rb +18 -2
- data/lib/chore/queues/filesystem/consumer.rb +116 -59
- data/lib/chore/queues/filesystem/filesystem_queue.rb +19 -0
- data/lib/chore/queues/filesystem/publisher.rb +12 -18
- data/lib/chore/queues/sqs/consumer.rb +6 -21
- data/lib/chore/strategies/consumer/batcher.rb +8 -9
- data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +3 -1
- data/lib/chore/strategies/consumer/throttled_consumer_strategy.rb +121 -0
- data/lib/chore/strategies/worker/forked_worker_strategy.rb +5 -6
- data/lib/chore/strategies/worker/helpers/ipc.rb +88 -0
- data/lib/chore/strategies/worker/helpers/preforked_worker.rb +163 -0
- data/lib/chore/strategies/worker/helpers/work_distributor.rb +65 -0
- data/lib/chore/strategies/worker/helpers/worker_info.rb +13 -0
- data/lib/chore/strategies/worker/helpers/worker_killer.rb +40 -0
- data/lib/chore/strategies/worker/helpers/worker_manager.rb +183 -0
- data/lib/chore/strategies/worker/preforked_worker_strategy.rb +150 -0
- data/lib/chore/strategies/worker/single_worker_strategy.rb +35 -13
- data/lib/chore/unit_of_work.rb +8 -0
- data/lib/chore/util.rb +5 -1
- data/lib/chore/version.rb +3 -3
- data/lib/chore/worker.rb +29 -0
- data/spec/chore/cli_spec.rb +2 -2
- data/spec/chore/consumer_spec.rb +0 -4
- data/spec/chore/duplicate_detector_spec.rb +17 -5
- data/spec/chore/fetcher_spec.rb +0 -11
- data/spec/chore/manager_spec.rb +7 -0
- data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -11
- data/spec/chore/queues/sqs/consumer_spec.rb +1 -3
- data/spec/chore/strategies/consumer/batcher_spec.rb +50 -0
- data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +1 -0
- data/spec/chore/strategies/consumer/throttled_consumer_strategy_spec.rb +165 -0
- data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +16 -1
- data/spec/chore/strategies/worker/helpers/ipc_spec.rb +127 -0
- data/spec/chore/strategies/worker/helpers/preforked_worker_spec.rb +236 -0
- data/spec/chore/strategies/worker/helpers/work_distributor_spec.rb +131 -0
- data/spec/chore/strategies/worker/helpers/worker_info_spec.rb +14 -0
- data/spec/chore/strategies/worker/helpers/worker_killer_spec.rb +97 -0
- data/spec/chore/strategies/worker/helpers/worker_manager_spec.rb +304 -0
- data/spec/chore/strategies/worker/preforked_worker_strategy_spec.rb +183 -0
- data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +25 -0
- data/spec/chore/worker_spec.rb +69 -1
- metadata +33 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5aeebbb3efb30c9047c67996272864003542d9bd91d9887d3e149b6840f0573a
|
4
|
+
data.tar.gz: dd210d2eda88ddfab6e97646cb69ecccfc01a57a4198a8c2fc50c30d260dd32f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0df90580322b71a356a1b7bed8d1480c0794e6fcd12995bc82edfb3a878f40981fb87fefdcb2ad97a9841cabad2a151268a0e098d08f697cd56b727fcb973948
|
7
|
+
data.tar.gz: 24ef7ff568162c5f173ef599d687cd768c9b8a85948d5c80df10855fe30eac9df118e6a15b741e348e064e51ab693b395d81bf4e3689ca9519d9c15b69d05521
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Chore: Job processing... for the future!
|
2
2
|
|
3
|
+
[![Build Status](https://travis-ci.org/Tapjoy/chore.svg?branch=master)](https://travis-ci.org/Tapjoy/chore)
|
4
|
+
|
3
5
|
## About
|
4
6
|
|
5
7
|
Chore is a pluggable, multi-backend job processor. It was built from the ground up to be extremely flexible. We hope that you
|
@@ -33,6 +35,7 @@ Other options include:
|
|
33
35
|
--threads-per-queue 4 # number of threads per queue for consuming from a given queue.
|
34
36
|
--dedupe-servers # if using SQS or similiar queue with at-least once delivery and your memcache is running on something other than localhost
|
35
37
|
--batch-size 50 # how many messages are batched together before handing them to a worker
|
38
|
+
--batch-timeout 20 # maximum number of seconds to wait until handing a message over to a worker
|
36
39
|
--queue_prefix prefixy # A prefix to prepend to queue names, mainly for development and qa testing purposes
|
37
40
|
--max-attempts 100 # The maximum number of times a job can be attempted
|
38
41
|
--dupe-on-cache-failure # Determines the deduping behavior when a cache connection error occurs. When set to `false`, the message is assumed not to be a duplicate. Defaults to `false`.
|
@@ -92,6 +95,7 @@ Chore.configure do |c|
|
|
92
95
|
c.max_attempts = 100
|
93
96
|
...
|
94
97
|
c.batch_size = 50
|
98
|
+
c.batch_timeout = 20
|
95
99
|
end
|
96
100
|
```
|
97
101
|
|
@@ -189,11 +193,13 @@ A number of hooks, both global and per-job, exist in Chore for your convenience.
|
|
189
193
|
|
190
194
|
Global Hooks:
|
191
195
|
|
196
|
+
* before_start
|
192
197
|
* before_first_fork
|
193
198
|
* before_fork
|
194
199
|
* after_fork
|
195
200
|
* around_fork
|
196
201
|
* within_fork
|
202
|
+
* before_shutdown
|
197
203
|
|
198
204
|
("within_fork" behaves similarly to around_fork, except that it is called after the worker process has been forked. In contrast, around_fork is called by the parent process.)
|
199
205
|
|
data/chore-core.gemspec
CHANGED
@@ -39,6 +39,7 @@ Gem::Specification.new do |s|
|
|
39
39
|
s.add_runtime_dependency(%q<json>, [">= 0"])
|
40
40
|
s.add_runtime_dependency(%q<aws-sdk-v1>, ["~> 1.56", ">= 1.56.0"])
|
41
41
|
s.add_runtime_dependency(%q<thread>, ["~> 0.1.3"])
|
42
|
+
s.add_runtime_dependency('get_process_mem', ["~> 0.2.0"])
|
42
43
|
s.add_development_dependency(%q<rspec>, ["~> 3.3.0"])
|
43
44
|
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
44
45
|
s.add_development_dependency(%q<bundler>, [">= 0"])
|
data/lib/chore.rb
CHANGED
@@ -34,13 +34,18 @@ module Chore #:nodoc:
|
|
34
34
|
:fetcher => Fetcher,
|
35
35
|
:consumer_strategy => Strategy::ThreadedConsumerStrategy,
|
36
36
|
:batch_size => 50,
|
37
|
+
:batch_timeout => 20,
|
37
38
|
:log_level => Logger::WARN,
|
38
39
|
:log_path => STDOUT,
|
39
40
|
:default_queue_timeout => (12 * 60 * 60), # 12 hours
|
40
41
|
:shutdown_timeout => (2 * 60),
|
41
42
|
:max_attempts => 1.0 / 0.0, # Infinity
|
42
43
|
:dupe_on_cache_failure => false,
|
43
|
-
:
|
44
|
+
:queue_polling_size => 10,
|
45
|
+
:payload_handler => Chore::Job,
|
46
|
+
:master_procline => "chore-master-#{Chore::VERSION}",
|
47
|
+
:worker_procline => "chore-worker-#{Chore::VERSION}",
|
48
|
+
:consumer_sleep_interval => 1
|
44
49
|
}
|
45
50
|
|
46
51
|
class << self
|
@@ -110,9 +115,9 @@ module Chore #:nodoc:
|
|
110
115
|
# add_hook(:before_fork) {|worker| puts 1 }
|
111
116
|
# add_hook(:before_fork) {|worker| puts 2 }
|
112
117
|
# add_hook(:before_fork) {|worker| puts 3 }
|
113
|
-
#
|
118
|
+
#
|
114
119
|
# run_hooks_for(:before_fork, worker)
|
115
|
-
#
|
120
|
+
#
|
116
121
|
# # ...will produce the following output
|
117
122
|
# => 1
|
118
123
|
# => 2
|
@@ -129,9 +134,9 @@ module Chore #:nodoc:
|
|
129
134
|
# add_hook(:around_fork) {|worker, &block| puts 'before 1'; block.call; puts 'after 1'}
|
130
135
|
# add_hook(:around_fork) {|worker, &block| puts 'before 2'; block.call; puts 'after 2'}
|
131
136
|
# add_hook(:around_fork) {|worker, &block| puts 'before 3'; block.call; puts 'after 3'}
|
132
|
-
#
|
137
|
+
#
|
133
138
|
# run_hooks_for(:around_fork, worker) { puts 'block' }
|
134
|
-
#
|
139
|
+
#
|
135
140
|
# # ...will produce the following output
|
136
141
|
# => before 1
|
137
142
|
# => before 2
|
@@ -186,6 +191,7 @@ module Chore #:nodoc:
|
|
186
191
|
# Chore.configure do |c|
|
187
192
|
# c.consumer = Chore::Queues::SQS::Consumer
|
188
193
|
# c.batch_size = 50
|
194
|
+
# c.batch_timeout = 20
|
189
195
|
# end
|
190
196
|
def self.configure(opts={})
|
191
197
|
@config = (@config ? @config.merge_hash(opts) : Chore::Configuration.new(DEFAULT_OPTIONS.merge(opts)))
|
data/lib/chore/cli.rb
CHANGED
@@ -89,6 +89,7 @@ module Chore #:nodoc:
|
|
89
89
|
detect_queues
|
90
90
|
Chore.configure(options)
|
91
91
|
Chore.configuring = false
|
92
|
+
validate_strategy!
|
92
93
|
end
|
93
94
|
|
94
95
|
|
@@ -134,7 +135,7 @@ module Chore #:nodoc:
|
|
134
135
|
options[:consumer_strategy] = constantize(arg)
|
135
136
|
end
|
136
137
|
|
137
|
-
register_option 'consumer_sleep_interval', '--consumer-sleep-interval INTERVAL', Float, 'Length of time in seconds to sleep when the consumer does not find any messages
|
138
|
+
register_option 'consumer_sleep_interval', '--consumer-sleep-interval INTERVAL', Float, 'Length of time in seconds to sleep when the consumer does not find any messages (default: 1)'
|
138
139
|
|
139
140
|
register_option 'payload_handler', '--payload_handler CLASS_NAME', 'Name of a class to use as the payload handler (default: Chore::Job)' do |arg|
|
140
141
|
options[:payload_handler] = constantize(arg)
|
@@ -144,6 +145,7 @@ module Chore #:nodoc:
|
|
144
145
|
|
145
146
|
register_option 'dupe_on_cache_failure', '--dupe-on-cache-failure BOOLEAN', 'Determines the deduping behavior when a cache connection error occurs. When set to false, the message is assumed not to be a duplicate. (default: false)'
|
146
147
|
|
148
|
+
register_option 'queue_polling_size', '--queue_polling_size NUM', Integer, 'Amount of messages to grab on each request (default: 10)'
|
147
149
|
end
|
148
150
|
|
149
151
|
def parse_opts(argv, ignore_errors = false) #:nodoc:
|
@@ -254,7 +256,6 @@ module Chore #:nodoc:
|
|
254
256
|
end
|
255
257
|
|
256
258
|
def validate! #:nodoc:
|
257
|
-
|
258
259
|
missing_option!("--require [PATH|DIR]") unless options[:require]
|
259
260
|
|
260
261
|
if !File.exist?(options[:require]) ||
|
@@ -266,7 +267,25 @@ module Chore #:nodoc:
|
|
266
267
|
puts @parser
|
267
268
|
exit(1)
|
268
269
|
end
|
270
|
+
end
|
269
271
|
|
272
|
+
def validate_strategy!
|
273
|
+
consumer_strategy = Chore.config.consumer_strategy.to_s
|
274
|
+
worker_strategy = Chore.config.worker_strategy.to_s
|
275
|
+
|
276
|
+
throttled_consumer = 'Chore::Strategy::ThrottledConsumerStrategy'
|
277
|
+
preforked_worker = 'Chore::Strategy::PreForkedWorkerStrategy'
|
278
|
+
|
279
|
+
if consumer_strategy == throttled_consumer || worker_strategy == preforked_worker
|
280
|
+
unless consumer_strategy == throttled_consumer && worker_strategy == preforked_worker
|
281
|
+
puts "=================================================================="
|
282
|
+
puts " PreForkedWorkerStrategy may only be paired with "
|
283
|
+
puts " ThrottledConsumerStrategy or vice versa "
|
284
|
+
puts " Please check your configurations "
|
285
|
+
puts "=================================================================="
|
286
|
+
exit(1)
|
287
|
+
end
|
288
|
+
end
|
270
289
|
end
|
271
290
|
end
|
272
291
|
end
|
data/lib/chore/consumer.rb
CHANGED
@@ -21,11 +21,6 @@ module Chore
|
|
21
21
|
def self.reset_connection!
|
22
22
|
end
|
23
23
|
|
24
|
-
# Cleans up any resources that were left behind from prior instances of the
|
25
|
-
# chore process. By default, this is a no-op.
|
26
|
-
def self.cleanup(queue)
|
27
|
-
end
|
28
|
-
|
29
24
|
# Consume takes a block with an arity of two. The two params are
|
30
25
|
# |message_id,message_body| where message_id is any object that the
|
31
26
|
# consumer will need to be able to act on a message later (reject, complete, etc)
|
@@ -53,5 +48,20 @@ module Chore
|
|
53
48
|
def running?
|
54
49
|
@running
|
55
50
|
end
|
51
|
+
|
52
|
+
# returns up to n work
|
53
|
+
def provide_work(n)
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
|
57
|
+
# now, given an arbitrary key and klass, have we seen the key already?
|
58
|
+
def duplicate_message?(dedupe_key, klass, queue_timeout)
|
59
|
+
dupe_detector.found_duplicate?(:id=>dedupe_key, :queue=>klass.to_s, :visibility_timeout=>queue_timeout)
|
60
|
+
end
|
61
|
+
|
62
|
+
def dupe_detector
|
63
|
+
@dupes ||= DuplicateDetector.new({:servers => Chore.config.dedupe_servers,
|
64
|
+
:dupe_on_cache_failure => false})
|
65
|
+
end
|
56
66
|
end
|
57
67
|
end
|
data/lib/chore/fetcher.rb
CHANGED
@@ -11,21 +11,16 @@ module Chore
|
|
11
11
|
# Starts the fetcher with the configured Consumer Strategy. This will begin consuming messages from your queue
|
12
12
|
def start
|
13
13
|
Chore.logger.info "Fetcher starting up"
|
14
|
-
|
15
|
-
# Clean up configured queues in case there are any resources left behind
|
16
|
-
Chore.config.queues.each do |queue|
|
17
|
-
Chore.config.consumer.cleanup(queue)
|
18
|
-
end
|
19
|
-
|
20
14
|
@strategy.fetch
|
21
15
|
end
|
22
16
|
|
23
17
|
# Stops the fetcher, preventing any further messages from being pulled from the queue
|
24
18
|
def stop!
|
25
19
|
unless @stopping
|
26
|
-
Chore.logger.info "Fetcher shutting down"
|
20
|
+
Chore.logger.info "Fetcher shutting down started"
|
27
21
|
@stopping = true
|
28
22
|
@strategy.stop!
|
23
|
+
Chore.logger.info "Fetcher shutting down completed"
|
29
24
|
end
|
30
25
|
end
|
31
26
|
|
@@ -33,5 +28,15 @@ module Chore
|
|
33
28
|
def stopping?
|
34
29
|
@stopping
|
35
30
|
end
|
31
|
+
|
32
|
+
# returns upto n work units
|
33
|
+
def provide_work(n)
|
34
|
+
@strategy.provide_work(n)
|
35
|
+
end
|
36
|
+
|
37
|
+
# gives work back to the consumer in case it couldn't be assigned
|
38
|
+
def return_work(work_units)
|
39
|
+
@strategy.return_work(work_units)
|
40
|
+
end
|
36
41
|
end
|
37
42
|
end
|
data/lib/chore/hooks.rb
CHANGED
@@ -15,7 +15,8 @@ module Chore
|
|
15
15
|
|
16
16
|
private
|
17
17
|
def hooks_for(event)
|
18
|
-
|
18
|
+
@_chore_hooks ||= {}
|
19
|
+
@_chore_hooks[event] ||= candidate_methods.grep(/^#{event}/).sort
|
19
20
|
end
|
20
21
|
|
21
22
|
# NOTE: Any hook methods defined after this is first referenced (i.e.,
|
data/lib/chore/job.rb
CHANGED
@@ -60,6 +60,12 @@ module Chore
|
|
60
60
|
raise ArgumentError, "#{self.to_s}: backoff must accept a single argument"
|
61
61
|
end
|
62
62
|
end
|
63
|
+
|
64
|
+
if @chore_options.key?(:dedupe_lambda)
|
65
|
+
if !@chore_options[:dedupe_lambda].is_a?(Proc)
|
66
|
+
raise ArgumentError, "#{self.to_s}: dedupe_lambda must be a lambda or Proc"
|
67
|
+
end
|
68
|
+
end
|
63
69
|
end
|
64
70
|
|
65
71
|
# This is a method so it can be overriden to create additional required
|
@@ -108,6 +114,17 @@ module Chore
|
|
108
114
|
def has_backoff?
|
109
115
|
self.options.key?(:backoff)
|
110
116
|
end
|
117
|
+
|
118
|
+
def has_dedupe_lambda?
|
119
|
+
self.options.key?(:dedupe_lambda)
|
120
|
+
end
|
121
|
+
|
122
|
+
def dedupe_key(*args)
|
123
|
+
return unless has_dedupe_lambda?
|
124
|
+
|
125
|
+
# run the proc to get the key
|
126
|
+
self.options[:dedupe_lambda].call(*args).to_s
|
127
|
+
end
|
111
128
|
end #ClassMethods
|
112
129
|
|
113
130
|
# This is handy to override in an included job to be able to do job setup that requires
|
data/lib/chore/manager.rb
CHANGED
@@ -5,19 +5,21 @@ require 'chore/fetcher'
|
|
5
5
|
module Chore
|
6
6
|
# Manages the interactions between fetching messages (Consumer Strategy), and working over them (Worker Strategy)
|
7
7
|
class Manager
|
8
|
+
include Util
|
8
9
|
|
9
10
|
def initialize()
|
10
11
|
Chore.logger.info "Booting Chore #{Chore::VERSION}"
|
11
12
|
Chore.logger.debug { Chore.config.inspect }
|
13
|
+
procline("#{Chore.config.master_procline}:Started:#{Time.now}")
|
12
14
|
@started_at = nil
|
13
15
|
@worker_strategy = Chore.config.worker_strategy.new(self)
|
14
16
|
@fetcher = Chore.config.fetcher.new(self)
|
15
|
-
@processed = 0
|
16
17
|
@stopping = false
|
17
18
|
end
|
18
19
|
|
19
20
|
# Start the Manager. This calls both the #start method of the configured Worker Strategy, as well as Fetcher#start.
|
20
21
|
def start
|
22
|
+
Chore.run_hooks_for(:before_start)
|
21
23
|
@started_at = Time.now
|
22
24
|
@worker_strategy.start
|
23
25
|
@fetcher.start
|
@@ -26,11 +28,12 @@ module Chore
|
|
26
28
|
# Shut down the Manager, the Worker Strategy, and the Fetcher. This calls the +:before_shutdown+ hook.
|
27
29
|
def shutdown!
|
28
30
|
unless @stopping
|
29
|
-
Chore.logger.info "Manager shutting down"
|
31
|
+
Chore.logger.info "Manager shutting down started"
|
30
32
|
@stopping = true
|
31
33
|
Chore.run_hooks_for(:before_shutdown)
|
32
34
|
@fetcher.stop!
|
33
35
|
@worker_strategy.stop!
|
36
|
+
Chore.logger.info "Manager shutting down completed"
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
@@ -41,7 +44,20 @@ module Chore
|
|
41
44
|
# than they can be consumed.
|
42
45
|
def assign(work)
|
43
46
|
Chore.logger.debug { "Manager#assign: No. of UnitsOfWork: #{work.length})" }
|
47
|
+
work.each do | item |
|
48
|
+
Chore.run_hooks_for(:before_send_to_worker, item)
|
49
|
+
end
|
44
50
|
@worker_strategy.assign(work) unless @stopping
|
45
51
|
end
|
52
|
+
|
53
|
+
# returns up to n from the throttled consumer queue
|
54
|
+
def fetch_work(n)
|
55
|
+
@fetcher.provide_work(n)
|
56
|
+
end
|
57
|
+
|
58
|
+
# gives work back to the fetcher in case it couldn't be assigned
|
59
|
+
def return_work(work_units)
|
60
|
+
@fetcher.return_work(work_units)
|
61
|
+
end
|
46
62
|
end
|
47
63
|
end
|
@@ -21,55 +21,105 @@ module Chore
|
|
21
21
|
|
22
22
|
Chore::CLI.register_option 'fs_queue_root', '--fs-queue-root DIRECTORY', 'Root directory for fs based queue'
|
23
23
|
|
24
|
-
FILE_QUEUE_MUTEXES = {}
|
25
|
-
|
26
24
|
class << self
|
27
|
-
# Cleans up
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
62
|
-
|
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 =
|
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,38 +127,46 @@ 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 =
|
132
|
+
@queue_timeout = self.class.queue_timeout(queue_name)
|
88
133
|
end
|
89
134
|
|
90
|
-
def consume
|
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
|
-
#
|
95
|
-
|
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_jobs 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
|
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
|
-
|
106
|
-
make_new_again(id)
|
161
|
+
|
107
162
|
end
|
108
163
|
|
109
164
|
def complete(id)
|
110
165
|
Chore.logger.debug "Completing (deleting): #{id}"
|
111
|
-
|
166
|
+
File.delete(File.join(@in_progress_dir, id))
|
167
|
+
rescue Errno::ENOENT
|
168
|
+
# The job took too long to complete, was deemed expired, and moved
|
169
|
+
# back into "new". Ignore.
|
112
170
|
end
|
113
171
|
|
114
172
|
private
|
@@ -116,27 +174,26 @@ module Chore
|
|
116
174
|
# finds all new job files, moves them to in progress and starts the job
|
117
175
|
# Returns a list of the job files processed
|
118
176
|
def handle_jobs(&block)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
177
|
+
self.class.each_file(@new_dir, Chore.config.queue_polling_size) do |job_file|
|
178
|
+
Chore.logger.debug "Found a new job #{job_file}"
|
179
|
+
|
180
|
+
in_progress_path = make_in_progress(job_file)
|
181
|
+
next unless in_progress_path
|
182
|
+
|
183
|
+
# The job filename may have changed, so update it to reflect the in progress path
|
184
|
+
job_file = File.basename(in_progress_path)
|
185
|
+
|
186
|
+
job_json = File.read(in_progress_path)
|
187
|
+
basename, previous_attempts, * = self.class.file_info(job_file)
|
188
|
+
|
189
|
+
# job_file is just the name which is the job id
|
190
|
+
block.call(job_file, queue_name, queue_timeout, job_json, previous_attempts)
|
191
|
+
Chore.run_hooks_for(:on_fetch, job_file, job_json)
|
135
192
|
end
|
136
193
|
end
|
137
194
|
|
138
195
|
def make_in_progress(job)
|
139
|
-
self.class.make_in_progress(job, @new_dir, @in_progress_dir)
|
196
|
+
self.class.make_in_progress(job, @new_dir, @in_progress_dir, @queue_timeout)
|
140
197
|
end
|
141
198
|
|
142
199
|
def make_new_again(job)
|