lifesaver 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
# Lifesaver
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/paulnsorensen/lifesaver.png?branch=master)](https://travis-ci.org/paulnsorensen/lifesaver)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/lifesaver.png)](http://badge.fury.io/rb/lifesaver)
|
4
5
|
[![Dependency Status](https://gemnasium.com/paulnsorensen/lifesaver.png)](https://gemnasium.com/paulnsorensen/lifesaver)
|
5
6
|
[![Coverage Status](https://coveralls.io/repos/paulnsorensen/lifesaver/badge.png)](https://coveralls.io/r/paulnsorensen/lifesaver)
|
6
7
|
[![Code Climate](https://codeclimate.com/github/paulnsorensen/lifesaver.png)](https://codeclimate.com/github/paulnsorensen/lifesaver)
|
7
8
|
|
8
|
-
|
9
|
+
Asynchronously sends your ActiveRecord models for reindexing in [elasticsearch](https://github.com/elasticsearch/elasticsearch) by making use of [tire](https://github.com/karmi/tire) and [resque](https://github.com/resque/resque) (hence the name: resque + tire = lifesaver). Lifesaver also provides the ability to traverse ActiveRecord associations to trigger the index updates of related models.
|
9
10
|
|
10
11
|
## Installation
|
11
12
|
|
12
13
|
Add this line to your application's Gemfile:
|
13
14
|
|
14
|
-
gem 'lifesaver'
|
15
|
+
gem 'lifesaver'
|
15
16
|
|
16
17
|
And then execute:
|
17
18
|
|
@@ -34,8 +35,8 @@ Replaces the tire callbacks in your models
|
|
34
35
|
end
|
35
36
|
```
|
36
37
|
|
37
|
-
####
|
38
|
-
You can
|
38
|
+
#### Configuring Indexing Behavior
|
39
|
+
You can suppress index updates on a per-model basis or globally using `Lifesaver.suppress_indexing` (to turn suppression back off, you would use `Lifesaver.unsuppress_indexing`). Lifesaver exposes two instance methods on the model level (`supress_indexing`, `unsuppress_indexing`) that set a model's indexing behavior for life of that instance.
|
39
40
|
|
40
41
|
```ruby
|
41
42
|
class ArticlesController < ApplicationController
|
@@ -47,7 +48,7 @@ You can decided when or if the index gets updated at all based on your current s
|
|
47
48
|
@article.suppress_indexing
|
48
49
|
|
49
50
|
@article.save!
|
50
|
-
|
51
|
+
|
51
52
|
# Not neccessary but if saved
|
52
53
|
# after this following call,
|
53
54
|
# this article would reindex
|
@@ -72,8 +73,30 @@ Lifesaver can trigger other models to reindex if you have nested models in your
|
|
72
73
|
end
|
73
74
|
```
|
74
75
|
|
76
|
+
## Integration with Tire
|
77
|
+
Lifesaver will not execute any `<after|before>_update_elasticsearch_index` callback hooks. Lifesaver also does not currently support percolation.
|
78
|
+
|
75
79
|
## Integration with Resque
|
76
|
-
You will see two new queues: `lifesaver_indexing` and `lifesaver_notification
|
80
|
+
You will see two new queues: `lifesaver_indexing` and `lifesaver_notification`. The queue names are configurable.
|
81
|
+
|
82
|
+
## Testing
|
83
|
+
|
84
|
+
In your spec_helper, you should place something similar to the following to make sure Lifesaver isn't spawning up indexing jobs unless you want it to.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
config.before(:each) do
|
88
|
+
Lifesaver.suppress_indexing
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
Then, when your tests need Lifesaver to run, you should make sure you unsuppress indexing in a `before` block. You may also want to run [Resque inline](http://robots.thoughtbot.com/process-jobs-inline-when-running-acceptance-tests).
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
describe 'some test' do
|
96
|
+
before { Lifesaver.unsuppress_indexing }
|
97
|
+
# tests go here
|
98
|
+
end
|
99
|
+
```
|
77
100
|
|
78
101
|
## Contributing
|
79
102
|
|
@@ -84,10 +107,7 @@ You will see two new queues: `lifesaver_indexing` and `lifesaver_notification`
|
|
84
107
|
5. Create new Pull Request
|
85
108
|
|
86
109
|
## TODO
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
+ unsuppress indexing after save
|
92
|
-
+ sidekiq support
|
93
|
-
+ prepare for new elasticsearch library
|
110
|
+
Please visit TODO page [here](https://github.com/paulnsorensen/lifesaver/wiki/TODO)
|
111
|
+
|
112
|
+
|
113
|
+
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/paulnsorensen/lifesaver/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
|
data/lib/lifesaver.rb
CHANGED
@@ -1,24 +1,47 @@
|
|
1
1
|
require 'resque-loner'
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
2
|
+
require 'lifesaver/version'
|
3
|
+
require 'lifesaver/config'
|
4
|
+
require 'lifesaver/serialized_model'
|
5
|
+
require 'lifesaver/indexing/model_additions'
|
6
|
+
require 'lifesaver/indexing/enqueuer'
|
7
|
+
require 'lifesaver/indexing/indexer'
|
8
|
+
require 'lifesaver/notification/notifiable_associations'
|
9
|
+
require 'lifesaver/notification/model_additions'
|
10
|
+
require 'lifesaver/notification/eager_loader'
|
11
|
+
require 'lifesaver/notification/traversal_queue'
|
12
|
+
require 'lifesaver/notification/indexing_graph'
|
13
|
+
require 'lifesaver/notification/enqueuer'
|
14
|
+
require 'lifesaver/index_worker'
|
15
|
+
require 'lifesaver/visitor_worker'
|
16
|
+
require 'lifesaver/railtie' if defined? Rails
|
9
17
|
|
10
18
|
module Lifesaver
|
19
|
+
extend self
|
20
|
+
|
11
21
|
@@suppress_indexing = false
|
12
22
|
|
13
|
-
def
|
23
|
+
def suppress_indexing
|
14
24
|
@@suppress_indexing = true
|
15
25
|
end
|
16
26
|
|
17
|
-
def
|
27
|
+
def unsuppress_indexing
|
18
28
|
@@suppress_indexing = false
|
19
29
|
end
|
20
30
|
|
21
|
-
def
|
31
|
+
def indexing_suppressed?
|
22
32
|
@@suppress_indexing
|
23
33
|
end
|
24
|
-
|
34
|
+
|
35
|
+
def config=(options = {})
|
36
|
+
@config = Config.new(options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def config
|
40
|
+
@config ||= Config.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def configure
|
44
|
+
yield config
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
# A container for configuration parameters
|
3
|
+
class Config
|
4
|
+
attr_accessor :notification_queue, :indexing_queue
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
|
8
|
+
@notification_queue = :lifesaver_notification
|
9
|
+
@indexing_queue = :lifesaver_indexing
|
10
|
+
|
11
|
+
options.each do |key, value|
|
12
|
+
public_send("#{key}=", value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
class Lifesaver::IndexWorker
|
2
2
|
include ::Resque::Plugins::UniqueJob
|
3
|
-
|
4
|
-
def self.
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
|
4
|
+
def self.queue; Lifesaver.config.indexing_queue end
|
5
|
+
|
6
|
+
def self.perform(class_name, model_id, operation)
|
7
|
+
Lifesaver::Indexing::Indexer.new(
|
8
|
+
class_name: class_name,
|
9
|
+
model_id: model_id,
|
10
|
+
operation: operation
|
11
|
+
).perform
|
12
12
|
end
|
13
|
-
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module Indexing
|
3
|
+
class Enqueuer
|
4
|
+
def initialize(args)
|
5
|
+
@model = args[:model]
|
6
|
+
@operation = args[:operation]
|
7
|
+
end
|
8
|
+
|
9
|
+
def enqueue
|
10
|
+
if should_enqueue?(model)
|
11
|
+
::Resque.enqueue(
|
12
|
+
Lifesaver::IndexWorker,
|
13
|
+
class_name,
|
14
|
+
model_id,
|
15
|
+
operation
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :model, :operation
|
23
|
+
|
24
|
+
def should_enqueue?(model)
|
25
|
+
model.should_index?
|
26
|
+
end
|
27
|
+
|
28
|
+
def class_name
|
29
|
+
model.class.name.underscore.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def model_id
|
33
|
+
model.id
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module Indexing
|
3
|
+
class Indexer
|
4
|
+
def initialize(args)
|
5
|
+
@class_name = args[:class_name]
|
6
|
+
@model_id = args[:model_id]
|
7
|
+
@operation = args[:operation].to_sym
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
case operation
|
12
|
+
when :update
|
13
|
+
store
|
14
|
+
when :destroy
|
15
|
+
remove
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :class_name, :model_id, :operation
|
22
|
+
|
23
|
+
def index
|
24
|
+
@index ||= Tire.index(klass.index_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def klass
|
28
|
+
@klass ||= class_name.to_s.classify.constantize
|
29
|
+
end
|
30
|
+
|
31
|
+
def store
|
32
|
+
index.store(klass.find(model_id)) if klass.exists?(model_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove
|
36
|
+
index.remove(type: klass.document_type, id: model_id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module Indexing
|
3
|
+
module ModelAdditions
|
4
|
+
module ClassMethods
|
5
|
+
def enqueues_indexing
|
6
|
+
indexing_callbacks
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def indexing_callbacks(options = {})
|
12
|
+
# after_commit?
|
13
|
+
after_save do
|
14
|
+
send :enqueue_indexing, options.merge(operation: :update)
|
15
|
+
end
|
16
|
+
after_destroy do
|
17
|
+
send :enqueue_indexing, options.merge(operation: :destroy)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.extend(ClassMethods)
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_index?
|
27
|
+
self.respond_to?(:tire)
|
28
|
+
end
|
29
|
+
|
30
|
+
def should_index?
|
31
|
+
has_index? && !suppress_indexing?
|
32
|
+
end
|
33
|
+
|
34
|
+
def suppress_indexing
|
35
|
+
@indexing_suppressed = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def unsuppress_indexing
|
39
|
+
@indexing_suppressed = false
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def enqueue_indexing(options)
|
45
|
+
operation = options[:operation]
|
46
|
+
Lifesaver::Indexing::Enqueuer.new(model: self,
|
47
|
+
operation: operation).enqueue
|
48
|
+
end
|
49
|
+
|
50
|
+
def suppress_indexing?
|
51
|
+
Lifesaver.indexing_suppressed? || @indexing_suppressed || false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module Notification
|
3
|
+
class EagerLoader
|
4
|
+
def initialize
|
5
|
+
@models_to_load = {}
|
6
|
+
@loaded_models = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_model(class_name, id)
|
10
|
+
return if model_previously_added?(class_name, id)
|
11
|
+
models_to_load[class_name] ||= []
|
12
|
+
models_to_load[class_name] << id
|
13
|
+
mark_model_added(class_name, id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def load
|
17
|
+
models = []
|
18
|
+
models_to_load.each do |class_name, ids|
|
19
|
+
klass = class_name.classify.constantize
|
20
|
+
models |= load_associations(klass, ids)
|
21
|
+
models_to_load.delete(class_name)
|
22
|
+
end
|
23
|
+
models
|
24
|
+
end
|
25
|
+
|
26
|
+
def empty?
|
27
|
+
@models_to_load.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_accessor :models_to_load, :loaded_models
|
33
|
+
|
34
|
+
def load_associations(klass, ids)
|
35
|
+
klass.load_with_notifiable_associations(ids)
|
36
|
+
end
|
37
|
+
|
38
|
+
def model_previously_added?(class_name, id)
|
39
|
+
loaded_models[class_name] && loaded_models[class_name][id]
|
40
|
+
end
|
41
|
+
|
42
|
+
def mark_model_added(class_name, id)
|
43
|
+
loaded_models[class_name] ||= {}
|
44
|
+
loaded_models[class_name][id] = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module Notification
|
3
|
+
class Enqueuer
|
4
|
+
def initialize(models)
|
5
|
+
@serialized_models = models
|
6
|
+
end
|
7
|
+
|
8
|
+
def enqueue
|
9
|
+
if should_enqueue?
|
10
|
+
::Resque.enqueue(Lifesaver::VisitorWorker, serialized_models)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_accessor :serialized_models
|
17
|
+
|
18
|
+
def should_enqueue?
|
19
|
+
!serialized_models.empty? && !Lifesaver.indexing_suppressed?
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Lifesaver
|
2
|
+
module Notification
|
3
|
+
class IndexingGraph
|
4
|
+
def initialize
|
5
|
+
@queue = Lifesaver::Notification::TraversalQueue.new
|
6
|
+
@loader = Lifesaver::Notification::EagerLoader.new
|
7
|
+
@models_to_index = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize_models(serialized_models)
|
11
|
+
|
12
|
+
serialized_models.each do |model_hash|
|
13
|
+
model = Lifesaver::SerializedModel.new
|
14
|
+
model.class_name = model_hash['class_name']
|
15
|
+
model.id = model_hash['id']
|
16
|
+
add_model_to_loader(model.class_name, model.id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate
|
21
|
+
loop do
|
22
|
+
if queue_full?
|
23
|
+
model = pop_model
|
24
|
+
models_to_index << model if model_should_be_indexed?(model)
|
25
|
+
add_unvisited_associations(model)
|
26
|
+
elsif loader_full?
|
27
|
+
load_into_queue
|
28
|
+
else
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
models_to_index
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :queue, :loader, :models_to_index
|
38
|
+
|
39
|
+
def loader_full?
|
40
|
+
!loader.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def queue_full?
|
44
|
+
!queue.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def pop_model
|
48
|
+
queue.pop
|
49
|
+
end
|
50
|
+
|
51
|
+
def push_model(model)
|
52
|
+
queue << model
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_model_to_loader(class_name, id)
|
56
|
+
loader.add_model(class_name, id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_models
|
60
|
+
loader.load
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_into_queue
|
64
|
+
load_models.each { |model| queue << model }
|
65
|
+
end
|
66
|
+
|
67
|
+
def model_should_be_indexed?(model)
|
68
|
+
model.has_index?
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_associations_for_model(model)
|
72
|
+
model.associations_to_notify
|
73
|
+
end
|
74
|
+
|
75
|
+
def model_needs_to_notify?(model)
|
76
|
+
model.needs_to_notify?
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_unvisited_associations(model)
|
80
|
+
models = load_associations_for_model(model)
|
81
|
+
models.each do |m|
|
82
|
+
if model_needs_to_notify?(model)
|
83
|
+
add_model_to_loader(m.class.name, m.id)
|
84
|
+
else
|
85
|
+
push_model(model)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|