tom_queue 0.0.1.dev
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.
- data/lib/tom_queue.rb +56 -0
- data/lib/tom_queue/deferred_work_manager.rb +233 -0
- data/lib/tom_queue/deferred_work_set.rb +165 -0
- data/lib/tom_queue/delayed_job.rb +33 -0
- data/lib/tom_queue/delayed_job/external_messages.rb +56 -0
- data/lib/tom_queue/delayed_job/job.rb +365 -0
- data/lib/tom_queue/external_consumer.rb +136 -0
- data/lib/tom_queue/logging_helper.rb +19 -0
- data/lib/tom_queue/queue_manager.rb +264 -0
- data/lib/tom_queue/sorted_array.rb +69 -0
- data/lib/tom_queue/work.rb +62 -0
- data/spec/database.yml +14 -0
- data/spec/helper.rb +75 -0
- data/spec/tom_queue/deferred_work/deferred_work_manager_integration_spec.rb +186 -0
- data/spec/tom_queue/deferred_work/deferred_work_manager_spec.rb +134 -0
- data/spec/tom_queue/deferred_work/deferred_work_set_spec.rb +134 -0
- data/spec/tom_queue/delayed_job/delayed_job_integration_spec.rb +155 -0
- data/spec/tom_queue/delayed_job/delayed_job_spec.rb +818 -0
- data/spec/tom_queue/external_consumer_integration_spec.rb +225 -0
- data/spec/tom_queue/helper.rb +91 -0
- data/spec/tom_queue/logging_helper_spec.rb +152 -0
- data/spec/tom_queue/queue_manager_spec.rb +218 -0
- data/spec/tom_queue/sorted_array_spec.rb +160 -0
- data/spec/tom_queue/tom_queue_integration_spec.rb +296 -0
- data/spec/tom_queue/tom_queue_spec.rb +30 -0
- data/spec/tom_queue/work_spec.rb +35 -0
- data/tom_queue.gemspec +21 -0
- metadata +137 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
module TomQueue
|
2
|
+
|
3
|
+
# Internal: This represents a single payload of "work" to
|
4
|
+
# be completed by the application. You shouldn't create it
|
5
|
+
# directly, but instances will be returned by the
|
6
|
+
# QueueManager as work is de-queued.
|
7
|
+
#
|
8
|
+
class Work
|
9
|
+
|
10
|
+
# Public: The payload of the work.
|
11
|
+
#
|
12
|
+
# This is the serialized string passed to QueueManager#publish
|
13
|
+
# and it's up to the application to work out what to do with it!
|
14
|
+
attr_reader :payload
|
15
|
+
|
16
|
+
# Internal: The set of headers associated with the AMQP message
|
17
|
+
#
|
18
|
+
# NOTE: Use of these headers directly is discouraged, as their structure
|
19
|
+
# is an implementation detail of this library. See Public accessors
|
20
|
+
# below before grabbing data from here.
|
21
|
+
#
|
22
|
+
# This is a has of string values
|
23
|
+
attr_reader :headers
|
24
|
+
|
25
|
+
# Internal: The AMQP response returned when the work was delivered.
|
26
|
+
#
|
27
|
+
# NOTE: Use of data directly from this response is discouraged, for
|
28
|
+
# the same reason as #headers. It's an implementation detail...
|
29
|
+
#
|
30
|
+
# Returns an AMQ::Protocol::Basic::GetOk instance
|
31
|
+
attr_reader :response
|
32
|
+
|
33
|
+
# Internal: The queue manager to which this work belongs
|
34
|
+
attr_reader :manager
|
35
|
+
|
36
|
+
# Internal: Creates the work object, probably from an AMQP get
|
37
|
+
# response
|
38
|
+
#
|
39
|
+
# queue_manager - the QueueManager object that created this work
|
40
|
+
# amqp_response - this is the AMQP response object, i.e. the first
|
41
|
+
# returned object from @queue.pop
|
42
|
+
# header - this is a hash of headers attached to the message
|
43
|
+
# payload - the raw payload of the message
|
44
|
+
#
|
45
|
+
def initialize(queue_manager, amqp_response, headers, payload)
|
46
|
+
@manager = queue_manager
|
47
|
+
@response = amqp_response
|
48
|
+
@headers = headers
|
49
|
+
@payload = payload
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Ack this message, meaning the broker won't attempt to re-deliver
|
53
|
+
# the message.
|
54
|
+
#
|
55
|
+
# Returns self, so you chain this `pop.ack!.payload` for example
|
56
|
+
def ack!
|
57
|
+
@manager.ack(self)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/spec/database.yml
ADDED
data/spec/helper.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
4
|
+
SimpleCov::Formatter::HTMLFormatter
|
5
|
+
]
|
6
|
+
SimpleCov.start
|
7
|
+
|
8
|
+
require 'logger'
|
9
|
+
require 'rspec'
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'protected_attributes'
|
13
|
+
rescue LoadError
|
14
|
+
end
|
15
|
+
require 'delayed_job_active_record'
|
16
|
+
require 'delayed/backend/shared_spec'
|
17
|
+
|
18
|
+
Delayed::Worker.logger = Logger.new('/tmp/dj.log')
|
19
|
+
ENV['RAILS_ENV'] = 'test'
|
20
|
+
|
21
|
+
|
22
|
+
db_adapter, gemfile = ENV["ADAPTER"], ENV["BUNDLE_GEMFILE"]
|
23
|
+
db_adapter ||= gemfile && gemfile[%r(gemfiles/(.*?)/)] && $1
|
24
|
+
db_adapter ||= 'mysql'
|
25
|
+
|
26
|
+
begin
|
27
|
+
config = YAML.load(File.read('spec/database.yml'))
|
28
|
+
ActiveRecord::Base.establish_connection config[db_adapter]
|
29
|
+
ActiveRecord::Base.logger = Delayed::Worker.logger
|
30
|
+
ActiveRecord::Migration.verbose = false
|
31
|
+
|
32
|
+
ActiveRecord::Schema.define do
|
33
|
+
create_table :delayed_jobs, :force => true do |table|
|
34
|
+
table.integer :priority, :default => 0
|
35
|
+
table.integer :attempts, :default => 0
|
36
|
+
table.text :handler
|
37
|
+
table.text :last_error
|
38
|
+
table.datetime :run_at
|
39
|
+
table.datetime :locked_at
|
40
|
+
table.datetime :failed_at
|
41
|
+
table.string :locked_by
|
42
|
+
table.string :queue
|
43
|
+
table.timestamps
|
44
|
+
end
|
45
|
+
|
46
|
+
add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
|
47
|
+
|
48
|
+
create_table :stories, :primary_key => :story_id, :force => true do |table|
|
49
|
+
table.string :text
|
50
|
+
table.boolean :scoped, :default => true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue Mysql2::Error
|
54
|
+
if db_adapter == 'mysql'
|
55
|
+
$stderr.puts "\033[1;31mException when connecting to MySQL, is it running?\033[0m\n\n"
|
56
|
+
end
|
57
|
+
raise
|
58
|
+
end
|
59
|
+
|
60
|
+
# Purely useful for test cases...
|
61
|
+
class Story < ActiveRecord::Base
|
62
|
+
if ::ActiveRecord::VERSION::MAJOR < 4 && ActiveRecord::VERSION::MINOR < 2
|
63
|
+
set_primary_key :story_id
|
64
|
+
else
|
65
|
+
self.primary_key = :story_id
|
66
|
+
end
|
67
|
+
def tell; text; end
|
68
|
+
def whatever(n, _); tell*n; end
|
69
|
+
default_scope { where(:scoped => true) }
|
70
|
+
|
71
|
+
handle_asynchronously :whatever
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add this directory so the ActiveSupport autoloading works
|
75
|
+
ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'tom_queue/helper'
|
2
|
+
|
3
|
+
## "Integration" For lack of a better word, trying to simulate various
|
4
|
+
# failures, such as the deferred thread shitting itself
|
5
|
+
#
|
6
|
+
|
7
|
+
describe "DeferredWorkManager integration scenarios" do
|
8
|
+
let(:manager) { TomQueue::DeferredWorkManager.instance("test-#{Time.now.to_f}") }
|
9
|
+
let(:consumer) { TomQueue::QueueManager.new(manager.prefix) }
|
10
|
+
|
11
|
+
# Allow us to manipulate the deferred set object to induce a crash
|
12
|
+
before do
|
13
|
+
# Look away now...
|
14
|
+
class TomQueue::DeferredWorkManager
|
15
|
+
attr_reader :deferred_set
|
16
|
+
end
|
17
|
+
# ... and welcome back.
|
18
|
+
end
|
19
|
+
|
20
|
+
# Push a single piece of work through to make sure the
|
21
|
+
# deferred thread is running OK
|
22
|
+
before do
|
23
|
+
# This will start the deferred manager, and should block
|
24
|
+
# until the thread is actually functional
|
25
|
+
consumer.publish("work1", :run_at => Time.now + 0.1)
|
26
|
+
consumer.pop.ack!.payload.should == "work1"
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "background thread behaviour" do
|
30
|
+
before do
|
31
|
+
consumer.publish("work2", :run_at => Time.now + 0.5)
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "when the thread is shutdown" do
|
35
|
+
|
36
|
+
# This will shut the thread down with a single un-acked deferred messsage
|
37
|
+
before { manager.ensure_stopped }
|
38
|
+
|
39
|
+
it "should restore un-acked messages when the thread is shutdown" do
|
40
|
+
# we would expect the message to be on the queue again, so this
|
41
|
+
# should "just work"
|
42
|
+
consumer.pop.ack!.payload.should == "work2"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should nullify the thread" do
|
46
|
+
manager.thread.should be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should nullify the deferred_set" do
|
50
|
+
manager.deferred_set.should be_nil
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "if (heaven forbid) the deferred thread were to crash" do
|
56
|
+
|
57
|
+
before do
|
58
|
+
TomQueue.exception_reporter = nil
|
59
|
+
# Crash the deferred thread with a YAK stampede
|
60
|
+
manager.deferred_set.should_receive(:pop).and_raise(RuntimeError, "Yaks Everywhere!")
|
61
|
+
end
|
62
|
+
|
63
|
+
let(:crash!) do
|
64
|
+
thread = manager.thread
|
65
|
+
|
66
|
+
# This will triggert the thread run-loop to spin once and, hopefully, crash!
|
67
|
+
manager.deferred_set.interrupt
|
68
|
+
|
69
|
+
# wait for the inevitable - this will not induce a crash, just wait for the thread
|
70
|
+
# to die.
|
71
|
+
manager.thread.join
|
72
|
+
|
73
|
+
# Sanity check
|
74
|
+
thread.should_not be_alive
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should start a new deferred thread on the next pop" do
|
78
|
+
crashed_thread = manager.thread
|
79
|
+
|
80
|
+
crash!
|
81
|
+
|
82
|
+
# We don't actually care if the pop works, just that the thread gets started
|
83
|
+
Timeout.timeout(0.1) { consumer.pop.ack! } rescue nil
|
84
|
+
|
85
|
+
manager.thread.should_not == crashed_thread
|
86
|
+
manager.thread.should be_alive
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should nullify the thread" do
|
90
|
+
crash!
|
91
|
+
manager.thread.should be_nil
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should nullify the deferred set" do
|
95
|
+
crash!
|
96
|
+
manager.deferred_set.should be_nil
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should revert un-acked messages back to the broker" do
|
100
|
+
crash!
|
101
|
+
consumer.pop.ack!.payload.should == "work2"
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should notify something if the deferred thread crashes" do
|
105
|
+
TomQueue.exception_reporter = double("ExceptionReporter", :notify => nil)
|
106
|
+
|
107
|
+
TomQueue.exception_reporter.should_receive(:notify) do |exception|
|
108
|
+
exception.should be_a(RuntimeError)
|
109
|
+
exception.message.should == "Yaks Everywhere!"
|
110
|
+
end
|
111
|
+
|
112
|
+
crash!
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "if the AMQP consumer thread crashes" do
|
119
|
+
|
120
|
+
# Tweak the deferred set to asplode when payload == "explosive"
|
121
|
+
before do
|
122
|
+
TomQueue.exception_reporter = nil
|
123
|
+
require 'tom_queue/deferred_work_set'
|
124
|
+
|
125
|
+
# Look away...
|
126
|
+
class TomQueue::DeferredWorkSet
|
127
|
+
|
128
|
+
unless method_defined?(:orig_schedule)
|
129
|
+
def new_schedule(run_at, message)
|
130
|
+
raise RuntimeError, "ENOHAIR" if message.last == "explosive"
|
131
|
+
orig_schedule(run_at, message)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
alias_method :orig_schedule, :schedule
|
135
|
+
alias_method :schedule, :new_schedule
|
136
|
+
end
|
137
|
+
# ... welcome back
|
138
|
+
end
|
139
|
+
|
140
|
+
after do
|
141
|
+
class TomQueue::DeferredWorkSet
|
142
|
+
if method_defined?(:orig_schedule)
|
143
|
+
undef_method :schedule
|
144
|
+
alias_method :schedule, :orig_schedule
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
before do
|
150
|
+
manager.ensure_running
|
151
|
+
end
|
152
|
+
|
153
|
+
let(:crash!) do
|
154
|
+
consumer.publish("explosive", :run_at => Time.now + 0.2)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should notify the exception_reporter" do
|
158
|
+
TomQueue.exception_reporter = double("Reporter")
|
159
|
+
TomQueue.exception_reporter.should_receive(:notify) do |exception|
|
160
|
+
exception.should be_a(RuntimeError)
|
161
|
+
exception.message.should == "ENOHAIR"
|
162
|
+
end
|
163
|
+
|
164
|
+
crash!
|
165
|
+
TomQueue::DeferredWorkManager.instance(consumer.prefix).ensure_stopped
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should work around the broken messages" do
|
169
|
+
consumer.publish("foo", :run_at => Time.now + 0.1)
|
170
|
+
crash!
|
171
|
+
consumer.publish("bar", :run_at => Time.now + 0.1)
|
172
|
+
|
173
|
+
consumer.pop.ack!.payload.should == "foo"
|
174
|
+
consumer.pop.ack!.payload.should == "bar"
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should re-queue the message once" do
|
178
|
+
TomQueue.exception_reporter = double("Reporter")
|
179
|
+
TomQueue.exception_reporter.should_receive(:notify).twice
|
180
|
+
crash!
|
181
|
+
consumer.publish("bar", :run_at => Time.now + 0.1)
|
182
|
+
consumer.pop.ack!
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'tom_queue/helper'
|
2
|
+
|
3
|
+
describe TomQueue::DeferredWorkManager do
|
4
|
+
|
5
|
+
|
6
|
+
describe "thread control" do
|
7
|
+
let(:manager) { TomQueue::DeferredWorkManager.instance("test-#{Time.now.to_f}") }
|
8
|
+
|
9
|
+
it "should expose the thread via a #thread accessor" do
|
10
|
+
manager.thread.should be_nil
|
11
|
+
manager.ensure_running
|
12
|
+
manager.thread.should be_a(Thread)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "ensure_running" do
|
16
|
+
it "should start the thread if it's not running" do
|
17
|
+
manager.ensure_running
|
18
|
+
manager.thread.should be_alive
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should do nothing if the thread is already running" do
|
22
|
+
manager.ensure_running
|
23
|
+
first_thread = manager.thread
|
24
|
+
manager.ensure_running
|
25
|
+
manager.thread.should == first_thread
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should re-start a dead thread" do
|
29
|
+
manager.ensure_running
|
30
|
+
manager.thread.kill
|
31
|
+
manager.thread.join
|
32
|
+
manager.ensure_running
|
33
|
+
manager.thread.should be_alive
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "ensure_stopped" do
|
38
|
+
it "should block until the thread has stopped" do
|
39
|
+
manager.ensure_running
|
40
|
+
thread = manager.thread
|
41
|
+
manager.ensure_stopped
|
42
|
+
thread.should_not be_alive
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set the thread to nil" do
|
46
|
+
manager.ensure_running
|
47
|
+
manager.ensure_stopped
|
48
|
+
manager.thread.should be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should do nothing if the thread is already stopped" do
|
52
|
+
manager.ensure_running
|
53
|
+
manager.ensure_stopped
|
54
|
+
manager.ensure_stopped
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "DeferredWorkManager.instance - singleton accessor" do
|
60
|
+
|
61
|
+
it "should return a DelayedWorkManager instance" do
|
62
|
+
TomQueue::DeferredWorkManager.instance('some.prefix').should be_a(TomQueue::DeferredWorkManager)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should use the default prefix if none is provided" do
|
66
|
+
TomQueue.default_prefix = 'default.prefix'
|
67
|
+
TomQueue::DeferredWorkManager.instance.prefix.should == 'default.prefix'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should raise an exception if no prefix is provided and default isn't set" do
|
71
|
+
TomQueue.default_prefix = nil
|
72
|
+
lambda {
|
73
|
+
TomQueue::DeferredWorkManager.instance
|
74
|
+
}.should raise_exception(ArgumentError, 'prefix is required')
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should return the same object for the same prefix" do
|
78
|
+
TomQueue::DeferredWorkManager.instance('some.prefix').should == TomQueue::DeferredWorkManager.instance('some.prefix')
|
79
|
+
TomQueue::DeferredWorkManager.instance('another.prefix').should == TomQueue::DeferredWorkManager.instance('another.prefix')
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should return a different object for different prefixes" do
|
83
|
+
TomQueue::DeferredWorkManager.instance('some.prefix').should_not == TomQueue::DeferredWorkManager.instance('another.prefix')
|
84
|
+
end
|
85
|
+
it "should set the prefix for the instance created" do
|
86
|
+
TomQueue::DeferredWorkManager.instance('some.prefix').prefix.should == 'some.prefix'
|
87
|
+
TomQueue::DeferredWorkManager.instance('another.prefix').prefix.should == 'another.prefix'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "DeferredWorkManager.instances - running instance collection" do
|
92
|
+
|
93
|
+
it "should return an empty hash if no instances have been created" do
|
94
|
+
TomQueue::DeferredWorkManager.instances.should == {}
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should return a hash of created instances, keyed on the prefix" do
|
98
|
+
some_prefix = TomQueue::DeferredWorkManager.instance('some.prefix')
|
99
|
+
another_prefix = TomQueue::DeferredWorkManager.instance('another.prefix')
|
100
|
+
|
101
|
+
TomQueue::DeferredWorkManager.instances.should == {
|
102
|
+
"some.prefix" => some_prefix,
|
103
|
+
"another.prefix" => another_prefix
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should dupe the hash before returning" do
|
108
|
+
value = TomQueue::DeferredWorkManager.instances
|
109
|
+
TomQueue::DeferredWorkManager.instance('some.prefix')
|
110
|
+
value.should == {}
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should freeze the hash before returning" do
|
114
|
+
lambda {
|
115
|
+
TomQueue::DeferredWorkManager.instances['foo'] = 'bar'
|
116
|
+
}.should raise_exception(/frozen/)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe "for testing - DeferredWorkManager.reset! method" do
|
121
|
+
|
122
|
+
it "should return nil" do
|
123
|
+
TomQueue::DeferredWorkManager.reset!.should be_nil
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should cleared the singleton instances" do
|
127
|
+
first_singleton = TomQueue::DeferredWorkManager.instance('prefix')
|
128
|
+
TomQueue::DeferredWorkManager.reset!
|
129
|
+
TomQueue::DeferredWorkManager.instance('prefix').should_not == first_singleton
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
end
|