lifesaver 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.rspec +1 -0
  2. data/.travis.yml +3 -1
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +4 -0
  5. data/README.md +33 -13
  6. data/lib/lifesaver.rb +34 -11
  7. data/lib/lifesaver/config.rb +16 -0
  8. data/lib/lifesaver/index_worker.rb +10 -10
  9. data/lib/lifesaver/indexing/enqueuer.rb +37 -0
  10. data/lib/lifesaver/indexing/indexer.rb +40 -0
  11. data/lib/lifesaver/indexing/model_additions.rb +55 -0
  12. data/lib/lifesaver/notification/eager_loader.rb +48 -0
  13. data/lib/lifesaver/notification/enqueuer.rb +24 -0
  14. data/lib/lifesaver/notification/indexing_graph.rb +91 -0
  15. data/lib/lifesaver/notification/model_additions.rb +104 -0
  16. data/lib/lifesaver/notification/notifiable_associations.rb +49 -0
  17. data/lib/lifesaver/notification/traversal_queue.rb +48 -0
  18. data/lib/lifesaver/railtie.rb +4 -3
  19. data/lib/lifesaver/serialized_model.rb +3 -0
  20. data/lib/lifesaver/version.rb +1 -1
  21. data/lib/lifesaver/visitor_worker.rb +8 -4
  22. data/spec/integration/lifesaver_spec.rb +78 -0
  23. data/spec/spec_helper.rb +12 -10
  24. data/spec/support/active_record.rb +3 -3
  25. data/spec/support/test_models.rb +8 -6
  26. data/spec/support/tire_helper.rb +37 -0
  27. data/spec/unit/config_spec.rb +17 -0
  28. data/spec/unit/indexing/indexer_spec.rb +51 -0
  29. data/spec/unit/indexing/model_addtions_spec.rb +53 -0
  30. data/spec/unit/notification/eager_loader_spec.rb +64 -0
  31. data/spec/unit/notification/enqueuer_spec.rb +39 -0
  32. data/spec/unit/notification/indexing_graph_spec.rb +73 -0
  33. data/spec/unit/notification/model_additions_spec.rb +69 -0
  34. data/spec/unit/notification/notifiable_associations_spec.rb +75 -0
  35. data/spec/unit/notification/traversal_queue_spec.rb +67 -0
  36. metadata +40 -14
  37. data/lib/lifesaver/index_graph.rb +0 -53
  38. data/lib/lifesaver/marshal.rb +0 -43
  39. data/lib/lifesaver/model_additions.rb +0 -133
  40. data/spec/lifesaver/index_graph_spec.rb +0 -64
  41. data/spec/lifesaver/marshal_spec.rb +0 -80
  42. data/spec/lifesaver/model_additions_spec.rb +0 -91
  43. data/spec/lifesaver_spec.rb +0 -111
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml CHANGED
@@ -4,4 +4,6 @@ rvm:
4
4
  - 2.0.0
5
5
  services:
6
6
  - redis-server
7
- - elasticsearch
7
+ - elasticsearch
8
+ env:
9
+ - RUN_COVERALLS=true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## v0.1.0
2
+
3
+ * Added config
4
+ * Decoupled Indexing and Notification
5
+ * Notification uses eager loading to avoid N+1 queries during graph traversal
6
+
7
+
1
8
  ## v0.0.1
2
9
 
3
10
  * initial release
data/Gemfile CHANGED
@@ -6,3 +6,7 @@ gemspec
6
6
  group :test do
7
7
  gem 'coveralls', require: false
8
8
  end
9
+
10
+ group :development, :test do
11
+ gem 'pry', require: false
12
+ end
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
- Indexes your ActiveRecord models in [elasticsearch](https://github.com/elasticsearch/elasticsearch) asynchronously by making use of [tire](https://github.com/karmi/tire) and [resque](https://github.com/resque/resque) (hence the name: resque + tire = lifesaver). Using lifesaver, you can easily control when or if to reindex your model depending on your context. Lifesaver also provides the ability to traverse ActiveRecord associations to trigger the index updates of related models.
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', git: "git://github.com/paulnsorensen/lifesaver.git"
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
- #### Configurable Behavior
38
- You can decided when or if the index gets updated at all based on your current situation. Lifesaver exposes two methods (`supress_indexing`, `unsuppress_indexing`) that set a model's indexing behavior until that model is saved.
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
- + specify which fields will trigger indexing changes
88
- + configuration options
89
- + bulk indexing
90
- + resque-scheduler to provide `delay_indexing` and `enqueues_indexing after: 30.minutes, on: :save`
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 "lifesaver/version"
3
- require "lifesaver/marshal"
4
- require "lifesaver/index_graph"
5
- require "lifesaver/model_additions"
6
- require "lifesaver/index_worker"
7
- require "lifesaver/visitor_worker"
8
- require "lifesaver/railtie" if defined? Rails
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 self.suppress_indexing
23
+ def suppress_indexing
14
24
  @@suppress_indexing = true
15
25
  end
16
26
 
17
- def self.unsuppress_indexing
27
+ def unsuppress_indexing
18
28
  @@suppress_indexing = false
19
29
  end
20
30
 
21
- def self.indexing_suppressed?
31
+ def indexing_suppressed?
22
32
  @@suppress_indexing
23
33
  end
24
- end
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
- @queue = :lifesaver_indexing
4
- def self.perform(class_name, id, action)
5
- klass = class_name.to_s.classify.constantize
6
- case action.to_sym
7
- when :update
8
- klass.find(id).update_index if klass.exists?(id)
9
- when :destroy
10
- klass.index.remove({type: klass.document_type, id: id})
11
- end
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