chore-core 1.8.2 → 3.2.3

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 (51) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +6 -0
  3. data/chore-core.gemspec +1 -0
  4. data/lib/chore.rb +11 -5
  5. data/lib/chore/cli.rb +21 -2
  6. data/lib/chore/consumer.rb +15 -5
  7. data/lib/chore/fetcher.rb +12 -7
  8. data/lib/chore/hooks.rb +2 -1
  9. data/lib/chore/job.rb +17 -0
  10. data/lib/chore/manager.rb +18 -2
  11. data/lib/chore/queues/filesystem/consumer.rb +116 -59
  12. data/lib/chore/queues/filesystem/filesystem_queue.rb +19 -0
  13. data/lib/chore/queues/filesystem/publisher.rb +12 -18
  14. data/lib/chore/queues/sqs/consumer.rb +6 -21
  15. data/lib/chore/strategies/consumer/batcher.rb +8 -9
  16. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +3 -1
  17. data/lib/chore/strategies/consumer/throttled_consumer_strategy.rb +121 -0
  18. data/lib/chore/strategies/worker/forked_worker_strategy.rb +5 -6
  19. data/lib/chore/strategies/worker/helpers/ipc.rb +88 -0
  20. data/lib/chore/strategies/worker/helpers/preforked_worker.rb +163 -0
  21. data/lib/chore/strategies/worker/helpers/work_distributor.rb +65 -0
  22. data/lib/chore/strategies/worker/helpers/worker_info.rb +13 -0
  23. data/lib/chore/strategies/worker/helpers/worker_killer.rb +40 -0
  24. data/lib/chore/strategies/worker/helpers/worker_manager.rb +183 -0
  25. data/lib/chore/strategies/worker/preforked_worker_strategy.rb +150 -0
  26. data/lib/chore/strategies/worker/single_worker_strategy.rb +35 -13
  27. data/lib/chore/unit_of_work.rb +8 -0
  28. data/lib/chore/util.rb +5 -1
  29. data/lib/chore/version.rb +3 -3
  30. data/lib/chore/worker.rb +29 -0
  31. data/spec/chore/cli_spec.rb +2 -2
  32. data/spec/chore/consumer_spec.rb +0 -4
  33. data/spec/chore/duplicate_detector_spec.rb +17 -5
  34. data/spec/chore/fetcher_spec.rb +0 -11
  35. data/spec/chore/manager_spec.rb +7 -0
  36. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -11
  37. data/spec/chore/queues/sqs/consumer_spec.rb +1 -3
  38. data/spec/chore/strategies/consumer/batcher_spec.rb +50 -0
  39. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +1 -0
  40. data/spec/chore/strategies/consumer/throttled_consumer_strategy_spec.rb +165 -0
  41. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +16 -1
  42. data/spec/chore/strategies/worker/helpers/ipc_spec.rb +127 -0
  43. data/spec/chore/strategies/worker/helpers/preforked_worker_spec.rb +236 -0
  44. data/spec/chore/strategies/worker/helpers/work_distributor_spec.rb +131 -0
  45. data/spec/chore/strategies/worker/helpers/worker_info_spec.rb +14 -0
  46. data/spec/chore/strategies/worker/helpers/worker_killer_spec.rb +97 -0
  47. data/spec/chore/strategies/worker/helpers/worker_manager_spec.rb +304 -0
  48. data/spec/chore/strategies/worker/preforked_worker_strategy_spec.rb +183 -0
  49. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +25 -0
  50. data/spec/chore/worker_spec.rb +69 -1
  51. metadata +33 -5
