hotseat 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "couchrest", ">= 1.1.0"
4
+
5
+ group :development, :test do
6
+ gem "rspec"
7
+ gem "yard"
8
+ gem "bundler"
9
+ gem "jeweler"
10
+ end
11
+
12
+ group :test do
13
+ gem 'simplecov', '>= 0.4.0', :require => false
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Elad Kehat
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = Hotseat
2
+
3
+ Add work queue functionality to an existing CouchDB database.
4
+
5
+ == Contributing to Hotseat
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Elad Kehat. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,35 @@
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.name = "hotseat"
17
+ gem.homepage = "http://github.com/eladkehat/hotseat"
18
+ gem.license = "MIT"
19
+ gem.summary = %Q{Add work queue functionality to an existing CouchDB database}
20
+ #gem.description = %Q{longer description of the gem}
21
+ gem.email = "eladkehat@gmail.com"
22
+ gem.authors = ["Elad Kehat"]
23
+ end
24
+ Jeweler::RubygemsDotOrgTasks.new
25
+
26
+ require 'rspec/core'
27
+ require 'rspec/core/rake_task'
28
+ RSpec::Core::RakeTask.new(:spec) do |spec|
29
+ spec.pattern = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ task :default => :spec
33
+
34
+ require 'yard'
35
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
data/hotseat.gemspec ADDED
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{hotseat}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Elad Kehat"]
12
+ s.date = %q{2011-08-06}
13
+ s.email = %q{eladkehat@gmail.com}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE.txt",
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ ".rspec",
21
+ "Gemfile",
22
+ "LICENSE.txt",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/hotseat.rb",
27
+ "lib/hotseat/hotseat.rb",
28
+ "lib/hotseat/queue.rb",
29
+ "spec/hotseat/hotseat_spec.rb",
30
+ "spec/hotseat/queue_spec.rb",
31
+ "spec/spec_helper.rb"
32
+ ]
33
+ s.homepage = %q{http://github.com/eladkehat/hotseat}
34
+ s.licenses = ["MIT"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.7.2}
37
+ s.summary = %q{Add work queue functionality to an existing CouchDB database}
38
+
39
+ if s.respond_to? :specification_version then
40
+ s.specification_version = 3
41
+
42
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
43
+ s.add_runtime_dependency(%q<couchrest>, [">= 1.1.0"])
44
+ s.add_development_dependency(%q<rspec>, [">= 0"])
45
+ s.add_development_dependency(%q<yard>, [">= 0"])
46
+ s.add_development_dependency(%q<bundler>, [">= 0"])
47
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
48
+ else
49
+ s.add_dependency(%q<couchrest>, [">= 1.1.0"])
50
+ s.add_dependency(%q<rspec>, [">= 0"])
51
+ s.add_dependency(%q<yard>, [">= 0"])
52
+ s.add_dependency(%q<bundler>, [">= 0"])
53
+ s.add_dependency(%q<jeweler>, [">= 0"])
54
+ end
55
+ else
56
+ s.add_dependency(%q<couchrest>, [">= 1.1.0"])
57
+ s.add_dependency(%q<rspec>, [">= 0"])
58
+ s.add_dependency(%q<yard>, [">= 0"])
59
+ s.add_dependency(%q<bundler>, [">= 0"])
60
+ s.add_dependency(%q<jeweler>, [">= 0"])
61
+ end
62
+ end
63
+
@@ -0,0 +1,89 @@
1
+ module Hotseat
2
+
3
+ class << self
4
+
5
+ def queue(db)
6
+ Hotseat::Queue.new(db)
7
+ end
8
+ alias :make_queue :queue
9
+
10
+ def queue?(db)
11
+ # ignore system dbs like _replicator and _users
12
+ return false if db.name =~ /^_/
13
+ begin
14
+ db.get design_doc_id
15
+ rescue RestClient::ResourceNotFound
16
+ # either the database or the design doc does not exist
17
+ false
18
+ end
19
+ end
20
+
21
+ def queues(couch_server)
22
+ couch_server.databases.select do |db|
23
+ queue?(couch_server.database(db))
24
+ end
25
+ end
26
+
27
+ def design_doc_id
28
+ "_design/#{config[:design_doc_name]}"
29
+ end
30
+
31
+ def pending_view_name
32
+ "#{config[:design_doc_name]}/#{config[:pending_view_name]}"
33
+ end
34
+
35
+ def locked_view_name
36
+ "#{config[:design_doc_name]}/#{config[:locked_view_name]}"
37
+ end
38
+
39
+ def done_view_name
40
+ "#{config[:design_doc_name]}/#{config[:done_view_name]}"
41
+ end
42
+
43
+ def all_view_name
44
+ "#{config[:design_doc_name]}/#{config[:all_view_name]}"
45
+ end
46
+
47
+ def design_doc
48
+ q = "doc.#{config[:object_name]}"
49
+ lock = "#{q}.lock"
50
+ done = "#{q}.done"
51
+ pending_func = <<-JAVASCRIPT
52
+ function(doc) { if (#{q} && !(#{lock} || #{done})) emit(#{q}.at, null); }
53
+ JAVASCRIPT
54
+ locked_func = <<-JAVASCRIPT
55
+ function(doc) { if (#{q} && #{lock}) emit(#{lock}.at, null); }
56
+ JAVASCRIPT
57
+ done_func = <<-JAVASCRIPT
58
+ function(doc) { if (#{q} && #{done}) emit(#{done}.at, null); }
59
+ JAVASCRIPT
60
+ all_func = <<-JAVASCRIPT
61
+ function(doc) { if (#{q}) emit(#{q}.at, null); }
62
+ JAVASCRIPT
63
+ {
64
+ '_id' => "_design/#{config[:design_doc_name]}",
65
+ :views => {
66
+ config[:pending_view_name] => { :map => pending_func.chomp },
67
+ config[:locked_view_name] => { :map => locked_func.chomp },
68
+ config[:done_view_name] => { :map => done_func.chomp },
69
+ config[:all_view_name] => { :map => all_func.chomp },
70
+ }
71
+ }
72
+ end
73
+
74
+ def config
75
+ CONFIG
76
+ end
77
+
78
+ end
79
+
80
+ CONFIG = {
81
+ :design_doc_name => 'hotseat_queue',
82
+ :pending_view_name => 'pending',
83
+ :locked_view_name => 'locked',
84
+ :done_view_name => 'done',
85
+ :all_view_name => 'all',
86
+ :object_name => 'hotseat',
87
+ }
88
+
89
+ end
@@ -0,0 +1,157 @@
1
+ require 'time'
2
+
3
+ module Hotseat
4
+
5
+ class QueueError < RuntimeError
6
+ end
7
+
8
+ class Queue
9
+ attr_reader :db
10
+
11
+ class << self
12
+
13
+ def patch(doc)
14
+ doc[Hotseat.config[:object_name]] = {'at' => Time.now.utc.iso8601, 'by' => $$}
15
+ doc
16
+ end
17
+
18
+ def unpatch(doc)
19
+ doc.delete( Hotseat.config[:object_name] )
20
+ doc
21
+ end
22
+
23
+ def add_lock(doc)
24
+ obj = doc[Hotseat.config[:object_name]]
25
+ obj['lock'] = {'at' => Time.now.utc.iso8601, 'by' => $$}
26
+ doc
27
+ end
28
+
29
+ def locked?(doc)
30
+ if obj = doc[Hotseat.config[:object_name]]
31
+ obj.has_key? 'lock'
32
+ end
33
+ end
34
+
35
+ def remove_lock(doc)
36
+ obj = doc[Hotseat.config[:object_name]]
37
+ obj.delete 'lock'
38
+ doc
39
+ end
40
+
41
+ def mark_done(doc)
42
+ obj = doc[Hotseat.config[:object_name]]
43
+ obj['done'] = {'at' => Time.now.utc.iso8601, 'by' => $$}
44
+ doc
45
+ end
46
+
47
+ end
48
+
49
+ def initialize(db)
50
+ @db = db
51
+ unless Hotseat.queue?(@db)
52
+ @db.save_doc Hotseat.design_doc
53
+ end
54
+ end
55
+
56
+ def add(doc_id)
57
+ @db.update_doc(doc_id) {|doc| Queue.patch doc }
58
+ end
59
+
60
+ def add_bulk(doc_ids)
61
+ #Note: this silently ignores missing doc_ids
62
+ docs = @db.bulk_load(doc_ids)['rows'].map{|row| row['doc']}.compact
63
+ docs.each {|doc| Queue.patch doc }
64
+ @db.bulk_save docs, use_uuids=false
65
+ end
66
+
67
+ def num_pending
68
+ @db.view(Hotseat.pending_view_name, :limit => 0)['total_rows']
69
+ end
70
+ alias :size :num_pending
71
+
72
+ def get(n=1)
73
+ rows = @db.view(Hotseat.pending_view_name, :limit => n, :include_docs => true)['rows']
74
+ rows.map{|row| row['doc']} unless rows.empty?
75
+ end
76
+
77
+ def lease(n=1)
78
+ if docs = get(n)
79
+ docs.each {|doc| Queue.add_lock doc }
80
+ response = @db.bulk_save docs, use_uuids=false
81
+ # Some docs may have failed to lock - probably updated by another process
82
+ locked_ids = response.reject{|res| res['error']}.map{|res| res['id']}
83
+ if locked_ids.length < docs.length
84
+ # This runs in O(n^2) time. Performance will be bad here if the number of documents
85
+ # is very large. Assuming that this isn't normally the case I'm keeping it simple.
86
+ docs.keep_if{|doc| locked_ids.include? doc['_id']}
87
+ end
88
+ docs
89
+ end
90
+ end
91
+
92
+ def num_locked
93
+ @db.view(Hotseat.locked_view_name, :limit => 0)['total_rows']
94
+ end
95
+
96
+ def remove(doc_id, opts={})
97
+ @db.update_doc(doc_id) do |doc|
98
+ raise(QueueError, "Document was already removed") unless Queue.locked?(doc)
99
+ if opts.delete(:forget)
100
+ Queue.unpatch doc
101
+ else
102
+ Queue.mark_done( Queue.remove_lock( doc ) )
103
+ end
104
+ end
105
+ end
106
+
107
+ def remove_bulk(doc_ids, opts={})
108
+ rows = @db.bulk_load(doc_ids)['rows']
109
+ docs, missing = rows.partition {|row| row['doc'] }
110
+ docs.map! {|row| row['doc'] }
111
+ locked, unlocked = docs.partition {|doc| Queue.locked? doc }
112
+ forget = opts.delete(:forget)
113
+ locked.each do |doc|
114
+ if forget
115
+ Queue.unpatch doc
116
+ else
117
+ Queue.mark_done( Queue.remove_lock( doc ) )
118
+ end
119
+ end
120
+ @db.bulk_save locked, use_uuids=false
121
+ {'errors' =>
122
+ unlocked.map {|doc| {'id' => doc['_id'], 'error' => 'unlocked' } } +
123
+ missing.map {|row| {'id' => row['key'], 'error' => row['error']} }
124
+ }
125
+ end
126
+
127
+ def num_done
128
+ @db.view(Hotseat.done_view_name, :limit => 0)['total_rows']
129
+ end
130
+
131
+ def num_all
132
+ @db.view(Hotseat.all_view_name, :limit => 0)['total_rows']
133
+ end
134
+ alias :num_total :num_all
135
+
136
+ def forget(doc_id)
137
+ @db.update_doc(doc_id) do |doc|
138
+ Queue.unpatch doc
139
+ end
140
+ end
141
+
142
+ def forget_bulk(doc_ids)
143
+ #Note: this silently ignores missing doc_ids
144
+ docs = @db.bulk_load(doc_ids)['rows'].map{|row| row['doc']}.compact
145
+ docs.each {|doc| Queue.unpatch doc }
146
+ @db.bulk_save docs, use_uuids=false
147
+ end
148
+
149
+ def purge
150
+ rows = @db.view(Hotseat.all_view_name, :include_docs => true)['rows']
151
+ docs = rows.map{|row| row['doc']}
152
+ docs.each{|doc| Queue.unpatch doc }
153
+ @db.bulk_save docs, use_uuids=false
154
+ end
155
+
156
+ end
157
+ end
data/lib/hotseat.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'hotseat/queue'
2
+ require 'hotseat/hotseat'
@@ -0,0 +1,50 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ module Hotseat
4
+
5
+ describe "#queue" do
6
+ it "should return a queue on the given database" do
7
+ reset_test_db!
8
+ q = Hotseat.queue DB
9
+ q.should be_instance_of Hotseat::Queue
10
+ Hotseat.queue?(DB).should be_true
11
+ end
12
+
13
+ end
14
+
15
+ describe "#queue?" do
16
+ it "should be true when a given database is a queue" do
17
+ reset_test_db!
18
+ Hotseat.queue DB
19
+ Hotseat.queue?(DB).should be_true
20
+ end
21
+
22
+ it "should be false when a given database is not a queue" do
23
+ reset_test_db!
24
+ Hotseat.queue?(DB).should be_false
25
+ end
26
+
27
+ it "should be false when a given database does not exist" do
28
+ delete_test_db!
29
+ Hotseat.queue?(DB).should be_false
30
+ end
31
+ end
32
+
33
+ describe "#queues" do
34
+ # not making sure that non-queues aren't returned since the host we test on
35
+ # may be have some non-testing (production) queues on it which we don't know about
36
+ it "should return a list of queues (database names) on the host" do
37
+ delete_test_db!
38
+ test_dbs = (1..3).map{|i| TEST_SERVER.database("#{TESTDB}#{i}") }
39
+ begin
40
+ test_dbs.each do |db|
41
+ db.create!
42
+ Hotseat.queue(db)
43
+ end
44
+ results = Hotseat.queues TEST_SERVER
45
+ test_dbs.each {|db| results.should include URI.unescape(db.name) }
46
+ ensure test_dbs.each{|db| db.delete! rescue nil } end
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,394 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ module Hotseat
4
+ describe Queue do
5
+
6
+ ########################
7
+ ### Helper methods ###
8
+ ########################
9
+ def reset_test_queue!
10
+ reset_test_db!
11
+ @q = Hotseat.make_queue DB
12
+ end
13
+
14
+ def sample_doc(var=nil)
15
+ {:field => var || 'value' }
16
+ end
17
+
18
+ # Returns the new doc id's
19
+ def create_some_docs(n=3)
20
+ (1..n).map {|i| DB.save_doc(sample_doc(i))['id'] }
21
+ end
22
+
23
+ def enqueue(doc_ids)
24
+ @q.add_bulk doc_ids
25
+ end
26
+
27
+ describe "#initialize" do
28
+ it "should create a Hotseat design doc in the database if one does not exist" do
29
+ reset_test_db!
30
+ q = Hotseat::Queue.new(DB)
31
+ q.db.get(Hotseat.design_doc_id).should_not be_nil
32
+ end
33
+ end
34
+
35
+ describe "#patch" do
36
+ it "should add a queue object on a document" do
37
+ doc = sample_doc
38
+ Queue.patch doc
39
+ doc.should have_key(Hotseat.config[:object_name])
40
+ end
41
+
42
+ it "should return the patched document with original data intact" do
43
+ doc = Queue.patch sample_doc
44
+ sample_doc.each do |k,v|
45
+ doc[k].should == v
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#unpatch" do
51
+ it "should remove the queue object from a document" do
52
+ doc = Queue.unpatch( Queue.patch( sample_doc ) )
53
+ doc.should_not have_key(Hotseat.config[:object_name])
54
+ end
55
+ end
56
+
57
+ describe "#add_lock" do
58
+ it "should add a lock object on a patched document" do
59
+ doc = Queue.add_lock( Queue.patch( sample_doc ) )
60
+ patch = doc[Hotseat.config[:object_name]]
61
+ patch.should have_key('lock')
62
+ end
63
+ end
64
+
65
+ describe "#remove_lock" do
66
+ before(:each) { @doc = Queue.remove_lock( Queue.add_lock( Queue.patch( sample_doc ) ) ) }
67
+ it "should remove a lock object added by #add_lock" do
68
+ patch = @doc[Hotseat.config[:object_name]]
69
+ patch.should_not have_key('lock')
70
+ end
71
+ it "should leave the queue patch intact" do
72
+ @doc.should have_key(Hotseat.config[:object_name])
73
+ end
74
+ end
75
+
76
+ describe "#locked?" do
77
+ it "should be true for a locked document" do
78
+ doc = Queue.add_lock( Queue.patch( sample_doc ) )
79
+ Queue.locked?(doc).should be_true
80
+ end
81
+ it "should be false for a patched, but not locked document" do
82
+ doc = Queue.patch( sample_doc )
83
+ Queue.locked?(doc).should be_false
84
+ end
85
+ it "should be false for an unlocked document" do
86
+ doc = Queue.remove_lock( Queue.add_lock( Queue.patch( sample_doc ) ) )
87
+ Queue.locked?(doc).should be_false
88
+ end
89
+ end
90
+
91
+ describe "#mark_done" do
92
+ before(:each) { @doc = Queue.mark_done( Queue.patch( sample_doc ) ) }
93
+ it "should leave the queue patch intact" do
94
+ @doc.should have_key(Hotseat.config[:object_name])
95
+ end
96
+ it "should add a 'done' object on a patched document" do
97
+ patch = @doc[Hotseat.config[:object_name]]
98
+ patch.should have_key('done')
99
+ end
100
+ end
101
+
102
+ describe "#add" do
103
+ it "should add a document to the queue, given a doc id" do
104
+ reset_test_queue!
105
+ doc_id = DB.save_doc(sample_doc)['id']
106
+ @q.add doc_id
107
+ DB.get(doc_id).should have_key(Hotseat.config[:object_name])
108
+ end
109
+ end
110
+
111
+ describe "#add_bulk" do
112
+ it "should add multiple documents in bulk, given multiple doc ids" do
113
+ reset_test_queue!
114
+ doc_ids = create_some_docs
115
+ @q.add_bulk doc_ids
116
+ doc_ids.each do |doc_id|
117
+ DB.get(doc_id).should have_key(Hotseat.config[:object_name])
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "#num_pending" do
123
+ it "should return the number of documents available for lease" do
124
+ reset_test_queue!
125
+ enqueue( create_some_docs(3) )
126
+ @q.num_pending.should == 3
127
+ end
128
+ end
129
+
130
+ describe "#lease" do
131
+ before(:each) do
132
+ reset_test_queue!
133
+ enqueue( create_some_docs(3) )
134
+ end
135
+
136
+ it "should return an array of queued documents" do
137
+ docs = @q.lease 2
138
+ docs.should_not be_nil
139
+ docs.should have(2).items
140
+ docs.each do |doc|
141
+ db_doc = DB.get(doc['_id'])
142
+ db_doc.should be_kind_of CouchRest::Document
143
+ db_doc.should have_key(Hotseat.config[:object_name])
144
+ end
145
+ end
146
+
147
+ it "should lock a pending document" do
148
+ doc_id = @q.lease.first['_id']
149
+ doc = DB.get(doc_id)
150
+ doc.should have_key(Hotseat.config[:object_name])
151
+ doc[Hotseat.config[:object_name]].should have_key('lock')
152
+ end
153
+
154
+ it "should lock and return up to the specified number of documents" do
155
+ ids = @q.lease 4
156
+ ids.should have(3).items
157
+ end
158
+ end
159
+
160
+ context "when no documents are pending" do
161
+ before(:each) { reset_test_queue! }
162
+
163
+ it "#lease should return nil" do
164
+ @q.lease.should be_nil
165
+ end
166
+
167
+ it "#get should return nil" do
168
+ @q.get.should be_nil
169
+ end
170
+
171
+ it "#num_pending should return zero" do
172
+ @q.num_pending.should be_zero
173
+ end
174
+ end
175
+
176
+ context "locked documents" do
177
+ before(:each) do
178
+ reset_test_queue!
179
+ enqueue( create_some_docs(3) )
180
+ end
181
+
182
+ it "should not be pending" do
183
+ locked_id = @q.lease.first['_id']
184
+ pending_ids = @q.db.view(Hotseat.pending_view_name)['rows'].map{|row| row['id']}
185
+ pending_ids.should_not include(locked_id)
186
+ end
187
+
188
+ it "should be counted by #num_locked" do
189
+ @q.lease 2
190
+ @q.num_locked.should == 2
191
+ end
192
+ end
193
+
194
+ describe "#get" do
195
+ before(:each) do
196
+ reset_test_queue!
197
+ enqueue( create_some_docs(3) )
198
+ end
199
+
200
+ it "should return an array of pending documents" do
201
+ docs = @q.get 2
202
+ docs.should_not be_nil
203
+ docs.should have(2).items
204
+ docs.each do |doc|
205
+ db_doc = DB.get(doc['_id'])
206
+ db_doc.should have_key(Hotseat.config[:object_name])
207
+ end
208
+ end
209
+
210
+ it "should not lock the documents it returns" do
211
+ doc_id = @q.get.first['_id']
212
+ doc = DB.get(doc_id)
213
+ doc.should have_key(Hotseat.config[:object_name])
214
+ doc[Hotseat.config[:object_name]].should_not have_key('lock')
215
+ end
216
+
217
+ it "should return up to the specified number of documents" do
218
+ docs = @q.get 4
219
+ docs.should have(3).items
220
+ end
221
+ end
222
+
223
+ describe "#remove" do
224
+ before(:each) do
225
+ reset_test_queue!
226
+ enqueue( create_some_docs(3) )
227
+ @leased = @q.lease 2
228
+ @doc_id = @leased.first['_id']
229
+ end
230
+
231
+ it "should unlock a leased document" do
232
+ @q.remove @doc_id
233
+ doc = DB.get(@doc_id)
234
+ doc.should have_key(Hotseat.config[:object_name])
235
+ doc[Hotseat.config[:object_name]].should_not have_key('lock')
236
+ end
237
+
238
+ it "should remove a document from the queue" do
239
+ @q.remove @doc_id
240
+ pending_docs = @q.get 3 # ensure we get all remaining pending docs
241
+ pending_docs.map{|doc| doc['_id']}.should_not include(@doc_id)
242
+ end
243
+
244
+ it "should raise an error if the lock was already removed" do
245
+ doc = @leased.first
246
+ @q.remove @doc_id
247
+ expect {
248
+ @q.remove @doc_id
249
+ }.to raise_error(Hotseat::QueueError)
250
+ end
251
+
252
+ it "should raise an error if the document is missing from the database" do
253
+ doc_id = @leased.first['_id']
254
+ doc = @q.db.get(doc_id)
255
+ @q.db.delete_doc(doc)
256
+ expect {
257
+ @q.remove doc['_id']
258
+ }.to raise_error
259
+ end
260
+
261
+ it "should leave queue history in the document (mark as done) by default" do
262
+ @q.remove @doc_id
263
+ doc = DB.get(@doc_id)
264
+ doc.should have_key(Hotseat.config[:object_name])
265
+ doc[Hotseat.config[:object_name]].should have_key('done')
266
+ end
267
+
268
+ it "should delete queue history from the document when forget=true" do
269
+ @q.remove @doc_id, :forget => true
270
+ doc = DB.get(@doc_id)
271
+ doc.should_not have_key(Hotseat.config[:object_name])
272
+ end
273
+ end
274
+
275
+ describe "#remove_bulk" do
276
+ before(:each) do
277
+ reset_test_queue!
278
+ enqueue( create_some_docs(10) )
279
+ @leased = @q.lease 8
280
+ @doc_ids = @leased.take(5).map{|doc| doc['_id'] }
281
+ end
282
+
283
+ it "should unlock leased documents" do
284
+ @q.remove_bulk @doc_ids
285
+ docs = DB.get_bulk(@doc_ids)['rows'].map{|row| row['doc']}
286
+ docs.each do |doc|
287
+ doc.should have_key(Hotseat.config[:object_name])
288
+ doc[Hotseat.config[:object_name]].should_not have_key('lock')
289
+ end
290
+ end
291
+
292
+ it "should remove multiple documents from the queue" do
293
+ @q.remove_bulk @doc_ids
294
+ pending_docs = @q.get 3 # ensure we get all remaining pending docs
295
+ pending_ids = pending_docs.map{|doc| doc['_id'] }
296
+ (pending_ids - @doc_ids).should eql(pending_ids)
297
+ end
298
+
299
+ it "should report docs whose lock was already removed" do
300
+ rem_ids = @doc_ids.take(2)
301
+ @q.remove_bulk rem_ids
302
+ res = @q.remove_bulk @doc_ids
303
+ res['errors'].should have(2).errors
304
+ res['errors'].map{|err| err['id']}.should == rem_ids
305
+ end
306
+
307
+ it "should report docs that are missing from the database" do
308
+ rem_ids = @doc_ids.take(2)
309
+ docs = rem_ids.map{|id| @q.db.get(id) }
310
+ docs.each {|doc| @q.db.delete_doc(doc) }
311
+ res = @q.remove_bulk @doc_ids
312
+ res['errors'].should have(2).errors
313
+ res['errors'].map{|err| err['id']}.should == rem_ids
314
+ end
315
+
316
+ it "should leave queue history in the document (mark as done) by default" do
317
+ @q.remove_bulk @doc_ids
318
+ docs = DB.get_bulk(@doc_ids)['rows'].map{|row| row['doc']}
319
+ docs.each do |doc|
320
+ doc.should have_key(Hotseat.config[:object_name])
321
+ doc[Hotseat.config[:object_name]].should have_key('done')
322
+ end
323
+ end
324
+
325
+ it "should delete queue history from the document when forget=true" do
326
+ @q.remove_bulk @doc_ids, :forget => true
327
+ docs = DB.get_bulk(@doc_ids)['rows'].map{|row| row['doc']}
328
+ docs.each do |doc|
329
+ doc.should_not have_key(Hotseat.config[:object_name])
330
+ end
331
+ end
332
+ end
333
+
334
+ describe "#num_done" do
335
+ it "should return the number of documents done" do
336
+ reset_test_queue!
337
+ enqueue( create_some_docs(10) )
338
+ @leased = @q.lease 8
339
+ @doc_ids = @leased.take(5).map{|doc| doc['_id'] }
340
+ @q.remove_bulk @doc_ids
341
+ @q.num_done.should == 5
342
+ end
343
+ end
344
+
345
+ describe "#num_all" do
346
+ it "should return the total number of documents in the queue" do
347
+ reset_test_queue!
348
+ enqueue( create_some_docs(10) )
349
+ @leased = @q.lease 8
350
+ @doc_ids = @leased.take(5).map{|doc| doc['_id'] }
351
+ @q.remove_bulk @doc_ids
352
+ @q.num_all.should == 10
353
+ end
354
+ end
355
+
356
+ describe "#forget" do
357
+ it "should delete queue history from a document" do
358
+ reset_test_queue!
359
+ enqueue( create_some_docs(1) )
360
+ doc_id = @q.get.first['_id']
361
+ @q.forget doc_id
362
+ doc = DB.get(doc_id)
363
+ doc.should_not have_key(Hotseat.config[:object_name])
364
+ end
365
+ end
366
+
367
+ describe "#forget_bulk" do
368
+ it "should delete queue history from multiple document" do
369
+ reset_test_queue!
370
+ enqueue( create_some_docs(3) )
371
+ doc_ids = @q.get(3).map{|doc| doc['_id'] }
372
+ @q.forget_bulk doc_ids
373
+ @q.db.bulk_load(doc_ids)['rows'].map{|row| row['doc']}.each do |doc|
374
+ doc.should_not have_key(Hotseat.config[:object_name])
375
+ end
376
+ end
377
+ end
378
+
379
+ describe "#purge" do
380
+ before do
381
+ reset_test_queue!
382
+ enqueue( create_some_docs(10) )
383
+ leased = @q.lease 5
384
+ @q.remove_bulk leased.take(2)
385
+ end
386
+
387
+ it "should remove (and forget) all documents from the queue" do
388
+ @q.purge
389
+ @q.num_all.should == 0
390
+ end
391
+ end
392
+ end
393
+
394
+ end
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require(:default, :test)
4
+ require 'simplecov'
5
+ SimpleCov.start
6
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
7
+ require 'hotseat'
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ #Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+ # copied and modified from https://github.com/couchrest/couchrest/blob/master/spec/spec_helper.rb
14
+ unless defined?(TESTDB)
15
+ COUCHHOST = ENV['COUCHHOST'] || "http://127.0.0.1:5984"
16
+ TESTDB = "hotseat%2Ftest"
17
+ TEST_SERVER = CouchRest.new COUCHHOST
18
+ TEST_SERVER.default_database = TESTDB
19
+ DB = TEST_SERVER.database(TESTDB)
20
+ end
21
+
22
+ def reset_test_db!
23
+ DB.recreate! rescue nil
24
+ DB
25
+ end
26
+
27
+ def delete_test_db!
28
+ DB.delete! rescue nil
29
+ end
30
+
31
+ def clean_up_test_dbs
32
+ cr = TEST_SERVER
33
+ test_dbs = cr.databases.select { |db| db =~ /^#{URI.unescape(TESTDB)}/ }
34
+ test_dbs.each do |db|
35
+ cr.database(db).delete! rescue nil
36
+ end
37
+ end
38
+
39
+ RSpec.configure do |config|
40
+ config.after(:all) do
41
+ clean_up_test_dbs
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hotseat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Elad Kehat
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-06 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: couchrest
16
+ requirement: &76166650 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *76166650
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &76165700 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *76165700
36
+ - !ruby/object:Gem::Dependency
37
+ name: yard
38
+ requirement: &76165210 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *76165210
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: &76160770 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *76160770
58
+ - !ruby/object:Gem::Dependency
59
+ name: jeweler
60
+ requirement: &76160430 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *76160430
69
+ description:
70
+ email: eladkehat@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - LICENSE.txt
75
+ - README.rdoc
76
+ files:
77
+ - .document
78
+ - .rspec
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.rdoc
82
+ - Rakefile
83
+ - VERSION
84
+ - hotseat.gemspec
85
+ - lib/hotseat.rb
86
+ - lib/hotseat/hotseat.rb
87
+ - lib/hotseat/queue.rb
88
+ - spec/hotseat/hotseat_spec.rb
89
+ - spec/hotseat/queue_spec.rb
90
+ - spec/spec_helper.rb
91
+ homepage: http://github.com/eladkehat/hotseat
92
+ licenses:
93
+ - MIT
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ segments:
105
+ - 0
106
+ hash: -1003361075
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 1.7.2
116
+ signing_key:
117
+ specification_version: 3
118
+ summary: Add work queue functionality to an existing CouchDB database
119
+ test_files: []