sunspot_index_queue 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
1
+ require 'sunspot/session_proxy/abstract_session_proxy'
2
+
3
+ module Sunspot
4
+ class IndexQueue
5
+ # This is a Sunspot::SessionProxy that works with the IndexQueue class. Most update requests will
6
+ # be added to the queue and be processed asynchronously. The exceptions are the +remove+ method with
7
+ # a block and the +remove_all+ method. These will send their commands directly to Solr since the queue
8
+ # cannot handle delete by query. You should avoid calling these methods
9
+ class SessionProxy < Sunspot::SessionProxy::AbstractSessionProxy
10
+ attr_reader :queue, :session
11
+
12
+ delegate :new_search, :search, :new_more_like_this, :more_like_this, :config, :to => :session
13
+
14
+ # Create a new session proxy for a particular queue (default to a queue for all classes bound to the
15
+ # default session configuration). You can specify the session argument if the session used for queries should be
16
+ # different than the one the queue is bound to.
17
+ def initialize (queue = nil, session = nil)
18
+ @queue = queue || IndexQueue.new
19
+ @session = session || @queue.session
20
+ end
21
+
22
+ # Does nothing in this implementation.
23
+ def batch
24
+ yield if block_given?
25
+ end
26
+
27
+ # Does nothing in this implementation.
28
+ def commit
29
+ # no op
30
+ end
31
+
32
+ # Does nothing in this implementation.
33
+ def commit_if_delete_dirty
34
+ # no op
35
+ end
36
+
37
+ # Does nothing in this implementation.
38
+ def commit_if_dirty
39
+ # no op
40
+ end
41
+
42
+ # Always returns false in this implementation.
43
+ def delete_dirty?
44
+ false
45
+ end
46
+
47
+ # Always returns false in this implementation.
48
+ def dirty?
49
+ false
50
+ end
51
+
52
+ # Queues up the index operation for later.
53
+ def index (*objects)
54
+ objects.flatten.each do |object|
55
+ queue.index(object)
56
+ end
57
+ end
58
+
59
+ # Queues up the index operation for later.
60
+ def index! (*objects)
61
+ index(*objects)
62
+ end
63
+
64
+ # Queues up the remove operation for later unless a block is passed. In that case it will
65
+ # be performed immediately.
66
+ def remove (*objects, &block)
67
+ if block
68
+ # Delete by query not supported by queue, so send to server
69
+ queue.session.remove(*objects, &block)
70
+ else
71
+ objects.flatten.each do |object|
72
+ queue.remove(object)
73
+ end
74
+ end
75
+ end
76
+
77
+ # Queues up the remove operation for later unless a block is passed. In that case it will
78
+ # be performed immediately.
79
+ def remove! (*objects, &block)
80
+ if block
81
+ # Delete by query not supported by queue, so send to server
82
+ queue.session.remove!(*objects, &block)
83
+ else
84
+ remove(*objects)
85
+ end
86
+ end
87
+
88
+ # Proxies remove_all to the queue session.
89
+ def remove_all (*classes)
90
+ # Delete by query not supported by queue, so send to server
91
+ queue.session.remove_all(*classes)
92
+ end
93
+
94
+ # Proxies remove_all! to the queue session.
95
+ def remove_all! (*classes)
96
+ # Delete by query not supported by queue, so send to server
97
+ queue.session.remove_all!(*classes)
98
+ end
99
+
100
+ # Queues up the index operation for later.
101
+ def remove_by_id (clazz, id)
102
+ queue.remove(:class => clazz, :id => id)
103
+ end
104
+
105
+ # Queues up the index operation for later.
106
+ def remove_by_id! (clazz, id)
107
+ remove_by_id(clazz, id)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,5 @@
1
+ require 'sunspot'
2
+
3
+ module Sunspot
4
+ autoload :IndexQueue, File.expand_path('../sunspot/index_queue', __FILE__)
5
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'sqlite3'
3
+ require File.expand_path('../entry_impl_examples', __FILE__)
4
+
5
+ describe Sunspot::IndexQueue::Entry::ActiveRecordImpl do
6
+
7
+ before :all do
8
+ db_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'tmp'))
9
+ Dir.mkdir(db_dir) unless File.exist?(db_dir)
10
+ db = File.join(db_dir, 'sunspot_index_queue_test.sqlite3')
11
+ ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => db)
12
+ Sunspot::IndexQueue::Entry.implementation = :active_record
13
+ Sunspot::IndexQueue::Entry::ActiveRecordImpl.create_table
14
+ end
15
+
16
+ after :all do
17
+ db_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'tmp'))
18
+ db = File.join(db_dir, 'sunspot_index_queue_test.sqlite3')
19
+ ActiveRecord::Base.connection.disconnect!
20
+ File.delete(db) if File.exist?(db)
21
+ Dir.delete(db_dir) if File.exist?(db_dir) and Dir.entries(db_dir).reject{|f| f.match(/^\.+$/)}.empty?
22
+ Sunspot::IndexQueue::Entry.implementation = nil
23
+ end
24
+
25
+ let(:factory) do
26
+ factory = Object.new
27
+ def factory.create (attributes)
28
+ Sunspot::IndexQueue::Entry::ActiveRecordImpl.create!(attributes)
29
+ end
30
+
31
+ def factory.delete_all
32
+ Sunspot::IndexQueue::Entry::ActiveRecordImpl.delete_all
33
+ end
34
+
35
+ def factory.find (id)
36
+ Sunspot::IndexQueue::Entry::ActiveRecordImpl.find_by_id(id)
37
+ end
38
+
39
+ factory
40
+ end
41
+
42
+ it_should_behave_like "Entry implementation"
43
+
44
+ end
@@ -0,0 +1,118 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Sunspot::IndexQueue::Batch do
4
+
5
+ before :all do
6
+ Sunspot::IndexQueue::Entry.implementation = :mock
7
+ end
8
+
9
+ after :all do
10
+ Sunspot::IndexQueue::Entry.implementation = nil
11
+ end
12
+
13
+ subject { Sunspot::IndexQueue::Batch.new(queue, [entry_1, entry_2]) }
14
+ let(:entry_1) { Sunspot::IndexQueue::Entry::MockImpl.new(:record => record_1, :delete => false) }
15
+ let(:entry_2) { Sunspot::IndexQueue::Entry::MockImpl.new(:record => record_2, :delete => true) }
16
+ let(:record_1) { Sunspot::IndexQueue::Test::Searchable.new(1) }
17
+ let(:record_2) { Sunspot::IndexQueue::Test::Searchable.new(2) }
18
+ let(:queue) { Sunspot::IndexQueue.new(:session => session) }
19
+ let(:session) { Sunspot::Session.new }
20
+
21
+ it "should submit all entries in a batch to Solr and commit them" do
22
+ entry_1.stub!(:record).and_return(record_1)
23
+ session.should_receive(:batch).and_yield
24
+ session.should_receive(:index).with(record_1)
25
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id)
26
+ session.should_receive(:commit)
27
+ Sunspot::IndexQueue::Entry.implementation.should_receive(:delete_entries).with([entry_1, entry_2])
28
+ subject.submit!
29
+ entry_1.processed?.should == true
30
+ entry_2.processed?.should == true
31
+ end
32
+
33
+ it "should submit all entries individually and commit them if the batch errors out" do
34
+ entry_1.stub!(:record).and_return(record_1)
35
+ session.should_receive(:batch).and_yield
36
+ session.should_receive(:index).with(record_1).twice
37
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id).twice
38
+ session.should_receive(:commit).and_raise("boom")
39
+ session.should_receive(:commit)
40
+ Sunspot::IndexQueue::Entry.implementation.should_receive(:delete_entries).with([entry_1, entry_2])
41
+ subject.submit!
42
+ entry_1.processed?.should == true
43
+ entry_2.processed?.should == true
44
+ end
45
+
46
+
47
+ it "should add error messages to each entry that errors out" do
48
+ entry_1.stub!(:record).and_return(record_1)
49
+ error = StandardError.new("boom")
50
+ session.should_receive(:batch).and_yield
51
+ session.should_receive(:index).and_raise(error)
52
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id)
53
+ session.should_receive(:commit)
54
+ entry_1.should_receive(:set_error!).with(error, queue.retry_interval)
55
+ Sunspot::IndexQueue::Entry.implementation.should_receive(:delete_entries).with([entry_2])
56
+ subject.submit!
57
+ entry_1.processed?.should == false
58
+ entry_2.processed?.should == true
59
+ end
60
+
61
+ it "should add error messages to all entries when a commit fails" do
62
+ entry_1.stub!(:record).and_return(record_1)
63
+ error = StandardError.new("boom")
64
+ session.should_receive(:batch).and_yield
65
+ session.should_receive(:index).with(record_1).twice
66
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id).twice
67
+ session.should_receive(:commit).twice.and_raise(error)
68
+ Sunspot::IndexQueue::Entry.implementation.should_not_receive(:delete_entries)
69
+ entry_1.should_receive(:set_error!).with(error, queue.retry_interval)
70
+ entry_2.should_receive(:set_error!).with(error, queue.retry_interval)
71
+ subject.submit!
72
+ entry_1.processed?.should == false
73
+ entry_2.processed?.should == false
74
+ end
75
+
76
+ it "should silently ignore entries that no longer have a record" do
77
+ entry_1.stub!(:record).and_return(nil)
78
+ session.should_receive(:batch).and_yield
79
+ session.should_not_receive(:index)
80
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id)
81
+ session.should_receive(:commit)
82
+ Sunspot::IndexQueue::Entry.implementation.should_receive(:delete_entries).with([entry_1, entry_2])
83
+ subject.submit!
84
+ entry_1.processed?.should == true
85
+ entry_2.processed?.should == true
86
+ end
87
+
88
+ it "should raise an error and reset entries if the Solr server is not responding for an entry" do
89
+ entry_1.stub!(:record).and_return(record_1)
90
+ error = Errno::ECONNREFUSED.new
91
+ session.should_receive(:batch).and_yield
92
+ session.should_receive(:index).with(record_1)
93
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id).and_raise(error)
94
+ session.should_not_receive(:commit)
95
+ Sunspot::IndexQueue::Entry.implementation.should_not_receive(:delete_entries)
96
+ entry_1.should_receive(:reset!)
97
+ entry_2.should_receive(:reset!)
98
+ lambda{subject.submit!}.should raise_error(Sunspot::IndexQueue::SolrNotResponding)
99
+ entry_1.processed?.should == false
100
+ entry_2.processed?.should == false
101
+ end
102
+
103
+ it "should raise an error and reset entries if the Solr server is not responding on a commit" do
104
+ entry_1.stub!(:record).and_return(record_1)
105
+ error = Errno::ECONNREFUSED.new
106
+ session.should_receive(:batch).and_yield
107
+ session.should_receive(:index).with(record_1)
108
+ session.should_receive(:remove_by_id).with(entry_2.record_class_name, entry_2.record_id)
109
+ session.should_receive(:commit).and_raise(error)
110
+ Sunspot::IndexQueue::Entry.implementation.should_not_receive(:delete_entries)
111
+ entry_1.should_receive(:reset!)
112
+ entry_2.should_receive(:reset!)
113
+ lambda{subject.submit!}.should raise_error(Sunspot::IndexQueue::SolrNotResponding)
114
+ entry_1.processed?.should == false
115
+ entry_2.processed?.should == false
116
+ end
117
+
118
+ end
@@ -0,0 +1,37 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'sqlite3'
3
+ require 'dm-migrations'
4
+ require File.expand_path('../entry_impl_examples', __FILE__)
5
+
6
+ describe Sunspot::IndexQueue::Entry::DataMapperImpl do
7
+
8
+ before :all do
9
+ DataMapper.setup(:default, 'sqlite::memory:')
10
+ Sunspot::IndexQueue::Entry.implementation = :data_mapper
11
+ Sunspot::IndexQueue::Entry::DataMapperImpl.auto_migrate!
12
+ end
13
+
14
+ after :all do
15
+ Sunspot::IndexQueue::Entry.implementation = nil
16
+ end
17
+
18
+ let(:factory) do
19
+ factory = Object.new
20
+ def factory.create (attributes)
21
+ Sunspot::IndexQueue::Entry::DataMapperImpl.create!(attributes)
22
+ end
23
+
24
+ def factory.delete_all
25
+ Sunspot::IndexQueue::Entry::DataMapperImpl.all.destroy!
26
+ end
27
+
28
+ def factory.find (id)
29
+ Sunspot::IndexQueue::Entry::DataMapperImpl.get(id)
30
+ end
31
+
32
+ factory
33
+ end
34
+
35
+ it_should_behave_like "Entry implementation"
36
+
37
+ end
@@ -0,0 +1,184 @@
1
+ require 'spec_helper'
2
+
3
+ # Shared examples for Entry implementations. In order to use these examples, the example group should define
4
+ # a block for :factory that will create an entry when yielded to with a hash of attributes..
5
+ shared_examples_for "Entry implementation" do
6
+
7
+ after :each do
8
+ factory.delete_all
9
+ end
10
+
11
+ context "class methods" do
12
+ before :each do
13
+ test_class = "Sunspot::IndexQueue::Test::Searchable"
14
+ @entry_1 = factory.create('record_class_name' => test_class, 'record_id' => '1', 'is_delete' => false, 'priority' => 0, 'run_at' => Time.now.utc)
15
+ @entry_2 = factory.create('record_class_name' => test_class, 'record_id' => '2', 'is_delete' => false, 'priority' => 10, 'run_at' => Time.now.utc, 'error' => "boom!", 'attempts' => 1)
16
+ @entry_3 = factory.create('record_class_name' => "Object", 'record_id' => '3', 'is_delete' => false, 'priority' => 5, 'run_at' => Time.now.utc, 'error' => "boom!", 'attempts' => 1)
17
+ @entry_4 = factory.create('record_class_name' => test_class, 'record_id' => '4', 'is_delete' => true, 'priority' => 0, 'run_at' => Time.now.utc + 60)
18
+ @entry_5 = factory.create('record_class_name' => test_class, 'record_id' => '5', 'is_delete' => false, 'priority' => -10, 'run_at' => Time.now.utc - 60)
19
+ @entry_6 = factory.create('record_class_name' => test_class, 'record_id' => '6', 'is_delete' => false, 'priority' => 0, 'run_at' => Time.now.utc - 3600)
20
+ @entry_7 = factory.create('record_class_name' => test_class, 'record_id' => '7', 'is_delete' => true, 'priority' => 10, 'run_at' => Time.now.utc - 60)
21
+ end
22
+
23
+ context "without class_names filter" do
24
+ let(:queue) { Sunspot::IndexQueue.new(:batch_size => 3, :retry_interval => 5)}
25
+
26
+ it "should get the total_count" do
27
+ Sunspot::IndexQueue::Entry.implementation.total_count(queue).should == 7
28
+ end
29
+
30
+ it "should get the ready_count" do
31
+ Sunspot::IndexQueue::Entry.implementation.ready_count(queue).should == 6
32
+ end
33
+
34
+ it "should get the error_count" do
35
+ Sunspot::IndexQueue::Entry.implementation.error_count(queue).should == 2
36
+ end
37
+
38
+ it "should get the errors" do
39
+ errors = Sunspot::IndexQueue::Entry.implementation.errors(queue, 2, 0)
40
+ errors.collect{|e| e.record_id}.sort.should == [@entry_2.record_id, @entry_3.record_id]
41
+
42
+ errors = Sunspot::IndexQueue::Entry.implementation.errors(queue, 1, 1)
43
+ errors.collect{|e| e.record_id}.sort.should == [@entry_3.record_id]
44
+ end
45
+
46
+ it "should reset all entries" do
47
+ Sunspot::IndexQueue::Entry.implementation.reset!(queue)
48
+ Sunspot::IndexQueue::Entry.implementation.error_count(queue).should == 0
49
+ @entry_2 = factory.find(@entry_2.id)
50
+ @entry_2.error.should == nil
51
+ @entry_2.attempts.should == 0
52
+
53
+ @entry_3 = factory.find(@entry_3.id)
54
+ @entry_3.error.should == nil
55
+ @entry_3.attempts.should == 0
56
+ end
57
+
58
+ it "should get the next_batch! by index time and priority" do
59
+ batch = Sunspot::IndexQueue::Entry.implementation.next_batch!(queue)
60
+ batch.collect{|e| e.record_id}.sort.should == [@entry_2.record_id, @entry_3.record_id, @entry_7.record_id]
61
+ batch = Sunspot::IndexQueue::Entry.implementation.next_batch!(queue)
62
+ batch.collect{|e| e.record_id}.sort.should == [@entry_1.record_id, @entry_5.record_id, @entry_6.record_id]
63
+ end
64
+ end
65
+
66
+ context "with class_names filter" do
67
+ let(:queue) { Sunspot::IndexQueue.new(:batch_size => 3, :retry_interval => 5, :class_names => "Sunspot::IndexQueue::Test::Searchable")}
68
+
69
+ it "should get the total_count" do
70
+ Sunspot::IndexQueue::Entry.implementation.total_count(queue).should == 6
71
+ end
72
+
73
+ it "should get the ready_count" do
74
+ Sunspot::IndexQueue::Entry.implementation.ready_count(queue).should == 5
75
+ end
76
+
77
+ it "should get the error_count" do
78
+ Sunspot::IndexQueue::Entry.implementation.error_count(queue).should == 1
79
+ end
80
+
81
+ it "should get the errors" do
82
+ errors = Sunspot::IndexQueue::Entry.implementation.errors(queue, 2, 0)
83
+ errors.collect{|e| e.record_id}.sort.should == [@entry_2.record_id]
84
+ errors = Sunspot::IndexQueue::Entry.implementation.errors(queue, 1, 1)
85
+ errors.collect{|e| e.record_id}.sort.should == []
86
+ end
87
+
88
+ it "should reset all entries" do
89
+ Sunspot::IndexQueue::Entry.implementation.reset!(queue)
90
+ Sunspot::IndexQueue::Entry.implementation.error_count(queue).should == 0
91
+ @entry_2 = factory.find(@entry_2.id)
92
+ @entry_2.error.should == nil
93
+ @entry_2.attempts.should == 0
94
+
95
+ @entry_3 = factory.find(@entry_3.id)
96
+ @entry_3.error.should_not == nil
97
+ @entry_3.attempts.should == 1
98
+ end
99
+
100
+ it "should get the next_batch! by index time and priority" do
101
+ batch = Sunspot::IndexQueue::Entry.implementation.next_batch!(queue)
102
+ batch.collect{|e| e.record_id}.sort.should == [@entry_2.record_id, @entry_6.record_id, @entry_7.record_id]
103
+ batch = Sunspot::IndexQueue::Entry.implementation.next_batch!(queue)
104
+ batch.collect{|e| e.record_id}.sort.should == [@entry_1.record_id, @entry_5.record_id]
105
+ end
106
+ end
107
+
108
+ context "add and remove" do
109
+ it "should add an entry" do
110
+ Sunspot::IndexQueue::Entry.implementation.add(Sunspot::IndexQueue::Test::Searchable, "10", false, 100)
111
+ entry = Sunspot::IndexQueue::Entry.implementation.next_batch!(Sunspot::IndexQueue.new).detect{|e| e.priority == 100}
112
+ entry.record_class_name.should == "Sunspot::IndexQueue::Test::Searchable"
113
+ entry.record_id.should == "10"
114
+ entry.is_delete?.should == false
115
+ entry.priority.should == 100
116
+ end
117
+
118
+ it "should delete a list of entry ids" do
119
+ Sunspot::IndexQueue::Entry.implementation.delete_entries([@entry_1.id, @entry_2.id])
120
+ factory.find(@entry_1.id).should == nil
121
+ factory.find(@entry_2.id).should == nil
122
+ factory.find(@entry_4.id).id.should == @entry_4.id
123
+ end
124
+
125
+ it "should not add multiple entries unless a row is being processed" do
126
+ Sunspot::IndexQueue::Entry.implementation.add(Sunspot::IndexQueue::Test::Searchable, "10", false, 80)
127
+ Sunspot::IndexQueue::Entry.implementation.next_batch!(Sunspot::IndexQueue.new)
128
+ Sunspot::IndexQueue::Entry.implementation.add(Sunspot::IndexQueue::Test::Searchable, "10", false, 100)
129
+ Sunspot::IndexQueue::Entry.implementation.add(Sunspot::IndexQueue::Test::Searchable, "10", false, 110)
130
+ Sunspot::IndexQueue::Entry.implementation.add(Sunspot::IndexQueue::Test::Searchable, "10", true, 90)
131
+ Sunspot::IndexQueue::Entry.implementation.reset!(Sunspot::IndexQueue.new)
132
+ entries = Sunspot::IndexQueue::Entry.implementation.next_batch!(Sunspot::IndexQueue.new)
133
+ entries.detect{|e| e.priority == 80}.record_id.should == "10"
134
+ entries.detect{|e| e.priority == 100}.should == nil
135
+ entries.detect{|e| e.priority == 90}.should == nil
136
+ entry = entries.detect{|e| e.priority == 110}
137
+ entry.is_delete?.should == true
138
+ end
139
+ end
140
+ end
141
+
142
+ context "instance methods" do
143
+
144
+ it "should get the record_class_name" do
145
+ entry = Sunspot::IndexQueue::Entry.implementation.new('record_class_name' => "Test")
146
+ entry.record_class_name.should == "Test"
147
+ end
148
+
149
+ it "should get the record_id" do
150
+ entry = Sunspot::IndexQueue::Entry.implementation.new('record_id' => "1")
151
+ entry.record_id.should == "1"
152
+ end
153
+
154
+ it "should determine if the entry is an delete" do
155
+ entry = Sunspot::IndexQueue::Entry.implementation.new('is_delete' => false)
156
+ entry.is_delete?.should == false
157
+ entry = Sunspot::IndexQueue::Entry.implementation.new('is_delete' => true)
158
+ entry.is_delete?.should == true
159
+ end
160
+
161
+ it "should reset an entry to be indexed immediately" do
162
+ entry = factory.create('record_class_name' => "Test", 'record_id' => 1, 'is_delete' => false, 'priority' => 10, 'run_at' => Time.now.utc + 600, 'error' => "boom!", 'attempts' => 2)
163
+ queue = Sunspot::IndexQueue.new
164
+ queue.error_count.should == 1
165
+ queue.ready_count.should == 0
166
+ entry.reset!
167
+ queue.error_count.should == 0
168
+ queue.ready_count.should == 1
169
+ factory.find(entry.id).attempts.should == 0
170
+ end
171
+
172
+ it "should set the error on an entry" do
173
+ entry = factory.create('record_class_name' => "Test", 'record_id' => 1, 'is_delete' => false, 'priority' => 10, 'run_at' => Time.now.utc + 600, 'attempts' => 1)
174
+ error = ArgumentError.new("boom")
175
+ error.stub!(:backtrace).and_return(["line 1", "line 2"])
176
+ entry.set_error!(error)
177
+ entry = factory.find(entry.id)
178
+ entry.error.should include("ArgumentError")
179
+ entry.error.should include("boom")
180
+ entry.error.should include("line 1")
181
+ entry.error.should include("line 2")
182
+ end
183
+ end
184
+ end