@@ -0,0 +1,150 @@
1
+ require 'chore/signal'
2
+ require 'socket'
3
+ require 'chore/strategies/worker/helpers/ipc'
4
+ require 'chore/strategies/worker/helpers/preforked_worker'
5
+ require 'chore/strategies/worker/helpers/worker_manager'
6
+ require 'chore/strategies/worker/helpers/work_distributor'
7
+
8
+ module Chore
9
+ module Strategy
10
+ class PreForkedWorkerStrategy #:nodoc:
11
+ include Ipc
12
+
13
+ NUM_TO_SIGNAL = { '1' => :CHLD,
14
+ '2' => :INT,
15
+ '3' => :QUIT,
16
+ '4' => :TERM,
17
+ '5' => :USR1 }.freeze
18
+
19
+ def initialize(manager, opts = {})
20
+ @options = opts
21
+ @manager = manager
22
+ @self_read, @self_write = IO.pipe
23
+ trap_signals(NUM_TO_SIGNAL, @self_write)
24
+ @worker_manager = WorkerManager.new(create_master_socket)
25
+ at_exit { delete_socket_file }
26
+ @running = true
27
+ end
28
+
29
+ def start
30
+ Chore.logger.info "PWS: Starting up worker strategy: #{self.class.name}"
31
+ Chore.run_hooks_for(:before_first_fork)
32
+ @worker_manager.create_and_attach_workers
33
+ worker_assignment_thread
34
+ end
35
+
36
+ def stop!
37
+ Chore.logger.info "PWS: Stopping worker strategy: #{self.class.name}"
38
+ @running = false
39
+ end
40
+
41
+ private
42
+
43
+ def worker_assignment_thread
44
+ Thread.new do
45
+ begin
46
+ worker_assignment_loop
47
+ rescue Chore::TerribleMistake => e
48
+ Chore.logger.error 'PWS: Terrible mistake, shutting down Chore'
49
+ Chore.logger.error e.message
50
+ Chore.logger.error e.backtrace
51
+ @manager.shutdown!
52
+ ensure
53
+ Chore.logger.info 'PWS: worker_assignment_thread ending'
54
+ # WorkerAssignment thread is independent of the main thread.
55
+ # The main thread is waiting on the consumer threads to join,
56
+ # Due to some weird SQS behaviour, its possible that these threads
57
+ # maynot join, and the assigment thread always exits, since it's
58
+ # nonblocking. This will ensure that the master process exits.
59
+ Process.exit(true)
60
+ end
61
+ end
62
+ end
63
+
64
+ def worker_assignment_loop
65
+ while running?
66
+ w_sockets = @worker_manager.worker_sockets
67
+
68
+ # select_sockets returns a list of readable sockets
69
+ # This would include worker connections and the read end
70
+ # of the self-pipe.
71
+ #
72
+ # Note this not only returns sockets from live workers
73
+ # that are readable, but it also returns sockets from
74
+ # *dead* workers. If the worker hasn't already been reaped,
75
+ # then we might get a socket for a dead worker than will
76
+ # fail on write.
77
+ readables, = select_sockets(w_sockets, @self_read)
78
+
79
+ # If select timed out, retry
80
+ if readables.nil?
81
+ Chore.logger.debug 'PWS: All sockets busy.. retry'
82
+ next
83
+ end
84
+
85
+ # Handle the signal from the self-pipe
86
+ if readables.include?(@self_read)
87
+ handle_signal
88
+ next
89
+ end
90
+
91
+ # Confirm they're actually alive! A socket will be readable even
92
+ # if the worker has died but not yet been reaped by the master. We
93
+ # need to confirm that the "Ready" flag has actually been written by
94
+ # the worker and readable by the master.
95
+ readables.reject! {|readable| readable.eof?}
96
+
97
+ # Check again to see if there are still sockets available
98
+ if readables.empty?
99
+ Chore.logger.debug 'PWS: All sockets busy.. retry'
100
+ next
101
+ end
102
+
103
+ # Fetch and assign work for the readable worker connections
104
+ @worker_manager.ready_workers(readables) do |workers|
105
+ WorkDistributor.fetch_and_assign_jobs(workers, @manager)
106
+ end
107
+ end
108
+ Chore.logger.info 'PWS: worker_assignment_loop ending'
109
+ end
110
+
111
+ # Wrapper need around running to help writing specs for worker_assignment_loop
112
+ def running?
113
+ @running
114
+ end
115
+
116
+ def handle_signal
117
+ signal = NUM_TO_SIGNAL[@self_read.read_nonblock(1)]
118
+ Chore.logger.info "PWS: recv #{signal}"
119
+
120
+ case signal
121
+ when :CHLD
122
+ @worker_manager.respawn_terminated_workers!
123
+ when :INT, :QUIT, :TERM
124
+ Signal.reset
125
+ @worker_manager.stop_workers(signal)
126
+ @manager.shutdown!
127
+ when :USR1
128
+ Chore.reopen_logs
129
+ Chore.logger.info 'PWS: Master process reopened log'
130
+ end
131
+ end
132
+
133
+ # Wrapper around fork for specs.
134
+ def fork(&block)
135
+ Kernel.fork(&block)
136
+ end
137
+
138
+ # In the event of a trapped signal, write to the self-pipe
139
+ def trap_signals(signal_hash, write_end)
140
+ Signal.reset
141
+
142
+ signal_hash.each do |sig_num, signal|
143
+ Signal.trap(signal) do
144
+ write_end.write(sig_num)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -10,7 +10,10 @@ module Chore
10
10
  def initialize(manager, opts={})
