boss_queue 0.1.0

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/.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: []