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.
@@ -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
@@ -0,0 +1,14 @@
1
+ mysql:
2
+ adapter: mysql2
3
+ database: delayed_job_test
4
+ username: root
5
+ encoding: utf8
6
+
7
+ postgresql:
8
+ adapter: postgresql
9
+ database: delayed_job_test
10
+ username: postgres
11
+
12
+ sqlite3:
13
+ adapter: sqlite3
14
+ database: "/tmp/tomqueue.sqlite3"
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