11
11
  @options = opts
12
12
  @manager = manager
13
+ @stopped = false
13
14
  @worker = nil
15
+ @queue = Queue.new
16
+ @queue << :worker
14
17
  end
15
18
 
16
19
  # Starts the <tt>SingleWorkerStrategy</tt>. Currently a noop
@@ -18,6 +21,11 @@ module Chore
18
21
 
19
22
  # Stops the <tt>SingleWorkerStrategy</tt> if there is a worker to stop
20
23
  def stop!
24
+ return if @stopped
25
+
26
+ @stopped = true
27
+ Chore.logger.info { "Manager #{Process.pid} stopping" }
28
+
21
29
  worker.stop! if worker
22
30
  end
23
31
 
@@ -25,16 +33,14 @@ module Chore
25
33
  # single worker strategy, this should never be called if the worker is in
26
34
  # progress.
27
35
  def assign(work)
28
- if workers_available?
29
- begin
30
- @worker = worker_klass.new(work, @options)
31
- @worker.start
32
- true
33
- ensure
34
- @worker = nil
35
- end
36
- else
37
- Chore.logger.error { "#{self.class}#assign: single worker is unavailable, but assign has been re-entered: #{caller * "\n"}" }
36
+ return unless acquire_worker
37
+
38
+ begin
39
+ @worker = worker_klass.new(work, @options)
40
+ @worker.start
41
+ true
42
+ ensure
43
+ release_worker
38
44
  end
39
45
  end
40
46
 
@@ -42,9 +48,25 @@ module Chore
42
48
  Worker
43
49
  end
44
50
 
45
- # Returns true if there is currently no worker
46
- def workers_available?
47
- @worker.nil?
51
+ private
52
+
53
+ # Attempts to essentially acquire a lock on a worker. If no workers are
54
+ # available, then this will block until one is.
55
+ def acquire_worker
56
+ result = @queue.pop
57
+
58
+ if @stopped
59
+ # Strategy has stopped since the worker was acquired
60
+ release_worker
61
+ nil
62
+ else
63
+ result
64
+ end
65
+ end
66
+
67
+ # Releases the lock on a worker so that another thread can pick it up.
68
+ def release_worker
69
+ @queue << :worker
48
70
  end
49
71
  end
50
72
  end
@@ -9,6 +9,14 @@ module Chore
9
9
  # * +:consumer+ The consumer instance used to fetch this message. Most queue implementations won't need access to this, but some (RabbitMQ) will. So we
10
10
  # make sure to pass it along with each message. This instance will be used by the Worker for things like <tt>complete</tt> and </tt>reject</tt>.
