boss_queue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'aws-sdk'
4
+
5
+ group :development do
6
+ gem 'rspec'
7
+ gem 'bundler'
8
+ gem 'jeweler', :git => 'https://github.com/technicalpickles/jeweler.git', :branch => :master
9
+ gem 'pry'
10
+ gem 'pry-nav'
11
+ gem 'pry-stack_explorer'
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,92 @@
1
+ GIT
2
+ remote: https://github.com/technicalpickles/jeweler.git
3
+ revision: f7e0a55a207d83f56637dd8fbabf26a803410faf
4
+ branch: master
5
+ specs:
6
+ jeweler (1.8.7)
7
+ builder
8
+ bundler (~> 1.0)
9
+ git (>= 1.2.5)
10
+ github_api (= 0.10.1)
11
+ highline (>= 1.6.15)
12
+ nokogiri (= 1.5.10)
13
+ rake
14
+ rdoc
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ addressable (2.3.5)
20
+ aws-sdk (1.19.0)
21
+ json (~> 1.4)
22
+ nokogiri (>= 1.4.4, < 1.6.0)
23
+ uuidtools (~> 2.1)
24
+ binding_of_caller (0.7.2)
25
+ debug_inspector (>= 0.0.1)
26
+ builder (3.2.2)
27
+ coderay (1.0.9)
28
+ debug_inspector (0.0.2)
29
+ diff-lcs (1.2.4)
30
+ faraday (0.8.8)
31
+ multipart-post (~> 1.2.0)
32
+ git (1.2.6)
33
+ github_api (0.10.1)
34
+ addressable
35
+ faraday (~> 0.8.1)
36
+ hashie (>= 1.2)
37
+ multi_json (~> 1.4)
38
+ nokogiri (~> 1.5.2)
39
+ oauth2
40
+ hashie (2.0.5)
41
+ highline (1.6.19)
42
+ httpauth (0.2.0)
43
+ json (1.8.0)
44
+ jwt (0.1.8)
45
+ multi_json (>= 1.5)
46
+ method_source (0.8.2)
47
+ multi_json (1.8.0)
48
+ multi_xml (0.5.5)
49
+ multipart-post (1.2.0)
50
+ nokogiri (1.5.10)
51
+ oauth2 (0.9.2)
52
+ faraday (~> 0.8)
53
+ httpauth (~> 0.2)
54
+ jwt (~> 0.1.4)
55
+ multi_json (~> 1.0)
56
+ multi_xml (~> 0.5)
57
+ rack (~> 1.2)
58
+ pry (0.9.12.2)
59
+ coderay (~> 1.0.5)
60
+ method_source (~> 0.8)
61
+ slop (~> 3.4)
62
+ pry-nav (0.2.3)
63
+ pry (~> 0.9.10)
64
+ pry-stack_explorer (0.4.9.1)
65
+ binding_of_caller (>= 0.7)
66
+ pry (>= 0.9.11)
67
+ rack (1.5.2)
68
+ rake (10.1.0)
69
+ rdoc (4.0.1)
70
+ json (~> 1.4)
71
+ rspec (2.14.1)
72
+ rspec-core (~> 2.14.0)
73
+ rspec-expectations (~> 2.14.0)
74
+ rspec-mocks (~> 2.14.0)
75
+ rspec-core (2.14.5)
76
+ rspec-expectations (2.14.3)
77
+ diff-lcs (>= 1.1.3, < 2.0)
78
+ rspec-mocks (2.14.3)
79
+ slop (3.4.6)
80
+ uuidtools (2.1.4)
81
+
82
+ PLATFORMS
83
+ ruby
84
+
85
+ DEPENDENCIES
86
+ aws-sdk
87
+ bundler
88
+ jeweler!
89
+ pry
90
+ pry-nav
91
+ pry-stack_explorer
92
+ rspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Populr.me
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ boss_queue
2
+ ==========
3
+
4
+ A fault tolerant job queue built around Amazon SQS &amp; DynamoDB
5
+
6
+
7
+ Setup
8
+ ============
9
+
10
+ In your Gemfile:
11
+
12
+ gem 'boss_queue'
13
+
14
+
15
+ boss_queue uses an Amazon SQS queue and a Amazon DynamoDB table for each environment (production, staging, test). To set these up in Rails do:
16
+
17
+ $ rails c
18
+
19
+ AWS.config(:access_key_id => <access_key_id>,
20
+ :secret_access_key => <secret_access_key>)
21
+
22
+ BossQueue.environment = 'development'
23
+ BossQueue.create_table
24
+ BossQueue.create_queue
25
+
26
+ BossQueue.environment = 'staging'
27
+ BossQueue.create_table
28
+ BossQueue.create_queue
29
+
30
+ # BossQueue.create_table(read_capacity, write_capacity)
31
+ # One read capacity unit = two eventually consistent reads per second, for items up 4 KB in size.
32
+ # One write capacity unit = one write per second, for items up to 1 KB in size.
33
+
34
+ BossQueue.environment = 'production'
35
+ BossQueue.create_table(50, 10)
36
+ BossQueue.create_queue
37
+
38
+
39
+ Alternatively, in each of the respective environments, do:
40
+
41
+ $ rails c
42
+
43
+ AWS.config(:access_key_id => <access_key_id>,
44
+ :secret_access_key => <secret_access_key>)
45
+
46
+ # environment does not need to be set because it is taken from Rails.env
47
+ BossQueue.create_table
48
+ BossQueue.create_queue
49
+
50
+
51
+ Or these could be put into a migration.
52
+
53
+
54
+ Usage
55
+ =====
56
+
57
+ myobject = MyClass.new
58
+ BossQueue.failure_action = 'none' # default is 'retry' which retries up to four times
59
+
60
+ # can enqueue instance methods (assumes that objects have an id and a #find(id) method)
61
+ BossQueue.enqueue(myobject, :method_to_execute, arg1, arg2)
62
+ # enqueue with a delay of up to 900 seconds (15 minutes)
63
+ BossQueue.enqueue_with_delay(60, myobject, :method_to_execute, arg1, arg2, arg3)
64
+
65
+ # can enqueue class methods
66
+ BossQueue.enqueue(MyClass, :method_to_execute)
67
+ BossQueue.enqueue_with_delay(60, MyClass, :method_to_execute, arg1, arg2)
68
+
69
+ BossQueue.work
70
+
71
+ # failures are left in the DynamoDB table with the failed boolean set to true
72
+
73
+ BossQueue does not at present have a daemon component such as Sidekiq or Resque.
74
+
75
+
76
+ Future Work
77
+ ===========
78
+
79
+ Create some mechanism for viewing failed jobs (and perhaps queued jobs...they are all in the same table)
80
+
81
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "boss_queue"
18
+ gem.homepage = "https://github.com/populr/boss_queue"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{A fault tolerant job queue built around Amazon SQS & DynamoDB}
21
+ gem.description = %Q{A fault tolerant job queue built around Amazon SQS & DynamoDB}
22
+ gem.email = "daniel@populr.me"
23
+ gem.authors = ["Daniel Nelson"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rdoc/task'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "boss_queue #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+
3
+ class BossQueue
4
+ @@environment
5
+
6
+ def self.environment=(env)
7
+ @@environment = env
8
+ end
9
+
10
+ @@failure_action
11
+
12
+ def self.failure_action
13
+ @@failure_action ||= 'retry'
14
+ end
15
+
16
+ def self.failure_action=(env)
17
+ @@failure_action = env
18
+ end
19
+
20
+
21
+ def self.table_name
22
+ "#{self.queue_prefix}boss_queue_jobs"
23
+ end
24
+
25
+ def self.queue_name
26
+ "#{self.queue_prefix}boss_queue"
27
+ end
28
+
29
+
30
+ def self.create_table(read_capacity=1, write_capacity=1, options={})
31
+ create_opts = {}
32
+ create_opts[:hash_key] = { hash_key => :string }
33
+ create_opts[:range_key] = { :kind => :string }
34
+
35
+ AWS::DynamoDB.new.tables.create(self.table_name, read_capacity, write_capacity, create_opts)
36
+ end
37
+
38
+ def self.create_queue
39
+ AWS::SQS::QueueCollection.new.create(self.queue_name, :default_visibility_timeout => 5 * 60)
40
+ end
41
+
42
+ def self.work
43
+ queue = AWS::SQS.new.queues[self.queue_name]
44
+ queue.receive_message do |job_id|
45
+ # When a block is given, each message is yielded to the block and then deleted as long as the block exits normally - http://docs.aws.amazon.com/AWSRubySDK/latest/frames.html
46
+ job = BossQueue::Job.shard(table_name).find_by_id(job_id.body)
47
+ job.queue_name = self.queue_name
48
+ job.work
49
+ end
50
+ end
51
+
52
+ def self.enqueue(class_or_instance, method_name, *args)
53
+ job = self.create_job(class_or_instance, method_name, *args)
54
+ job.enqueue
55
+ end
56
+
57
+ def self.enqueue_with_delay(delay, class_or_instance, method_name, *args)
58
+ job = self.create_job(class_or_instance, method_name, *args)
59
+ job.enqueue_with_delay(delay)
60
+ end
61
+
62
+ def self.create_job(class_or_instance, method_name, *args) # :nodoc:
63
+ job = BossQueue::Job.shard(table_name).new
64
+ if class_or_instance.is_a?(Class)
65
+ class_name = class_or_instance.to_s
66
+ instance_id = nil
67
+ job.kind = "#{class_name}@#{method_name}"
68
+ else
69
+ class_name = class_or_instance.class.to_s
70
+ instance_id = class_or_instance.id
71
+ job.kind = "#{class_name}##{method_name}"
72
+ end
73
+ job.queue_name = self.queue_name
74
+ job.failure_action = self.failure_action
75
+ job.model_class_name = class_name
76
+ job.model_id = instance_id unless instance_id.nil?
77
+ job.job_method = method_name.to_s
78
+ job.job_arguments = JSON.generate(args)
79
+ job.save!
80
+ job
81
+ end
82
+
83
+ def self.environment # :nodoc:
84
+ @@environment ||= if Module.const_get('Rails')
85
+ Rails.env
86
+ elsif Module.const_get('Rack')
87
+ Rack.env
88
+ else
89
+ raise 'BossQueue requires an environment'
90
+ end
91
+ end
92
+
93
+ def self.queue_prefix # :nodoc:
94
+ case self.environment
95
+ when 'production'
96
+ ''
97
+ when 'development'
98
+ 'dev_'
99
+ else
100
+ environment + '_'
101
+ end
102
+ end
103
+
104
+ end
@@ -0,0 +1,100 @@
1
+ require 'json'
2
+
3
+ class BossQueue
4
+
5
+ class Job < AWS::Record::HashModel
6
+ attr_accessor :queue_name
7
+
8
+ string_attr :kind # an index based model_class_name, job_method
9
+ boolean_attr :failed
10
+
11
+ string_attr :model_class_name
12
+ string_attr :model_id
13
+ string_attr :job_method
14
+ string_attr :job_arguments
15
+
16
+ integer_attr :failed_attempts
17
+ string_attr :failure_action
18
+ string_attr :exception_name
19
+ string_attr :exception_message
20
+ string_attr :stacktrace
21
+
22
+ timestamps
23
+
24
+ def enqueue
25
+ queue = AWS::SQS.new.queues[queue_name]
26
+ queue.send_message(id.to_s)
27
+ end
28
+
29
+ def enqueue_with_delay(delay)
30
+ queue = AWS::SQS.new.queues[queue_name]
31
+ queue.send_message(id.to_s, :delay_seconds => [900, [0, delay].max].min)
32
+ end
33
+
34
+ def work
35
+ begin
36
+ klass = constantize(model_class_name)
37
+ if model_id
38
+ target = klass.find(model_id)
39
+ else
40
+ target = klass
41
+ end
42
+ args = JSON.parse(job_arguments)
43
+ target.send(job_method, *args)
44
+ destroy
45
+ rescue StandardError => err
46
+ fail(err)
47
+ end
48
+ end
49
+
50
+ def fail(err)
51
+ self.failed_attempts ||= 0
52
+ self.failed_attempts += 1
53
+ self.exception_name = err.class.to_s
54
+ self.exception_message = err.message
55
+ self.stacktrace = err.backtrace[0, 7].join("\n")
56
+
57
+ if failure_action == 'retry' && retry_delay
58
+ enqueue_with_delay(retry_delay)
59
+ else
60
+ self.failed = true
61
+ end
62
+
63
+ self.save!
64
+ end
65
+
66
+ def retry_delay
67
+ return nil if failed_attempts.nil? || failed_attempts > 4
68
+ 60 * 2**(failed_attempts - 1)
69
+ end
70
+
71
+ # from ActiveSupport source: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize
72
+ def constantize(camel_cased_word) # :nodoc:
73
+ names = camel_cased_word.split('::')
74
+ names.shift if names.empty? || names.first.empty?
75
+
76
+ names.inject(Object) do |constant, name|
77
+ if constant == Object
78
+ constant.const_get(name)
79
+ else
80
+ candidate = constant.const_get(name)
81
+ next candidate if constant.const_defined?(name, false)
82
+ next candidate unless Object.const_defined?(name)
83
+
84
+ # Go down the ancestors to check it it's owned
85
+ # directly before we reach Object or the end of ancestors.
86
+ constant = constant.ancestors.inject do |const, ancestor|
87
+ break const if ancestor == Object
88
+ break ancestor if ancestor.const_defined?(name, false)
89
+ const
90
+ end
91
+
92
+ # owner is in Object, so raise
93
+ constant.const_get(name, false)
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
data/lib/boss_queue.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'boss_queue/boss_queue'
2
+ require 'boss_queue/job'
@@ -0,0 +1,244 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "BossQueue module" do
4
+
5
+ it "should respond to environment" do
6
+ BossQueue.should respond_to(:environment)
7
+ end
8
+
9
+ it "should respond to environment=" do
10
+ BossQueue.should respond_to(:environment=)
11
+ end
12
+
13
+ it "should respond to failure_action" do
14
+ BossQueue.should respond_to(:failure_action)
15
+ end
16
+
17
+ it "should respond to failure_action=" do
18
+ BossQueue.should respond_to(:failure_action=)
19
+ end
20
+
21
+ describe "#failure_action" do
22
+ it "should default to 'retry'" do
23
+ BossQueue.failure_action.should == 'retry'
24
+ end
25
+ end
26
+
27
+ describe "#table_name" do
28
+ before(:each) do
29
+ BossQueue.environment = nil
30
+ end
31
+
32
+ context "when @@environment is 'development'" do
33
+ it "should be 'dev_boss_queue_jobs'" do
34
+ BossQueue.environment = 'development'
35
+ BossQueue.table_name.should == 'dev_boss_queue_jobs'
36
+ end
37
+ end
38
+
39
+ context "when @@environment is 'production'" do
40
+ it "should be 'boss_queue_jobs'" do
41
+ BossQueue.environment = 'production'
42
+ BossQueue.table_name.should == 'boss_queue_jobs'
43
+ end
44
+ end
45
+
46
+ context "when @@environment is 'staging'" do
47
+ it "should be 'staging_boss_queue_jobs'" do
48
+ BossQueue.environment = 'staging'
49
+ BossQueue.table_name.should == 'staging_boss_queue_jobs'
50
+ end
51
+ end
52
+
53
+ context "when @@environment is 'staging'" do
54
+ it "should be 'staging_boss_queue_jobs'" do
55
+ BossQueue.environment = 'staging'
56
+ BossQueue.table_name.should == 'staging_boss_queue_jobs'
57
+ end
58
+ end
59
+
60
+ context "when @@environment is nil" do
61
+ it "should raise an exception" do
62
+ lambda {
63
+ BossQueue.table_name
64
+ }.should raise_error
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ describe "#queue_name" do
71
+ before(:each) do
72
+ BossQueue.environment = nil
73
+ end
74
+
75
+ context "when @@environment is 'development'" do
76
+ it "should be 'dev_boss_queue'" do
77
+ BossQueue.environment = 'development'
78
+ BossQueue.queue_name.should == 'dev_boss_queue'
79
+ end
80
+ end
81
+
82
+ context "when @@environment is 'production'" do
83
+ it "should be 'boss_queue'" do
84
+ BossQueue.environment = 'production'
85
+ BossQueue.queue_name.should == 'boss_queue'
86
+ end
87
+ end
88
+
89
+ context "when @@environment is 'staging'" do
90
+ it "should be 'staging_boss_queue'" do
91
+ BossQueue.environment = 'staging'
92
+ BossQueue.queue_name.should == 'staging_boss_queue'
93
+ end
94
+ end
95
+
96
+ context "when @@environment is 'staging'" do
97
+ it "should be 'staging_boss_queue'" do
98
+ BossQueue.environment = 'staging'
99
+ BossQueue.queue_name.should == 'staging_boss_queue'
100
+ end
101
+ end
102
+
103
+ context "when @@environment is nil" do
104
+ it "should raise an exception" do
105
+ lambda {
106
+ BossQueue.queue_name
107
+ }.should raise_error
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#enqueue" do
113
+ before(:each) do
114
+ @arguments = ['a', 'b', { 'c' => 2, 'd' => 1 }]
115
+ @argument_json = JSON.generate(@arguments)
116
+
117
+ class TestClass
118
+ def id
119
+ 'xyz'
120
+ end
121
+
122
+ def self.test_class_method
123
+ end
124
+
125
+ def test_instance_method
126
+ end
127
+ end
128
+ end
129
+
130
+ context "when a class" do
131
+ it "should initialize a new BossQueue::Job object, save and call enqueue on it" do
132
+ BossQueue.environment = 'test'
133
+ BossQueue.failure_action = 'retry'
134
+ BossQueue::Job.any_instance.should_receive(:kind=).with('TestClass@test_class_method')
135
+ BossQueue::Job.any_instance.should_receive(:queue_name=).with('test_boss_queue')
136
+ BossQueue::Job.any_instance.should_receive(:failure_action=).with('retry')
137
+ BossQueue::Job.any_instance.should_receive(:model_class_name=).with('TestClass')
138
+ BossQueue::Job.any_instance.should_not_receive(:model_id=)
139
+ BossQueue::Job.any_instance.should_receive(:job_method=).with('test_class_method')
140
+ BossQueue::Job.any_instance.should_receive(:job_arguments=).with(@argument_json)
141
+ BossQueue::Job.any_instance.should_receive(:save!)
142
+ BossQueue::Job.any_instance.should_receive(:enqueue)
143
+ BossQueue.enqueue(TestClass, :test_class_method, 'a', 'b', { 'c' => 2, 'd' => 1 })
144
+ end
145
+ end
146
+
147
+ context "when a class instance" do
148
+ it "should initialize a new BossQueue::Job object, save and call enqueue on it" do
149
+ BossQueue.environment = 'test'
150
+ BossQueue.failure_action = 'retry'
151
+ BossQueue::Job.any_instance.should_receive(:kind=).with('TestClass#test_instance_method')
152
+ BossQueue::Job.any_instance.should_receive(:queue_name=).with('test_boss_queue')
153
+ BossQueue::Job.any_instance.should_receive(:failure_action=).with('retry')
154
+ BossQueue::Job.any_instance.should_receive(:model_class_name=).with('TestClass')
155
+ BossQueue::Job.any_instance.should_receive(:model_id=).with('xyz')
156
+ BossQueue::Job.any_instance.should_receive(:job_method=).with('test_instance_method')
157
+ BossQueue::Job.any_instance.should_receive(:job_arguments=).with(@argument_json)
158
+ BossQueue::Job.any_instance.should_receive(:save!)
159
+ BossQueue::Job.any_instance.should_receive(:enqueue)
160
+ BossQueue.enqueue(TestClass.new, :test_instance_method, 'a', 'b', { 'c' => 2, 'd' => 1 })
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ describe "#enqueue_with_delay" do
167
+ before(:each) do
168
+ @arguments = ['a', 'b', { 'c' => 2, 'd' => 1 }]
169
+ @argument_json = JSON.generate(@arguments)
170
+ end
171
+
172
+ context "when a class" do
173
+ it "should initialize a new BossQueue::Job object, save and call enqueue on it" do
174
+ BossQueue.environment = 'test'
175
+ BossQueue.failure_action = 'retry'
176
+ BossQueue::Job.any_instance.should_receive(:kind=).with('TestClass@test_class_method')
177
+ BossQueue::Job.any_instance.should_receive(:queue_name=).with('test_boss_queue')
178
+ BossQueue::Job.any_instance.should_receive(:failure_action=).with('retry')
179
+ BossQueue::Job.any_instance.should_receive(:model_class_name=).with('TestClass')
180
+ BossQueue::Job.any_instance.should_not_receive(:model_id=)
181
+ BossQueue::Job.any_instance.should_receive(:job_method=).with('test_class_method')
182
+ BossQueue::Job.any_instance.should_receive(:job_arguments=).with(@argument_json)
183
+ BossQueue::Job.any_instance.should_receive(:save!)
184
+ BossQueue::Job.any_instance.should_receive(:enqueue_with_delay).with(60)
185
+ BossQueue.enqueue_with_delay(60, TestClass, :test_class_method, 'a', 'b', { 'c' => 2, 'd' => 1 })
186
+ end
187
+ end
188
+
189
+ context "when a class instance" do
190
+ it "should initialize a new BossQueue::Job object, save and call enqueue on it" do
191
+ BossQueue.environment = 'test'
192
+ BossQueue.failure_action = 'retry'
193
+ BossQueue::Job.any_instance.should_receive(:kind=).with('TestClass#test_instance_method')
194
+ BossQueue::Job.any_instance.should_receive(:queue_name=).with('test_boss_queue')
195
+ BossQueue::Job.any_instance.should_receive(:failure_action=).with('retry')
196
+ BossQueue::Job.any_instance.should_receive(:model_class_name=).with('TestClass')
197
+ BossQueue::Job.any_instance.should_receive(:model_id=).with('xyz')
198
+ BossQueue::Job.any_instance.should_receive(:job_method=).with('test_instance_method')
199
+ BossQueue::Job.any_instance.should_receive(:job_arguments=).with(@argument_json)
200
+ BossQueue::Job.any_instance.should_receive(:save!)
201
+ BossQueue::Job.any_instance.should_receive(:enqueue_with_delay).with(60)
202
+ BossQueue.enqueue_with_delay(60, TestClass.new, :test_instance_method, 'a', 'b', { 'c' => 2, 'd' => 1 })
203
+ end
204
+ end
205
+
206
+ end
207
+
208
+ describe "#work" do
209
+ before(:each) do
210
+ @queue = double('queue')
211
+ AWS::SQS.stub_chain(:new, :queues, :[]).and_return(@queue)
212
+
213
+ @sqs_message = double('message')
214
+ @sqs_message.stub(:body).and_return('ijk')
215
+ @queue.stub(:receive_message).and_yield(@sqs_message)
216
+
217
+ @job = double('job')
218
+ @job.stub(:work)
219
+ @job.stub(:queue_name=)
220
+ BossQueue::Job.stub_chain(:shard, :find_by_id).and_return(@job)
221
+ end
222
+
223
+ it "should dequeue from SQS" do
224
+ @queue.should_receive(:receive_message).and_yield(@sqs_message)
225
+ BossQueue.work
226
+ end
227
+
228
+ context "when something is dequeued from SQS" do
229
+ it "should use the dequeued id to retrieve a BossQueue::Job object" do
230
+ @queue.should_receive(:receive_message).and_yield(@sqs_message)
231
+ shard = double('shard')
232
+ BossQueue::Job.should_receive(:shard).with(BossQueue.table_name).and_return(shard)
233
+ shard.should_receive(:find_by_id).with('ijk').and_return(@job)
234
+ BossQueue.work
235
+ end
236
+
237
+ it "should call work on the BossQueue::Job object" do
238
+ @job.should_receive(:work)
239
+ BossQueue.work
240
+ end
241
+ end
242
+ end
243
+
244
+ end
data/spec/job_spec.rb ADDED
@@ -0,0 +1,358 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "BossQueue::Job" do
4
+
5
+ it "should respond to id" do
6
+ BossQueue::Job.new.should respond_to(:id)
7
+ end
8
+
9
+ it "should respond to id=" do
10
+ BossQueue::Job.new.should respond_to(:id=)
11
+ end
12
+
13
+
14
+ it "should respond to failed" do
15
+ BossQueue::Job.new.should respond_to(:failed)
16
+ end
17
+
18
+ it "should respond to failed=" do
19
+ BossQueue::Job.new.should respond_to(:failed=)
20
+ end
21
+
22
+ describe "#failed" do
23
+ it "should default to false" do
24
+ BossQueue::Job.new.failed.should be_false
25
+ end
26
+ end
27
+
28
+ it "should respond to queue_name" do
29
+ BossQueue::Job.new.should respond_to(:queue_name)
30
+ end
31
+
32
+ it "should respond to queue_name=" do
33
+ BossQueue::Job.new.should respond_to(:queue_name=)
34
+ end
35
+
36
+ it "should respond to failed_attempts" do
37
+ BossQueue::Job.new.should respond_to(:failed_attempts)
38
+ end
39
+
40
+ it "should respond to failed_attempts=" do
41
+ BossQueue::Job.new.should respond_to(:failed_attempts=)
42
+ end
43
+
44
+
45
+ it "should respond to failure_action" do
46
+ BossQueue::Job.new.should respond_to(:failure_action)
47
+ end
48
+
49
+ it "should respond to failure_action=" do
50
+ BossQueue::Job.new.should respond_to(:failure_action=)
51
+ end
52
+
53
+
54
+ it "should respond to exception_name" do
55
+ BossQueue::Job.new.should respond_to(:exception_name)
56
+ end
57
+
58
+ it "should respond to exception_name=" do
59
+ BossQueue::Job.new.should respond_to(:exception_name=)
60
+ end
61
+
62
+
63
+ it "should respond to exception_message" do
64
+ BossQueue::Job.new.should respond_to(:exception_message)
65
+ end
66
+
67
+ it "should respond to exception_message=" do
68
+ BossQueue::Job.new.should respond_to(:exception_message=)
69
+ end
70
+
71
+
72
+ it "should respond to stacktrace" do
73
+ BossQueue::Job.new.should respond_to(:stacktrace)
74
+ end
75
+
76
+ it "should respond to stacktrace=" do
77
+ BossQueue::Job.new.should respond_to(:stacktrace=)
78
+ end
79
+
80
+
81
+ it "should respond to model_class_name" do
82
+ BossQueue::Job.new.should respond_to(:model_class_name)
83
+ end
84
+
85
+ it "should respond to model_class_name=" do
86
+ BossQueue::Job.new.should respond_to(:model_class_name=)
87
+ end
88
+
89
+
90
+ it "should respond to model_id" do
91
+ BossQueue::Job.new.should respond_to(:model_id)
92
+ end
93
+
94
+ it "should respond to model_id=" do
95
+ BossQueue::Job.new.should respond_to(:model_id=)
96
+ end
97
+
98
+
99
+ it "should respond to job_method" do
100
+ BossQueue::Job.new.should respond_to(:job_method)
101
+ end
102
+
103
+ it "should respond to job_method=" do
104
+ BossQueue::Job.new.should respond_to(:job_method=)
105
+ end
106
+
107
+
108
+ it "should respond to job_arguments" do
109
+ BossQueue::Job.new.should respond_to(:job_arguments)
110
+ end
111
+
112
+ it "should respond to job_arguments=" do
113
+ BossQueue::Job.new.should respond_to(:job_arguments=)
114
+ end
115
+
116
+
117
+ describe "#work" do
118
+ before(:each) do
119
+ @job = BossQueue::Job.new
120
+ @job.stub(:destroy)
121
+ @job.model_class_name = 'TestClass'
122
+ @job.model_id = 'xyz'
123
+ @job.job_method = 'test_instance_method'
124
+ @arguments = ['a', 'b', { 'c' => 2, 'd' => 1 }]
125
+ @argument_json = JSON.generate(@arguments)
126
+ @job.job_arguments = @argument_json
127
+ @instance_to_work_on = double('instance_to_work_on')
128
+ @instance_to_work_on.stub(:test_instance_method)
129
+ TestClass.stub(:find).and_return(@instance_to_work_on)
130
+ end
131
+
132
+
133
+ context "when model_id is not nil" do
134
+ it "should use #find on the model class to instantiate an object to work on" do
135
+ TestClass.should_receive(:find).with('xyz').and_return(@instance_to_work_on)
136
+ @job.work
137
+ end
138
+
139
+ it "should pass the job arguments to the job method" do
140
+ @instance_to_work_on.should_receive(:test_instance_method).with('a', 'b', { 'c' => 2, 'd' => 1 })
141
+ @job.work
142
+ end
143
+ end
144
+
145
+ context "when model_id is nil" do
146
+ before(:each) do
147
+ @job = BossQueue::Job.new
148
+ @job.stub(:destroy)
149
+ @job.model_class_name = 'TestClass'
150
+ @job.job_method = 'test_class_method'
151
+ @arguments = ['a', 'b', { 'c' => 2, 'd' => 1 }]
152
+ @argument_json = JSON.generate(@arguments)
153
+ @job.job_arguments = @argument_json
154
+ end
155
+
156
+ it "should pass the job arguments to the job method on the class" do
157
+ TestClass.should_receive(:test_class_method).with('a', 'b', { 'c' => 2, 'd' => 1 })
158
+ @job.work
159
+ end
160
+ end
161
+
162
+ context "when the job method doesn't raise an exception" do
163
+ it "should call destroy" do
164
+ @job.should_receive(:destroy)
165
+ @job.work
166
+ end
167
+ end
168
+
169
+ context "when the job method raises an exception" do
170
+ before(:each) do
171
+ @instance_to_work_on.stub(:test_instance_method).and_raise(StandardError.new)
172
+ @job.stub(:fail)
173
+ end
174
+
175
+ it "should call fail" do
176
+ @job.should_receive(:fail)
177
+ @job.work
178
+ end
179
+
180
+ it "should not call destroy" do
181
+ @job.should_not_receive(:destroy)
182
+ @job.work
183
+ end
184
+
185
+ it "should not raise an exception" do
186
+ lambda {
187
+ @job.work
188
+ }.should_not raise_error
189
+ end
190
+ end
191
+
192
+ end
193
+
194
+ describe "#enqueue" do
195
+ it "should enqueue id into the SQS queue" do
196
+ queue = double('queue')
197
+ AWS::SQS.stub_chain(:new, :queues, :[]).and_return(queue)
198
+ queue.should_receive(:send_message).with('ijk')
199
+ job = BossQueue::Job.new
200
+ job.id = 'ijk'
201
+ job.enqueue
202
+ end
203
+ end
204
+
205
+ describe "#enqueue_with_delay" do
206
+ it "should enqueue id into the SQS queue with a delay" do
207
+ queue = double('queue')
208
+ AWS::SQS.stub_chain(:new, :queues, :[]).and_return(queue)
209
+ queue.should_receive(:send_message).with('ijk', :delay_seconds => 60)
210
+ job = BossQueue::Job.new
211
+ job.id = 'ijk'
212
+ job.enqueue_with_delay(60)
213
+ end
214
+
215
+ it "should limit the delay to 15 minutes" do
216
+ queue = double('queue')
217
+ AWS::SQS.stub_chain(:new, :queues, :[]).and_return(queue)
218
+ queue.should_receive(:send_message).with('ijk', :delay_seconds => 900)
219
+ job = BossQueue::Job.new
220
+ job.id = 'ijk'
221
+ job.enqueue_with_delay(10000)
222
+ end
223
+
224
+ it "should set a negative delay to 0" do
225
+ queue = double('queue')
226
+ AWS::SQS.stub_chain(:new, :queues, :[]).and_return(queue)
227
+ queue.should_receive(:send_message).with('ijk', :delay_seconds => 0)
228
+ job = BossQueue::Job.new
229
+ job.id = 'ijk'
230
+ job.enqueue_with_delay(-60)
231
+ end
232
+ end
233
+
234
+ describe "#retry_delay" do
235
+ before(:each) do
236
+ @job = BossQueue::Job.new
237
+ end
238
+
239
+ context "when failed_attempts is nil" do
240
+ it "should be nil" do
241
+ @job.retry_delay.should == nil
242
+ end
243
+ end
244
+
245
+ context "when failed_attempts is 1" do
246
+ it "should be 60" do
247
+ @job.failed_attempts = 1
248
+ @job.retry_delay.should == 60
249
+ end
250
+ end
251
+
252
+ context "when failed_attempts is 2" do
253
+ it "should be 120" do
254
+ @job.failed_attempts = 2
255
+ @job.retry_delay.should == 120
256
+ end
257
+ end
258
+
259
+ context "when failed_attempts is 3" do
260
+ it "should be 240" do
261
+ @job.failed_attempts = 3
262
+ @job.retry_delay.should == 240
263
+ end
264
+ end
265
+
266
+ context "when failed_attempts is 4" do
267
+ it "should be 480" do
268
+ @job.failed_attempts = 4
269
+ @job.retry_delay.should == 480
270
+ end
271
+ end
272
+
273
+ context "when failed_attempts greater than 4" do
274
+ it "should be nil ((60 + 120 + 240 + 480) == 900 (15 minutes), the maximum delay supported by SQS)" do
275
+ @job.failed_attempts = 5
276
+ @job.retry_delay.should be_nil
277
+ end
278
+ end
279
+ end
280
+
281
+ describe "#fail" do
282
+ before(:each) do
283
+ @job = BossQueue::Job.new
284
+ @job.stub(:retry_delay).and_return(nil)
285
+ @job.stub(:save!)
286
+ @job.stub(:enqueue_with_delay)
287
+ @err
288
+ begin
289
+ raise StandardError.new('hello world')
290
+ rescue StandardError => err
291
+ @err = err
292
+ end
293
+ end
294
+
295
+ context "when failed_attempts is a number" do
296
+ it "should increment failed_attempts" do
297
+ @job.failed_attempts = 1
298
+ @job.fail(@err)
299
+ @job.failed_attempts.should == 2
300
+ end
301
+ end
302
+
303
+ it "should store the exception, message, and the first 7 lines of the stacktrace in the BossQueue::Job object" do
304
+ @job.fail(@err)
305
+ @job.exception_name.should == @err.class.to_s
306
+ @job.exception_message.should == @err.message
307
+ @job.stacktrace.should == @err.backtrace[0, 7].join("\n")
308
+ end
309
+
310
+ it "should call save!" do
311
+ @job.should_receive(:save!)
312
+ @job.fail(@err)
313
+ end
314
+
315
+ context "when failure_action is 'retry'" do
316
+ before(:each) do
317
+ @job.failure_action = 'retry'
318
+ end
319
+
320
+ context "when retry_delay returns a number" do
321
+ it "should re-enqueue with that delay" do
322
+ @job.stub(:retry_delay).and_return(60)
323
+ @job.should_receive(:enqueue_with_delay).with(60)
324
+ @job.fail(@err)
325
+ end
326
+
327
+ context "when failed_attempts is nil" do
328
+ it "should set failed_attempts to 1" do
329
+ @job.fail(@err)
330
+ @job.failed_attempts.should == 1
331
+ end
332
+ end
333
+ end
334
+
335
+ context "when retry_delay returns nil" do
336
+ it "should not re-enqueue" do
337
+ @job.should_not_receive(:enqueue)
338
+ @job.should_not_receive(:enqueue_with_delay)
339
+ @job.fail(@err)
340
+ end
341
+
342
+ it "should set failed to true" do
343
+ @job.fail(@err)
344
+ @job.failed.should be_true
345
+ end
346
+ end
347
+ end
348
+
349
+ context "when failure_action is not 'retry'" do
350
+ it "should not re-enqueue" do
351
+ @job.should_not_receive(:enqueue)
352
+ @job.should_not_receive(:enqueue_with_delay)
353
+ @job.fail(@err)
354
+ end
355
+ end
356
+ end
357
+
358
+ end
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'bundler'
5
+ Bundler.require
6
+ require 'pry'
7
+ require 'pry-nav'
8
+ require 'pry-stack_explorer'
9
+
10
+ require 'boss_queue'
11
+
12
+ # Requires supporting files with custom matchers and macros, etc,
13
+ # in ./support/ and its subdirectories.
14
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
15
+
16
+ RSpec.configure do |config|
17
+
18
+
19
+ class TestClass
20
+ def id
21
+ 'xyz'
22
+ end
23
+
24
+ def self.test_class_method
25
+ end
26
+
27
+ def test_instance_method
28
+ end
29
+ end
30
+
31
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: boss_queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Nelson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: jeweler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: pry
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: pry-nav
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: pry-stack_explorer
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: A fault tolerant job queue built around Amazon SQS & DynamoDB
127
+ email: daniel@populr.me
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files:
131
+ - LICENSE
132
+ - README.md
133
+ files:
134
+ - .rspec
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - LICENSE
138
+ - README.md
139
+ - Rakefile
140
+ - VERSION
141
+ - lib/boss_queue.rb
142
+ - lib/boss_queue/boss_queue.rb
143
+ - lib/boss_queue/job.rb
144
+ - spec/boss_queue_spec.rb
145
+ - spec/job_spec.rb
146
+ - spec/spec_helper.rb
147
+ homepage: https://github.com/populr/boss_queue
148
+ licenses:
149
+ - MIT
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ! '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ segments:
161
+ - 0
162
+ hash: 1086179720775375384
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ! '>='
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 1.8.25
172
+ signing_key:
173
+ specification_version: 3
174
+ summary: A fault tolerant job queue built around Amazon SQS & DynamoDB
175
+ test_files: []