resque-clues 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +163 -0
- data/Rakefile +6 -0
- data/TODO +4 -0
- data/lib/resque-clues.rb +78 -0
- data/lib/resque/plugins/clues/event_publisher.rb +91 -0
- data/lib/resque/plugins/clues/job_extension.rb +65 -0
- data/lib/resque/plugins/clues/queue_extension.rb +69 -0
- data/lib/resque/plugins/clues/util.rb +45 -0
- data/lib/resque/plugins/clues/version.rb +5 -0
- data/resque-clues.gemspec +26 -0
- data/spec/event_publisher_spec.rb +193 -0
- data/spec/integration_spec.rb +183 -0
- data/spec/job_extension_spec.rb +100 -0
- data/spec/queue_extension_spec.rb +137 -0
- data/spec/spec_helper.rb +73 -0
- data/spec/test_publisher.rb +42 -0
- data/spec/test_worker.rb +6 -0
- data/spec/util_spec.rb +31 -0
- metadata +193 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Resque
|
5
|
+
module Plugins
|
6
|
+
module Clues
|
7
|
+
# Module capable of redefining the Resque#push and Resque#pop methods so
|
8
|
+
# that:
|
9
|
+
#
|
10
|
+
# * metadata will be stored in redis.
|
11
|
+
# * The metadata can be injected with arbitrary data by a configured item
|
12
|
+
# preprocessor.
|
13
|
+
# * That event data (including its metadata) will be published, provided
|
14
|
+
# an event publisher has been configured.
|
15
|
+
module QueueExtension
|
16
|
+
# push an item onto the queue. If resque-clues is configured, this
|
17
|
+
# will First create the metadata associated with the event and adds it
|
18
|
+
# to the item. This will include:
|
19
|
+
#
|
20
|
+
# * event_hash: a unique item identifying the job, will be included
|
21
|
+
# with other events arising from that job.
|
22
|
+
# * hostname: the hostname of the machine where the event occurred.
|
23
|
+
# * process: The process id of the ruby process where the event
|
24
|
+
# occurred.
|
25
|
+
# * plus any items injected into the item via a configured
|
26
|
+
# item_preprocessor.
|
27
|
+
#
|
28
|
+
# After that, an enqueued event is published and the original push
|
29
|
+
# operation is invoked.
|
30
|
+
#
|
31
|
+
# queue:: The queue to push onto
|
32
|
+
# orig:: The original item to push onto the queue.
|
33
|
+
def _clues_push(queue, item)
|
34
|
+
return _base_push(queue, item) unless Clues.clues_configured?
|
35
|
+
item['clues_metadata'] = {
|
36
|
+
'event_hash' => Clues.event_hash,
|
37
|
+
'hostname' => Clues.hostname,
|
38
|
+
'process' => Clues.process,
|
39
|
+
'enqueued_time' => Time.now.utc.to_f
|
40
|
+
}
|
41
|
+
if Resque::Plugins::Clues.item_preprocessor
|
42
|
+
Resque::Plugins::Clues.item_preprocessor.call(queue, item)
|
43
|
+
end
|
44
|
+
Clues.event_publisher.publish(:enqueued, Clues.now, queue, item['clues_metadata'], item[:class], *item[:args])
|
45
|
+
_base_push(queue, item)
|
46
|
+
end
|
47
|
+
|
48
|
+
# pops an item off the head of the queue. This will use the original
|
49
|
+
# pop operation to get the item, then calculate the time in queue and
|
50
|
+
# broadcast a dequeued event.
|
51
|
+
#
|
52
|
+
# queue:: The queue to pop from.
|
53
|
+
def _clues_pop(queue)
|
54
|
+
_base_pop(queue).tap do |item|
|
55
|
+
# TODO refactor
|
56
|
+
unless item.nil?
|
57
|
+
if Clues.clues_configured? and item['clues_metadata']
|
58
|
+
item['clues_metadata']['hostname'] = Clues.hostname
|
59
|
+
item['clues_metadata']['process'] = Clues.process
|
60
|
+
item['clues_metadata']['time_in_queue'] = Clues.time_delta_since(item['clues_metadata']['enqueued_time'])
|
61
|
+
Clues.event_publisher.publish(:dequeued, Clues.now, queue, item['clues_metadata'], item['class'], *item['args'])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module Resque
|
5
|
+
module Plugins
|
6
|
+
module Clues
|
7
|
+
# A unique event hash crafted from the hostname, process and time.
|
8
|
+
def self.event_hash
|
9
|
+
Digest::MD5.hexdigest("#{hostname}#{process}#{Time.now.utc.to_f}")
|
10
|
+
end
|
11
|
+
|
12
|
+
# The hostname Resque is running on.
|
13
|
+
def self.hostname
|
14
|
+
@hostname ||= Socket.gethostname
|
15
|
+
end
|
16
|
+
|
17
|
+
# The process id.
|
18
|
+
def self.process
|
19
|
+
$$
|
20
|
+
end
|
21
|
+
|
22
|
+
# The current UTC time in iso 8601 format.
|
23
|
+
def self.now
|
24
|
+
Time.now.utc.iso8601
|
25
|
+
end
|
26
|
+
|
27
|
+
# The delta of time between the passed time and now.
|
28
|
+
def self.time_delta_since(start)
|
29
|
+
result = Time.now.utc.to_f - start.to_f
|
30
|
+
result >= 0.0 ? result : 0.0
|
31
|
+
end
|
32
|
+
|
33
|
+
# Convenience method to determine if resque-clues is properly
|
34
|
+
# configured.
|
35
|
+
def self.clues_configured?
|
36
|
+
Resque::Plugins::Clues.configured?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convenience method to access the resque-clues event publisher.
|
40
|
+
def self.event_publisher
|
41
|
+
Resque::Plugins::Clues.event_publisher
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'resque/plugins/clues/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "resque-clues"
|
8
|
+
gem.version = Resque::Clues::VERSION
|
9
|
+
gem.authors = ["Lance Woodson"]
|
10
|
+
gem.email = ["lance.woodson@peopleadmin.com"]
|
11
|
+
gem.description = %q{Adds event publishing and job tracking ability to Resque}
|
12
|
+
gem.summary = %q{Adds event publishing and job tracking}
|
13
|
+
gem.homepage = "https://github.com/PeopleAdmin/resque-clues"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.add_dependency 'resque', '>= 1.20.0'
|
20
|
+
gem.add_dependency 'multi_json', '~> 1.7.4'
|
21
|
+
gem.add_development_dependency 'rake', '~> 0.9.2.2'
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
gem.add_development_dependency 'pry'
|
24
|
+
gem.add_development_dependency 'pry-debugger'
|
25
|
+
gem.add_development_dependency 'cane'
|
26
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'time'
|
6
|
+
require 'tmpdir'
|
7
|
+
|
8
|
+
describe 'event publishers' do
|
9
|
+
|
10
|
+
before do
|
11
|
+
@current_time = Time.now.utc.iso8601
|
12
|
+
end
|
13
|
+
|
14
|
+
def publish_event_type(type)
|
15
|
+
@publisher.publish(type, @current_time, :test_queue, {}, "FooBar", "a", "b")
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Resque::Plugins::Clues::StreamPublisher do
|
19
|
+
before do
|
20
|
+
@stream = StreamIO.new
|
21
|
+
@publisher = Resque::Plugins::Clues::StreamPublisher.new(@stream)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe Resque::Plugins::Clues::StreamPublisher do
|
26
|
+
def verify_output_for_event_type(type)
|
27
|
+
@stream.rewind
|
28
|
+
event = MultiJson.load(@stream.readlines[-1].chomp)
|
29
|
+
event["queue"].should == "test_queue"
|
30
|
+
event["metadata"].should == {}
|
31
|
+
event["timestamp"].should_not be_nil
|
32
|
+
event["event_type"].should == type.to_s
|
33
|
+
event["worker_class"].should == "FooBar"
|
34
|
+
event["args"].should == ["a", "b"]
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
@stream = StringIO.new
|
39
|
+
@publisher = Resque::Plugins::Clues::StreamPublisher.new(@stream)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should pass Resque lint detection" do
|
43
|
+
Resque::Plugin.lint(Resque::Plugins::Clues::StandardOutPublisher)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should send enqueued event to STDOUT" do
|
47
|
+
publish_event_type :enqueued
|
48
|
+
verify_output_for_event_type :enqueued
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should send dequeued event to STDOUT" do
|
52
|
+
publish_event_type :dequeued
|
53
|
+
verify_output_for_event_type :dequeued
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should send perform_started event to STDOUT" do
|
57
|
+
publish_event_type :perform_started
|
58
|
+
verify_output_for_event_type :perform_started
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should send perform_finished event to STDOUT" do
|
62
|
+
publish_event_type :perform_finished
|
63
|
+
verify_output_for_event_type :perform_finished
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should send failed event to STDOUT" do
|
67
|
+
publish_event_type :failed
|
68
|
+
verify_output_for_event_type :failed
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should send destroyed event to STDOUT" do
|
72
|
+
publish_event_type :destroyed
|
73
|
+
verify_output_for_event_type :destroyed
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
describe Resque::Plugins::Clues::LogPublisher do
|
79
|
+
def verify_event_written_to_log(event_type)
|
80
|
+
last_event["event_type"].should == event_type.to_s
|
81
|
+
last_event["timestamp"].should == @current_time.to_s
|
82
|
+
last_event["queue"].should == 'test_queue'
|
83
|
+
last_event["metadata"].should == {}
|
84
|
+
last_event["worker_class"].should == "FooBar"
|
85
|
+
last_event["args"].should == ["a", "b"]
|
86
|
+
end
|
87
|
+
|
88
|
+
def last_event
|
89
|
+
MultiJson.load(File.readlines(@log_path)[-1].chomp)
|
90
|
+
end
|
91
|
+
|
92
|
+
before do
|
93
|
+
@log_path = File.join(Dir.tmpdir, "test_log.log")
|
94
|
+
FileUtils.rm(@log_path) if File.exists?(@log_path)
|
95
|
+
@publisher = Resque::Plugins::Clues::LogPublisher.new(@log_path)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should pass Resque lint detection" do
|
99
|
+
Resque::Plugin.lint(Resque::Plugins::Clues::LogPublisher)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should write enqueued event to file" do
|
103
|
+
publish_event_type :enqueued
|
104
|
+
verify_event_written_to_log :enqueued
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should write dequeued event to file" do
|
108
|
+
publish_event_type :dequeued
|
109
|
+
verify_event_written_to_log :dequeued
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should write destroyed event to file" do
|
113
|
+
publish_event_type :destroyed
|
114
|
+
verify_event_written_to_log :destroyed
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should write perform_started event to file" do
|
118
|
+
publish_event_type :perform_started
|
119
|
+
verify_event_written_to_log :perform_started
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should write perform_finished event to file" do
|
123
|
+
publish_event_type :perform_finished
|
124
|
+
verify_event_written_to_log :perform_finished
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should write failed event to file" do
|
128
|
+
publish_event_type :failed
|
129
|
+
verify_event_written_to_log :failed
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should write 1 event per line in the file" do
|
133
|
+
publish_event_type :enqueued
|
134
|
+
publish_event_type :dequeued
|
135
|
+
File.readlines(@log_path)[1..-1].size.should == 2
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe Resque::Plugins::Clues::CompositePublisher do
|
140
|
+
before do
|
141
|
+
@publisher = Resque::Plugins::Clues::CompositePublisher.new
|
142
|
+
@publisher << Resque::Plugins::Clues::StandardOutPublisher.new
|
143
|
+
@publisher << Resque::Plugins::Clues::StandardOutPublisher.new
|
144
|
+
end
|
145
|
+
|
146
|
+
def verify_event_delegated_to_children(event_type)
|
147
|
+
@publisher.each do |child|
|
148
|
+
child.should_receive(:publish).with(
|
149
|
+
event_type, @current_time, :test_queue, {}, "FooBar", "a", "b")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should pass Resque lint detection" do
|
154
|
+
Resque::Plugin.lint(Resque::Plugins::Clues::CompositePublisher)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should delegate enqueued event to children" do
|
158
|
+
verify_event_delegated_to_children :enqueued
|
159
|
+
publish_event_type :enqueued
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should delegate dequeued event to children" do
|
163
|
+
verify_event_delegated_to_children :dequeued
|
164
|
+
publish_event_type :dequeued
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should delegate destroyed event to children" do
|
168
|
+
verify_event_delegated_to_children :destroyed
|
169
|
+
publish_event_type :destroyed
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should delegate perform_started event to children" do
|
173
|
+
verify_event_delegated_to_children :perform_started
|
174
|
+
publish_event_type :perform_started
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should delegate perform_finished event to children" do
|
178
|
+
verify_event_delegated_to_children :perform_finished
|
179
|
+
publish_event_type :perform_finished
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should delegate failed event to children" do
|
183
|
+
verify_event_delegated_to_children :failed
|
184
|
+
publish_event_type :failed
|
185
|
+
end
|
186
|
+
|
187
|
+
it "all children should be invoked when the first child throws an exception" do
|
188
|
+
@publisher[0].stub(:enqueued) {raise 'YOU SHALL NOT PASS'}
|
189
|
+
verify_event_delegated_to_children :enqueued
|
190
|
+
publish_event_type :enqueued
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
require 'timeout'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
# Setting $TESTING to true will cause Resque to not fork a child to perform the
|
7
|
+
# job, doing all the work within the test process and allowing us to test
|
8
|
+
# results for all aspects of the job performance.
|
9
|
+
$TESTING = true
|
10
|
+
|
11
|
+
class DummyWorker
|
12
|
+
@queue = :test_queue
|
13
|
+
|
14
|
+
def self.invoked?
|
15
|
+
@invoked
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.reset!
|
19
|
+
@invoked = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.perform(msg)
|
23
|
+
@invoked = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class FailingDummyWorker
|
28
|
+
@queue = :test_queue
|
29
|
+
|
30
|
+
def self.perform(msg)
|
31
|
+
raise msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'end-to-end integration' do
|
36
|
+
before(:each) do
|
37
|
+
DummyWorker.reset!
|
38
|
+
@stream = StringIO.new
|
39
|
+
@worker = Resque::Worker.new(:test_queue)
|
40
|
+
#@worker.very_verbose = true
|
41
|
+
Resque::Plugins::Clues.event_publisher = Resque::Plugins::Clues::StreamPublisher.new(@stream)
|
42
|
+
end
|
43
|
+
|
44
|
+
def stream_size
|
45
|
+
@stream.rewind
|
46
|
+
@stream.readlines.size
|
47
|
+
end
|
48
|
+
|
49
|
+
def enqueue_then_verify(klass, *args, &block)
|
50
|
+
Resque.enqueue(klass, *args)
|
51
|
+
work_and_verify(&block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def work
|
55
|
+
timeout(0.2) do
|
56
|
+
@worker.work(0.1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def work_and_verify(&block)
|
61
|
+
work
|
62
|
+
@stream.rewind
|
63
|
+
block.call(@stream.readlines.map{|line| MultiJson.decode(line)})
|
64
|
+
end
|
65
|
+
|
66
|
+
def verify_event(event, type, klass, *args)
|
67
|
+
event["worker_class"].should == klass.to_s
|
68
|
+
event["args"].should == args
|
69
|
+
event["event_type"].should == type.to_s
|
70
|
+
event["timestamp"].should_not be_nil
|
71
|
+
event["metadata"]["event_hash"].should_not be_nil
|
72
|
+
event["metadata"]["hostname"].should == `hostname`.strip
|
73
|
+
event["metadata"]["process"].should == $$
|
74
|
+
yield(event) if block_given?
|
75
|
+
end
|
76
|
+
|
77
|
+
context "for job that finishes normally" do
|
78
|
+
it "should publish enqueued, dequeued, perform_started and perform_finished events" do
|
79
|
+
enqueue_then_verify(DummyWorker, 'test') do |events|
|
80
|
+
events.size.should == 4
|
81
|
+
verify_event(events[0], :enqueued, DummyWorker, "test")
|
82
|
+
verify_event(events[1], :dequeued, DummyWorker, "test") do |event|
|
83
|
+
event["metadata"]["time_in_queue"].should_not be_nil
|
84
|
+
end
|
85
|
+
verify_event(events[2], :perform_started, DummyWorker, "test")
|
86
|
+
verify_event(events[3], :perform_finished, DummyWorker, "test") do |event|
|
87
|
+
event["metadata"]["time_to_perform"].should_not be_nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "for job that fails" do
|
94
|
+
it "should publish enqueued, dequeued, perform_started and failed events" do
|
95
|
+
enqueue_then_verify(FailingDummyWorker, 'test') do |events|
|
96
|
+
events.size.should == 4
|
97
|
+
verify_event(events[0], :enqueued, FailingDummyWorker, "test")
|
98
|
+
verify_event(events[1], :dequeued, FailingDummyWorker, "test") do |event|
|
99
|
+
event["metadata"]["time_in_queue"].should_not be_nil
|
100
|
+
end
|
101
|
+
verify_event(events[2], :perform_started, FailingDummyWorker, "test")
|
102
|
+
verify_event(events[3], :failed, FailingDummyWorker, "test") do |event|
|
103
|
+
event["metadata"]["time_to_perform"].should_not be_nil
|
104
|
+
event["metadata"]["exception"].should == RuntimeError.to_s
|
105
|
+
event["metadata"]["message"].should == 'test'
|
106
|
+
event["metadata"]["backtrace"].should_not be_nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "for job enqueued prior to use of resque-clues gem" do
|
113
|
+
def enqueue_unpatched(worker, *args)
|
114
|
+
unpatch_resque
|
115
|
+
begin
|
116
|
+
Resque.enqueue(worker, *args)
|
117
|
+
ensure
|
118
|
+
repatch_resque
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "job that performs normally" do
|
123
|
+
before do
|
124
|
+
enqueue_unpatched(DummyWorker, "test")
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should succeed without failures" do
|
128
|
+
work
|
129
|
+
DummyWorker.invoked?.should == true
|
130
|
+
Resque::Failure.all.should == nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "job failures" do
|
135
|
+
before do
|
136
|
+
enqueue_unpatched(FailingDummyWorker, "test")
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should report failure normally" do
|
140
|
+
work
|
141
|
+
Resque::Failure.count.should == 1
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context "where job enqueued with resque-clues gem but worker performing job is not" do
|
147
|
+
def unpatch_and_work
|
148
|
+
unpatch_resque
|
149
|
+
work
|
150
|
+
yield if block_given?
|
151
|
+
ensure
|
152
|
+
repatch_resque
|
153
|
+
end
|
154
|
+
|
155
|
+
context "for job that performs normally" do
|
156
|
+
before {Resque.enqueue DummyWorker, "test"}
|
157
|
+
|
158
|
+
it "should succeed without failures" do
|
159
|
+
unpatch_and_work {Resque::Failure.all.should == nil}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "for job failures" do
|
164
|
+
before {Resque.enqueue FailingDummyWorker, "test"}
|
165
|
+
|
166
|
+
it "should report failure normally" do
|
167
|
+
unpatch_and_work {Resque::Failure.count.should == 1}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context "Resque internals assumptions" do
|
173
|
+
describe "Resque#push" do
|
174
|
+
it "should receive item with class and args as symbols" do
|
175
|
+
received_item = nil
|
176
|
+
Resque.stub(:push) {|queue, item| received_item = item}
|
177
|
+
Resque.enqueue(DummyWorker, 'test')
|
178
|
+
received_item[:class].should == "DummyWorker"
|
179
|
+
received_item[:args].should == ['test']
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|