11
11
  class UnitOfWork < Struct.new(:id,:queue_name,:queue_timeout,:message,:previous_attempts,:consumer,:decoded_message, :klass)
12
+ # The time at which this unit of work was created
13
+ attr_accessor :created_at
14
+
15
+ def initialize(*) #:nodoc:
16
+ super
17
+ @created_at = Time.now
18
+ end
19
+
12
20
  # The current attempt number for the worker processing this message.
13
21
  def current_attempt
14
22
  previous_attempts + 1
@@ -2,7 +2,7 @@ module Chore
2
2
 
3
3
  # Collection of utilities and helpers used by Chore internally
4
4
  module Util
5
-
5
+
6
6
  # To avoid bringing in all of active_support, we implemented constantize here
7
7
  def constantize(camel_cased_word)
8
8
  names = camel_cased_word.split('::')
@@ -14,5 +14,9 @@ module Chore
14
14
  end
15
15
  constant
16
16
  end
17
+
18
+ def procline(str)
19
+ $0 = str
20
+ end
17
21
  end
18
22
  end
@@ -1,8 +1,8 @@
1
1
  module Chore
2
2
  module Version #:nodoc:
3
- MAJOR = 1
4
- MINOR = 8
5
- PATCH = 2
3
+ MAJOR = 3
4
+ MINOR = 2
5
+ PATCH = 3
6
6
 
7
7
  STRING = [ MAJOR, MINOR, PATCH ].join('.')
8
8
  end
@@ -42,6 +42,28 @@ module Chore
42
42
  @started_at + total_timeout
43
43
  end
44
44
 
45
+ def duplicate_work?(item)
46
+ # if we've got a duplicate, remove the message from the queue by not actually running and also not reporting any errors
47
+ payload = options[:payload_handler].payload(item.decoded_message)
48
+
49
+ # if we're hitting the custom dedupe key, we want to remove this message from the queue
50
+ if item.klass.has_dedupe_lambda?
51
+ dedupe_key = item.klass.dedupe_key(*payload)
52
+ if dedupe_key.nil? || dedupe_key.strip.empty? # if the dedupe key is nil, don't continue with the rest of the dedupe lambda logic
53
+ Chore.logger.info { "#{item.klass} dedupe key nil, skipping memcached lookup." }
54
+ return false
55
+ end
56
+
57
+ if item.consumer.duplicate_message?(dedupe_key, item.klass, item.queue_timeout)
58
+ Chore.logger.info { "Found and deleted duplicate job #{item.klass}"}
59
+ item.consumer.complete(item.id)
60
+ return true
61
+ end
62
+ end
63
+
64
+ return false
65
+ end
66
+
45
67
  # The workhorse. Do the work, all of it. This will block for an entirely unspecified amount
46
68
  # of time based on the work to be performed. This will:
47
69
  # * Decode each message.
@@ -58,6 +80,10 @@ module Chore
58
80
  begin
59
81
  item.decoded_message = options[:payload_handler].decode(item.message)
60
82
  item.klass = options[:payload_handler].payload_class(item.decoded_message)
83
+
84
+ next if duplicate_work?(item)
85
+
86
+ Chore.run_hooks_for(:worker_to_start, item)
61
87
  start_item(item)
62
88
  rescue => e
63
89
  Chore.logger.error { "Failed to run job for #{item.message} with error: #{e.message} #{e.backtrace * "\n"}" }
@@ -66,6 +92,7 @@ module Chore
66
92
  item.consumer.complete(item.id)
67
93
  else
68
94
  Chore.run_hooks_for(:on_failure,item.message,e)
95
+ item.consumer.reject(item.id)
69
96
  end
70
97
  end
71
98
  end
@@ -88,6 +115,7 @@ module Chore
88
115
  item.consumer.complete(item.id)
89
116
  Chore.logger.info { "Finished job #{klass} with params #{message}"}
