lifesaver 0.0.1 → 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 +1 -0
- data/.travis.yml +3 -1
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/README.md +33 -13
- data/lib/lifesaver.rb +34 -11
- data/lib/lifesaver/config.rb +16 -0
- data/lib/lifesaver/index_worker.rb +10 -10
- data/lib/lifesaver/indexing/enqueuer.rb +37 -0
- data/lib/lifesaver/indexing/indexer.rb +40 -0
- data/lib/lifesaver/indexing/model_additions.rb +55 -0
- data/lib/lifesaver/notification/eager_loader.rb +48 -0
- data/lib/lifesaver/notification/enqueuer.rb +24 -0
- data/lib/lifesaver/notification/indexing_graph.rb +91 -0
- data/lib/lifesaver/notification/model_additions.rb +104 -0
- data/lib/lifesaver/notification/notifiable_associations.rb +49 -0
- data/lib/lifesaver/notification/traversal_queue.rb +48 -0
- data/lib/lifesaver/railtie.rb +4 -3
- data/lib/lifesaver/serialized_model.rb +3 -0
- data/lib/lifesaver/version.rb +1 -1
- data/lib/lifesaver/visitor_worker.rb +8 -4
- data/spec/integration/lifesaver_spec.rb +78 -0
- data/spec/spec_helper.rb +12 -10
- data/spec/support/active_record.rb +3 -3
- data/spec/support/test_models.rb +8 -6
- data/spec/support/tire_helper.rb +37 -0
- data/spec/unit/config_spec.rb +17 -0
- data/spec/unit/indexing/indexer_spec.rb +51 -0
- data/spec/unit/indexing/model_addtions_spec.rb +53 -0
- data/spec/unit/notification/eager_loader_spec.rb +64 -0
- data/spec/unit/notification/enqueuer_spec.rb +39 -0
- data/spec/unit/notification/indexing_graph_spec.rb +73 -0
- data/spec/unit/notification/model_additions_spec.rb +69 -0
- data/spec/unit/notification/notifiable_associations_spec.rb +75 -0
- data/spec/unit/notification/traversal_queue_spec.rb +67 -0
- metadata +40 -14
- data/lib/lifesaver/index_graph.rb +0 -53
- data/lib/lifesaver/marshal.rb +0 -43
- data/lib/lifesaver/model_additions.rb +0 -133
- data/spec/lifesaver/index_graph_spec.rb +0 -64
- data/spec/lifesaver/marshal_spec.rb +0 -80
- data/spec/lifesaver/model_additions_spec.rb +0 -91
- data/spec/lifesaver_spec.rb +0 -111
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Config do
|
4
|
+
describe '#initialize' do
|
5
|
+
context 'with no options' do
|
6
|
+
let(:config) { Lifesaver::Config.new }
|
7
|
+
|
8
|
+
it 'has the default indexing_queue' do
|
9
|
+
expect(config.indexing_queue).to eql(:lifesaver_indexing)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'has the default notification_queue' do
|
13
|
+
expect(config.notification_queue).to eql(:lifesaver_notification)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Indexing::Indexer do
|
4
|
+
context '#perform' do
|
5
|
+
let(:index) { double('index', remove: nil, store: nil) }
|
6
|
+
let(:index_name) { 'test-index-name' }
|
7
|
+
before do
|
8
|
+
Model.stub(:index_name).and_return(index_name)
|
9
|
+
Tire.stub(:index).with(index_name).and_return(index)
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'when operation is update' do
|
13
|
+
let(:model) { Model.new(1) }
|
14
|
+
before do
|
15
|
+
Model.stub(:exists?).with('1').and_return(true)
|
16
|
+
Model.stub(:find).with('1').and_return(model)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'calls tire with the correct arguments' do
|
20
|
+
indexer = Lifesaver::Indexing::Indexer.new(
|
21
|
+
class_name: 'model',
|
22
|
+
model_id: '1',
|
23
|
+
operation: 'update'
|
24
|
+
)
|
25
|
+
|
26
|
+
expect(index).to receive(:store).with(model)
|
27
|
+
|
28
|
+
indexer.perform
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when operation is destroy' do
|
33
|
+
before do
|
34
|
+
Model.stub(:document_type).and_return('class')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'calls tire with the correct arguments' do
|
38
|
+
indexer = Lifesaver::Indexing::Indexer.new(
|
39
|
+
class_name: 'model',
|
40
|
+
model_id: '1',
|
41
|
+
operation: 'destroy'
|
42
|
+
)
|
43
|
+
correct_arguments = { type: 'class', id: '1' }
|
44
|
+
|
45
|
+
expect(index).to receive(:remove).with(correct_arguments)
|
46
|
+
|
47
|
+
indexer.perform
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Indexing::ModelAdditions do
|
4
|
+
let(:post) { Post.new(title: 'Test Post') }
|
5
|
+
|
6
|
+
describe '.enqueues_indexing' do
|
7
|
+
before do
|
8
|
+
post.stub(:enqueue_indexing)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'calls enqueue_indexing on save' do
|
12
|
+
expect(post).to receive(:enqueue_indexing)
|
13
|
+
|
14
|
+
post.save!
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'calls enqueue_indexing on destroy' do
|
18
|
+
expect(post).to receive(:enqueue_indexing)
|
19
|
+
|
20
|
+
post.destroy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#indexing_suppressed?' do
|
25
|
+
it 'is false by default' do
|
26
|
+
expect(post.send(:suppress_indexing?)).to eql(false)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'is true if overridden locally' do
|
30
|
+
Lifesaver.suppress_indexing
|
31
|
+
|
32
|
+
expect(post.send(:suppress_indexing?)).to eql(true)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'is false if override is cancelled' do
|
36
|
+
Lifesaver.unsuppress_indexing
|
37
|
+
|
38
|
+
expect(post.send(:suppress_indexing?)).to eql(false)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'is true if set individually' do
|
42
|
+
post.suppress_indexing
|
43
|
+
|
44
|
+
expect(post.send(:suppress_indexing?)).to eql(true)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'is false if unset individually' do
|
48
|
+
post.unsuppress_indexing
|
49
|
+
|
50
|
+
expect(post.send(:suppress_indexing?)).to eql(false)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Notification::EagerLoader do
|
4
|
+
|
5
|
+
let(:eager_loader) { Lifesaver::Notification::EagerLoader.new }
|
6
|
+
|
7
|
+
describe '#add_model' do
|
8
|
+
|
9
|
+
it 'adds when empty' do
|
10
|
+
eager_loader.add_model('Post', 34)
|
11
|
+
expected_result = { 'Post' => [34] }
|
12
|
+
|
13
|
+
expect(eager_loader.send(:models_to_load)).to eql(expected_result)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'adds to when other model ids exist' do
|
17
|
+
eager_loader.add_model('Post', 34)
|
18
|
+
eager_loader.add_model('Post', 38)
|
19
|
+
eager_loader.add_model('Article', 3)
|
20
|
+
expected_result = { 'Post' => [34, 38], 'Article' => [3] }
|
21
|
+
|
22
|
+
expect(eager_loader.send(:models_to_load)).to eql(expected_result)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'does not add a previously added model' do
|
26
|
+
eager_loader.add_model('Post', 38)
|
27
|
+
eager_loader.add_model('Post', 38)
|
28
|
+
expected_result = { 'Post' => [38] }
|
29
|
+
|
30
|
+
expect(eager_loader.send(:models_to_load)).to eql(expected_result)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#load' do
|
36
|
+
before do
|
37
|
+
Post.stub(:load_with_notifiable_associations).and_return([])
|
38
|
+
eager_loader.add_model('Post', 34)
|
39
|
+
eager_loader.add_model('Post', 38)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'empties the models_to_load' do
|
43
|
+
eager_loader.load
|
44
|
+
|
45
|
+
expect(eager_loader.send(:models_to_load)).to be_empty
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'returns an array' do
|
49
|
+
expect(eager_loader.load).to eq([])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#empty?' do
|
54
|
+
it 'is true when empty' do
|
55
|
+
expect(eager_loader.empty?).to be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'is false when not empty' do
|
59
|
+
eager_loader.add_model('Post', 38)
|
60
|
+
|
61
|
+
expect(eager_loader.empty?).to be_false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Notification::Enqueuer do
|
4
|
+
let(:models) { [Lifesaver::SerializedModel.new('TestClass', '3')] }
|
5
|
+
let(:enqueuer) { Lifesaver::Notification::Enqueuer.new(models) }
|
6
|
+
before { ::Resque.stub(:enqueue) }
|
7
|
+
|
8
|
+
describe '#enqueue' do
|
9
|
+
context 'when indexing is not suppressed' do
|
10
|
+
context 'and there are models' do
|
11
|
+
it 'calls Resque.enqueue with the correct parameters' do
|
12
|
+
expect(::Resque).to receive(:enqueue)
|
13
|
+
.with(Lifesaver::VisitorWorker, models)
|
14
|
+
|
15
|
+
enqueuer.enqueue
|
16
|
+
end
|
17
|
+
end
|
18
|
+
context 'and there are no models' do
|
19
|
+
it 'does not call Resque.enqueue' do
|
20
|
+
empty_enqueuer = Lifesaver::Notification::Enqueuer.new([])
|
21
|
+
|
22
|
+
expect(::Resque).to_not receive(:enqueue)
|
23
|
+
|
24
|
+
empty_enqueuer.enqueue
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when indexing is suppressed' do
|
30
|
+
it 'does not call Resque.enqueue' do
|
31
|
+
Lifesaver.suppress_indexing
|
32
|
+
|
33
|
+
expect(::Resque).to_not receive(:enqueue)
|
34
|
+
|
35
|
+
enqueuer.enqueue
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Notification::IndexingGraph do
|
4
|
+
let(:indexing_graph) { Lifesaver::Notification::IndexingGraph.new }
|
5
|
+
|
6
|
+
describe '#initalize_models' do
|
7
|
+
it 'should load serialized models into the loader' do
|
8
|
+
indexing_graph.stub(:add_model_to_loader)
|
9
|
+
serialized_models = []
|
10
|
+
serialized_models << Lifesaver::SerializedModel.new('Post', 14)
|
11
|
+
|
12
|
+
expect(indexing_graph).to receive(:add_model_to_loader).with('Post', 14)
|
13
|
+
|
14
|
+
indexing_graph.initialize_models(serialized_models)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#generate' do
|
19
|
+
context 'when queue is empty' do
|
20
|
+
before do
|
21
|
+
indexing_graph.stub(:queue_full?).and_return(false)
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'and loader is empty' do
|
25
|
+
it 'exits and returns models_to_index' do
|
26
|
+
indexing_graph.stub(:loader_full?).and_return(false)
|
27
|
+
indexing_graph.stub(:models_to_index).and_return([Model.new(15)])
|
28
|
+
|
29
|
+
expect(indexing_graph.generate).to eql([Model.new(15)])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'and loader is not empty' do
|
34
|
+
it 'calls load_into_queue' do
|
35
|
+
indexing_graph.stub(:loader_full?).and_return(true, false)
|
36
|
+
indexing_graph.stub(:load_into_queue)
|
37
|
+
|
38
|
+
expect(indexing_graph).to receive(:load_into_queue)
|
39
|
+
|
40
|
+
indexing_graph.generate
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when queue is not empty' do
|
46
|
+
before do
|
47
|
+
indexing_graph.stub(:queue_full?).and_return(true, false)
|
48
|
+
indexing_graph.stub(:pop_model).and_return(Model.new(1))
|
49
|
+
indexing_graph.stub(:add_unvisited_associations)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'adds model if should be indexed' do
|
53
|
+
indexing_graph.stub(:model_should_be_indexed?).and_return(true)
|
54
|
+
|
55
|
+
expect(indexing_graph.generate).to eql([Model.new(1)])
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'does not add a model if it should not be indexed' do
|
59
|
+
indexing_graph.stub(:model_should_be_indexed?).and_return(false)
|
60
|
+
|
61
|
+
expect(indexing_graph.generate).to eql([])
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'adds unvisited associations to the graph' do
|
65
|
+
indexing_graph.stub(:model_should_be_indexed?).and_return(false)
|
66
|
+
|
67
|
+
expect(indexing_graph).to receive(:add_unvisited_associations)
|
68
|
+
|
69
|
+
indexing_graph.generate
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Notification::ModelAdditions do
|
4
|
+
let(:post) { Post.create!(title: 'Test', content: 'Lorem', tags: %w(tests)) }
|
5
|
+
let(:affiliate) { Affiliate.create(name: 'Some place') }
|
6
|
+
let(:author) { Author.create!(name: 'Paul Sorensen', affiliate: affiliate) }
|
7
|
+
let(:authorship) { Authorship.create!(post: post, author: author) }
|
8
|
+
before do
|
9
|
+
post.reload
|
10
|
+
author.reload
|
11
|
+
authorship.reload
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '.notifies_for_indexing' do
|
15
|
+
before do
|
16
|
+
post.stub(:update_associations)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'calls update_associations on save' do
|
20
|
+
expect(post).to receive(:update_associations)
|
21
|
+
|
22
|
+
post.save!
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'calls update_associations on destroy' do
|
26
|
+
expect(post).to receive(:update_associations)
|
27
|
+
|
28
|
+
post.destroy
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '.load_with_notifiable_associations' do
|
33
|
+
it 'returns the correct eager-loaded models' do
|
34
|
+
model = Authorship.load_with_notifiable_associations(authorship.id).first
|
35
|
+
|
36
|
+
expect(model.association(:author).loaded?).to be_true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#associations_to_notify' do
|
41
|
+
it 'returns the correct models' do
|
42
|
+
expect(author.associations_to_notify).to eql([authorship])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#needs_to_notify?' do
|
47
|
+
it 'is false if there are no notifiable associations' do
|
48
|
+
expect(post.needs_to_notify?).to be_false
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'is true if there are notifiable associations' do
|
52
|
+
expect(author.needs_to_notify?).to be_true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#models_for_association' do
|
57
|
+
it 'should return an array of models for multiple association' do
|
58
|
+
models = post.models_for_association(:authorships)
|
59
|
+
|
60
|
+
expect(models.first).to be_a_kind_of(Authorship)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should return an array of one model for a singular association' do
|
64
|
+
models = author.models_for_association(:affiliate)
|
65
|
+
|
66
|
+
expect(models.first).to be_a_kind_of(Affiliate)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lifesaver::Notification::NotifiableAssociations do
|
4
|
+
let(:notifiable_associations) do
|
5
|
+
Lifesaver::Notification::NotifiableAssociations.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#populate' do
|
9
|
+
it 'mirrors arguments for both keys' do
|
10
|
+
notifiable_associations.populate(:foo)
|
11
|
+
association_keys = notifiable_associations.send(:association_keys)
|
12
|
+
|
13
|
+
expect(association_keys.on_change).to eql(association_keys.on_notify)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'accepts arguments that are singular' do
|
17
|
+
notifiable_associations.populate(:foo)
|
18
|
+
association_keys = notifiable_associations.send(:association_keys)
|
19
|
+
|
20
|
+
expect(association_keys.on_change).to eql([:foo])
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'accepts arguments that are arrays' do
|
24
|
+
notifiable_associations.populate([:foo, :bar])
|
25
|
+
association_keys = notifiable_associations.send(:association_keys)
|
26
|
+
|
27
|
+
expect(association_keys.on_notify).to eql([:foo, :bar])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'accepts an options hash for :only_on_notify' do
|
31
|
+
notifiable_associations.populate(:foo, only_on_notify: [:baz])
|
32
|
+
association_keys = notifiable_associations.send(:association_keys)
|
33
|
+
|
34
|
+
expect(association_keys.on_notify).to eql([:foo, :baz])
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'accepts an options hash for :only_on_change' do
|
38
|
+
notifiable_associations.populate(:foo, only_on_change: [:baz])
|
39
|
+
association_keys = notifiable_associations.send(:association_keys)
|
40
|
+
|
41
|
+
expect(association_keys.on_change).to eql([:foo, :baz])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#any_to_notify?' do
|
46
|
+
it 'is true if the :on_notify array is not empty' do
|
47
|
+
notifiable_associations.populate([], only_on_notify: [:baz])
|
48
|
+
|
49
|
+
expect(notifiable_associations.any_to_notify?).to be_true
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'is false if the :on_notify array is empty' do
|
53
|
+
notifiable_associations.populate([], only_on_change: [:baz])
|
54
|
+
|
55
|
+
expect(notifiable_associations.any_to_notify?).to be_false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#on_notify' do
|
60
|
+
it 'reads the :on_notify key' do
|
61
|
+
notifiable_associations.populate([], only_on_notify: [:baz])
|
62
|
+
keys = notifiable_associations.send(:association_keys)
|
63
|
+
|
64
|
+
expect(notifiable_associations.on_notify).to eql(keys.on_notify)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
describe '#on_change' do
|
68
|
+
it 'reads the :on_change key' do
|
69
|
+
notifiable_associations.populate([], only_on_change: [:baz])
|
70
|
+
keys = notifiable_associations.send(:association_keys)
|
71
|
+
|
72
|
+
expect(notifiable_associations.on_change).to eql(keys.on_change)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|