sunspot_index_queue 1.0.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.
@@ -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