90
117
  klass.run_hooks_for(:after_perform, message)
118
+ Chore.run_hooks_for(:worker_ended, item)
91
119
  rescue Job::RejectMessageException
92
120
  item.consumer.reject(item.id)
93
121
  Chore.logger.error { "Failed to run job for #{item.message} with error: Job raised a RejectMessageException" }
@@ -116,6 +144,7 @@ module Chore
116
144
  item.consumer.complete(item.id)
117
145
  else
118
146
  klass.run_hooks_for(:on_failure, message, e)
147
+ item.consumer.reject(item.id)
119
148
  end
120
149
  end
121
150
 
@@ -201,8 +201,8 @@ describe Chore::CLI do
201
201
 
202
202
  context 'given no value' do
203
203
  let(:command) { [] }
204
- it 'is the default value, nil' do
205
- subject.should == nil
204
+ it 'is the default value, 1' do
205
+ subject.should == 1
206
206
  end
207
207
  end
208
208
  end
@@ -22,10 +22,6 @@ describe Chore::Consumer do
22
22
  Chore::Consumer.should respond_to :reset_connection!
23
23
  end
24
24
 
25
- it 'should have a class level cleanup method' do
26
- Chore::Consumer.should respond_to :cleanup
27
- end
28
-
29
25
  it 'should not have an implemented consume method' do
30
26
  expect { consumer.consume }.to raise_error(NotImplementedError)
31
27
  end
@@ -2,7 +2,21 @@ require 'spec_helper'
2
2
  require 'securerandom'
3
3
 
4
4
  describe Chore::DuplicateDetector do
5
- let(:memcache) { double("memcache") }
5
+ class FakeDalli
6
+ def initialize
7
+ @store = {}
8
+ end
9
+ def add(id, val, ttl=0)
10
+ if @store[id] && @store[id][:inserted] + @store[id][:ttl] > Time.now.to_i
11
+ return false
12
+ else
13
+ @store[id] = {:val => val, :ttl => ttl, :inserted => Time.now.to_i}
14
+ return true
15
+ end
16
+ end
17
+ end
18
+
19
+ let(:memcache) { FakeDalli.new }
6
20
  let(:dupe_on_cache_failure) { false }
7
21
  let(:dedupe_params) { { :memcached_client => memcache, :dupe_on_cache_failure => dupe_on_cache_failure } }
8
22
  let(:dedupe) { Chore::DuplicateDetector.new(dedupe_params)}
@@ -15,12 +29,11 @@ describe Chore::DuplicateDetector do
15
29
 
16
30
  describe "#found_duplicate" do
17
31
  it 'should not return true if the message has not already been seen' do
18
- expect(memcache).to receive(:add).and_return(true)
19
32
  expect(dedupe.found_duplicate?(message_data)).to_not be true
20
33
  end
21
34
 
22
35
  it 'should return true if the message has already been seen' do
23
- expect(memcache).to receive(:add).and_return(false)
36
+ memcache.add(message_data[:id], 1, message_data[:visibility_timeout])
24
37
  expect(dedupe.found_duplicate?(message_data)).to be true
25
38
  end
26
39
 
@@ -34,13 +47,12 @@ describe Chore::DuplicateDetector do
34
47
  end
35
48
 
36
49
  it "should set the timeout to be the queue's " do
37
- expect(memcache).to receive(:add).with(id,"1",timeout).and_return(true)
50
+ expect(memcache).to receive(:add).with(id,"1",timeout).and_call_original
38
51
  expect(dedupe.found_duplicate?(message_data)).to be false
39
52
  end
40
53
 
41
54
  it "should call #visibility_timeout once and only once" do
42
55
  expect(queue).to receive(:visibility_timeout).once
43
- expect(memcache).to receive(:add).at_least(3).times.and_return(true)
44
56
  3.times { dedupe.found_duplicate?(message_data) }
45
57
  end
46
58
 
