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
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
|
[](https://travis-ci.org/paulnsorensen/lifesaver)
|
4
|
+
[](http://badge.fury.io/rb/lifesaver)
|
4
5
|
[](https://gemnasium.com/paulnsorensen/lifesaver)
|
5
6
|
[](https://coveralls.io/r/paulnsorensen/lifesaver)
|
6
7
|
[](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
|
+
[](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
|