@@ -35,15 +35,4 @@ describe Chore::Fetcher do
35
35
  fetcher.start
36
36
  end
37
37
  end
38
-
39
- describe "cleaning up" do
40
- before(:each) do
41
- manager.stub(:assign)
42
- end
43
-
44
- it "should run cleanup on each queue" do
45
- consumer.should_receive(:cleanup).with('test')
46
- fetcher.start
47
- end
48
- end
49
38
  end
@@ -35,6 +35,13 @@ describe Chore::Manager do
35
35
  manager.assign(work)
36
36
  end
37
37
  end
38
+
39
+ describe 'returning work' do
40
+ it 'should return work to the fetcher' do
41
+ expect(fetcher).to receive(:return_work).with([work])
42
+ manager.return_work([work])
43
+ end
44
+ end
38
45
  end
39
46
 
40
47
  end
@@ -8,10 +8,16 @@ describe Chore::Queues::Filesystem::Consumer do
8
8
  let(:publisher) { Chore::Queues::Filesystem::Publisher.new }
9
9
  let(:test_queues_dir) { "test-queues" }
10
10
  let(:test_queue) { "test-queue" }
11
+ let(:default_timeout) { 60 }
12
+ let(:timeout) { nil }
11
13
 
12
14
  before do
13
15
  Chore.config.fs_queue_root = test_queues_dir
14
- expect(Chore.config).to receive(:default_queue_timeout).and_return(60)
16
+ if timeout
17
+ File.open("#{config_dir}/timeout", "w") {|f| f << timeout.to_s}
18
+ else
19
+ expect(Chore.config).to receive(:default_queue_timeout).and_return(default_timeout)
20
+ end
15
21
  allow(consumer).to receive(:sleep)
16
22
  end
17
23
 
@@ -22,35 +28,79 @@ describe Chore::Queues::Filesystem::Consumer do
22
28
  let(:test_job_hash) {{:class => "TestClass", :args => "test-args"}}
23
29
  let(:new_dir) { described_class.new_dir(test_queue) }
24
30
  let(:in_progress_dir) { described_class.in_progress_dir(test_queue) }
31
+ let(:config_dir) { described_class.config_dir(test_queue) }
25
32
 
26
33
  describe ".cleanup" do
27
- it "should move in_progress jobs to new dir" do
34
+ it "should move expired in_progress jobs to new dir" do
35
+ timestamp = Time.now.to_i - 1
36
+
37
+ FileUtils.touch("#{in_progress_dir}/foo.1.#{timestamp}.job")
38
+ described_class.cleanup(Time.now.to_i, new_dir, in_progress_dir)
39
+ expect(File.exist?("#{new_dir}/foo.2.job")).to eq(true)
40
+ end
41
+
42
+ it "should move non-timestamped jobs from in_progress_dir to new dir" do
28
43
  FileUtils.touch("#{in_progress_dir}/foo.1.job")
29
- described_class.cleanup(test_queue)
44
+ described_class.cleanup(Time.now.to_i, new_dir, in_progress_dir)
30
45
  expect(File.exist?("#{new_dir}/foo.2.job")).to eq(true)
31
46
  end
47
+
48
+ it "should not affect non-expired jobs" do
49
+ timestamp = Time.now.to_i - 1
50
+
51
+ FileUtils.touch("#{in_progress_dir}/foo.1.#{timestamp}.job")
52
+ described_class.cleanup(Time.now.to_i - 2, new_dir, in_progress_dir)
53
+ expect(File.exist?("#{new_dir}/foo.2.job")).to eq(false)
54
+ end
32
55
  end
33
56
 
34
57
  describe ".make_in_progress" do
35
- it "should move job to in_progress dir" do
58
+ it "should move non-empty job to in_progress dir" do
59
+ now = Time.now
60
+
61
+ Timecop.freeze(now) do
62
+ File.open("#{new_dir}/foo.1.job", "w") {|f| f << "{}"}
63
+ described_class.make_in_progress("foo.1.job", new_dir, in_progress_dir, default_timeout)
64
+ expect(File.exist?("#{in_progress_dir}/foo.1.#{now.to_i}.job")).to eq(true)
65
+ end
66
+ end
67
+
68
+ it "should not move empty jobs to in_progress dir" do
69
+ now = Time.now
70
+
71
+ Timecop.freeze(now) do
72
+ FileUtils.touch("#{new_dir}/foo.1.job")
73
+ described_class.make_in_progress("foo.1.job", new_dir, in_progress_dir, default_timeout)
74
+ expect(File.exist?("#{new_dir}/foo.1.job")).to eq(true)
75
+ expect(File.exist?("#{in_progress_dir}/foo.1.#{now.to_i}.job")).to eq(false)
76
+ end
77
+ end
78
+
79
+ it "should delete expired empty jobs" do
36
80
  FileUtils.touch("#{new_dir}/foo.1.job")
37
- described_class.make_in_progress("foo.1.job", new_dir, in_progress_dir)
38
- expect(File.exist?("#{in_progress_dir}/foo.1.job")).to eq(true)
81
+
82
+ now = Time.now + default_timeout
83
+ Timecop.freeze(now) do
84
+ described_class.make_in_progress("foo.1.job", new_dir, in_progress_dir, default_timeout)
85
+ expect(File.exist?("#{new_dir}/foo.1.job")).to eq(false)
86
+ expect(File.exist?("#{in_progress_dir}/foo.1.#{now.to_i}.job")).to eq(false)
87
+ end
39
88
  end
40
89
  end
41
90
 
42
91
  describe ".make_new_again" do
43
92
  it "should move job to new dir" do
44
- FileUtils.touch("#{in_progress_dir}/foo.1.job")
45
- described_class.make_new_again("foo.1.job", new_dir, in_progress_dir)
93
+ timestamp = Time.now.to_i
94
+ FileUtils.touch("#{in_progress_dir}/foo.1.#{timestamp}.job")
95
+ described_class.make_new_again("foo.1.#{timestamp}.job", new_dir, in_progress_dir)
46
96
  expect(File.exist?("#{new_dir}/foo.2.job")).to eq(true)
47
97
  end
48
98
  end
49
99
 
50
- describe ".job_files" do
100
+ describe ".each_file" do
51
101
  it "should list jobs in dir" do
52
102
  FileUtils.touch("#{new_dir}/foo.1.job")
53
- expect(described_class.job_files(new_dir)).to eq(["foo.1.job"])
103
+ expect {|b| described_class.each_file(new_dir, &b) }.to yield_with_args("foo.1.job")
54
104
  end
55
105
  end
56
106
 
@@ -85,7 +135,9 @@ describe Chore::Queues::Filesystem::Consumer do
85
135
  end
86
136
  expect(rejected).to be true
87
137
 
88
- expect { |b| consumer.consume(&b) }.to yield_with_args(anything, 'test-queue', 60, test_job_hash.to_json, 1)
138
+ Timecop.freeze(Time.now + 61) do
139
+ expect { |b| consumer.consume(&b) }.to yield_with_args(anything, 'test-queue', 60, test_job_hash.to_json, 1)
140
+ end
89
141
  end
90
142
  end
91
143
 
@@ -103,6 +155,14 @@ describe Chore::Queues::Filesystem::Consumer do
103
155
  expect { |b| consumer.consume(&b) }.to_not yield_control
104
156
  end
105
157
  end
158
+
159
+ context "with queue-specific timeout config" do
160
+ let(:timeout) { 30 }
161
+
162
+ it "should consume a published job and yield the job to the handler block" do
163
+ expect { |b| consumer.consume(&b) }.to yield_with_args(anything, 'test-queue', 30, test_job_hash.to_json, 0)
164
+ end
165
+ end
106
166
  end
107
167
 
108
168
  context "not finding a published job" do