tantot 0.1.5 → 0.1.6

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tantot.rb +7 -23
  3. data/lib/tantot/agent.rb +19 -0
  4. data/lib/tantot/agent/base.rb +71 -0
  5. data/lib/tantot/agent/block.rb +32 -0
  6. data/lib/tantot/agent/registry.rb +34 -0
  7. data/lib/tantot/agent/watcher.rb +46 -0
  8. data/lib/tantot/changes.rb +2 -3
  9. data/lib/tantot/config.rb +2 -2
  10. data/lib/tantot/errors.rb +6 -0
  11. data/lib/tantot/extensions/chewy.rb +66 -18
  12. data/lib/tantot/extensions/grape/middleware.rb +1 -1
  13. data/lib/tantot/manager.rb +31 -0
  14. data/lib/tantot/observe.rb +36 -31
  15. data/lib/tantot/railtie.rb +5 -0
  16. data/lib/tantot/strategy.rb +24 -0
  17. data/lib/tantot/{performer → strategy}/bypass.rb +2 -2
  18. data/lib/tantot/strategy/chewy.rb +33 -0
  19. data/lib/tantot/strategy/inline.rb +9 -0
  20. data/lib/tantot/strategy/sidekiq.rb +36 -0
  21. data/lib/tantot/version.rb +1 -1
  22. data/performance/profile.rb +12 -8
  23. data/spec/collector/block_spec.rb +33 -0
  24. data/spec/collector/options_spec.rb +211 -0
  25. data/spec/collector/watcher_spec.rb +180 -0
  26. data/spec/extensions/chewy_spec.rb +280 -78
  27. data/spec/sidekiq_spec.rb +38 -58
  28. data/spec/spec_helper.rb +27 -2
  29. data/spec/tantot_spec.rb +0 -370
  30. metadata +19 -15
  31. data/lib/tantot/collector.rb +0 -70
  32. data/lib/tantot/collector/base.rb +0 -46
  33. data/lib/tantot/collector/block.rb +0 -69
  34. data/lib/tantot/collector/watcher.rb +0 -67
  35. data/lib/tantot/formatter.rb +0 -10
  36. data/lib/tantot/formatter/compact.rb +0 -9
  37. data/lib/tantot/formatter/detailed.rb +0 -9
  38. data/lib/tantot/performer.rb +0 -24
  39. data/lib/tantot/performer/chewy.rb +0 -31
  40. data/lib/tantot/performer/inline.rb +0 -9
  41. data/lib/tantot/performer/sidekiq.rb +0 -21
  42. data/lib/tantot/registry.rb +0 -11
@@ -1,32 +1,37 @@
1
- require 'cityhash'
1
+ require 'ostruct'
2
2
 
3
3
  module Tantot
4
4
  module Observe
5
5
  module Helpers
6
- def condition_proc(context)
7
- attributes = context[:attributes]
8
- options = context[:options]
6
+ def condition_proc(watch)
7
+ attributes = watch.attributes
8
+ options = watch.options
9
9
  proc do
10
- has_changes = attributes.any? ? (self.destroyed? || (self._watch_changes.keys & attributes).any?) : true
10
+ has_changes = attributes[:only].any? ? (self.destroyed? || (self._watch_changes.keys & attributes[:watched]).any?) : true
11
11
  has_changes && (!options.key?(:if) || self.instance_exec(&options[:if]))
12
12
  end
13
13
  end
14
14
 
15
- def update_proc(context)
15
+ def update_proc(watch)
16
16
  proc do
17
- attributes = context[:attributes]
17
+ attributes = watch.attributes
18
18
  watched_changes =
19
- if attributes.any?
19
+ if attributes[:only].any?
20
20
  if self.destroyed?
21
- attributes.each.with_object({}) {|attr, hash| hash[attr] = [self[attr]]}
21
+ attributes[:watched].each.with_object({}) {|attr, hash| hash[attr] = [self.attributes[attr]]}
22
22
  else
23
- self._watch_changes.slice(*attributes)
23
+ self._watch_changes.slice(*attributes[:watched])
24
24
  end
25
25
  else
26
26
  self._watch_changes
27
27
  end
28
28
 
29
- Tantot.collector.push(context, self, watched_changes)
29
+ # If explicitly watching attributes, always include their values (if not already included through change tracking)
30
+ attributes[:always].each do |attribute|
31
+ watched_changes[attribute] = [self.attributes[attribute]] unless watched_changes.key?(attribute)
32
+ end
33
+
34
+ watch.agent.push(watch, self, watched_changes)
30
35
  end
31
36
  end
32
37
  end
@@ -48,33 +53,33 @@ module Tantot
48
53
  def watch(*args, &block)
49
54
  options = args.extract_options!
50
55
 
51
- watcher = args.first.is_a?(String) || args.first.is_a?(Class) ? Tantot.derive_watcher(args.shift) : nil
52
- unless !!watcher ^ block_given?
53
- raise ArgumentError.new("At least one, and only one of `watcher` or `block` can be passed")
54
- end
56
+ # Syntax allows for the first argument to be a watcher class, shift
57
+ # it if it is a string or class
58
+ watcher = args.first.is_a?(String) || args.first.is_a?(Class) ? args.shift : nil
55
59
 
56
- attributes = args.collect(&:to_s)
60
+ only_attributes = Array.wrap(options.fetch(:only, [])).collect(&:to_s)
61
+ always_attributes = Array.wrap(options.fetch(:always, [])).collect(&:to_s)
57
62
 
58
- context = {
59
- model: self,
60
- attributes: attributes,
61
- options: options
63
+ # Setup watch
64
+ watch = OpenStruct.new
65
+ watch.model = self
66
+ watch.attributes ={
67
+ only: only_attributes,
68
+ always: always_attributes,
69
+ watched: only_attributes | always_attributes
62
70
  }
71
+ watch.options = options
72
+ watch.block = block
73
+ watch.watcher = watcher
63
74
 
64
- if watcher
65
- context[:watcher] = watcher
66
- options.reverse_merge!(watcher.watcher_options)
67
- end
68
- context[:block_id] = CityHash.hash64(block.source_location.collect(&:to_s).join) if block_given?
69
-
70
- Tantot.collector.register_watch(context, block)
75
+ Tantot.agent_registry.register(watch)
71
76
 
77
+ # Setup and register callbacks
72
78
  callback_options = {}.tap do |opts|
73
- opts[:if] = Observe.condition_proc(context) if context[:attributes].any? || options.key?(:if)
74
- opts[:on] = options[:on] if options.key?(:on)
79
+ opts[:if] = Observe.condition_proc(watch) if watch.attributes[:only].any? || watch.options.key?(:if)
80
+ opts[:on] = watch.options[:on] if watch.options.key?(:on)
75
81
  end
76
- update_proc = Observe.update_proc(context)
77
-
82
+ update_proc = Observe.update_proc(watch)
78
83
  if Tantot.config.use_after_commit_callbacks
79
84
  after_commit(callback_options, &update_proc)
80
85
  else
@@ -19,5 +19,10 @@ module Tantot
19
19
  Tantot.logger.debug { "[Tantot] Installing Rails middleware" }
20
20
  app.config.middleware.insert_after(Rails::Rack::Logger, RequestStrategy)
21
21
  end
22
+
23
+ config.to_prepare do
24
+ Tantot.logger.debug { "[Tantot] Clearing registry" }
25
+ Tantot.registry.clear
26
+ end
22
27
  end
23
28
  end
@@ -0,0 +1,24 @@
1
+ require 'tantot/strategy/bypass'
2
+ require 'tantot/strategy/inline'
3
+
4
+ begin
5
+ require 'chewy'
6
+ require 'tantot/strategy/chewy'
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
11
+ begin
12
+ require 'sidekiq'
13
+ require 'tantot/strategy/sidekiq'
14
+ rescue LoadError
15
+ nil
16
+ end
17
+
18
+ module Tantot
19
+ module Strategy
20
+ def self.resolve(name)
21
+ "Tantot::Strategy::#{name.to_s.camelize}".safe_constantize or raise "Can't find strategy class `#{name}`"
22
+ end
23
+ end
24
+ end
@@ -1,7 +1,7 @@
1
1
  module Tantot
2
- module Performer
2
+ module Strategy
3
3
  class Bypass
4
- def run(context, changes)
4
+ def run(id, changes_by_model)
5
5
  # nop
6
6
  end
7
7
  end
@@ -0,0 +1,33 @@
1
+ module Tantot
2
+ module Strategy
3
+ class Chewy
4
+ class Worker
5
+ include ::Sidekiq::Worker
6
+
7
+ def perform(agent_id, chew_strategy, changes_by_model)
8
+ agent = Tantot.agent_registry.agent(agent_id)
9
+ raise AgentNotFound.new("No registered agent with id #{id}") unless agent
10
+
11
+ ::Chewy.strategy(chewy_strategy) do
12
+ agent.peform(Tantot::Strategy::Sidekiq.unmarshal(changes_by_model))
13
+ Tantot.collector.perform(context, changes)
14
+ end
15
+ end
16
+ end
17
+
18
+ def run(agent, changes_by_model)
19
+ case ::Chewy.strategy.current.name
20
+ when /sidekiq/
21
+ queue = agent.options[:queue] || Tantot.config.sidekiq_queue
22
+ ::Sidekiq::Client.push('class' => Tantot::Strategy::Chewy::Worker,
23
+ 'args' => [agent.id, ::Chewy.strategy.current.name, Tantot::Strategy::Sidekiq.marshal(changes_by_model)],
24
+ 'queue' => queue)
25
+ when :bypass
26
+ return
27
+ else # :atomic, :urgent, any other (even nil, which we want to pass and fail in Chewy)
28
+ Tantot::Strategy::Inline.new.run(agent, changes_by_model)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Tantot
2
+ module Strategy
3
+ class Inline
4
+ def run(agent, changes_by_model)
5
+ agent.perform(changes_by_model)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ module Tantot
2
+ module Strategy
3
+ class Sidekiq
4
+ class Worker
5
+ include ::Sidekiq::Worker
6
+
7
+ def perform(agent_id, changes_by_model)
8
+ agent = Tantot.agent_registry.agent(agent_id)
9
+ raise AgentNotFound.new("No registered agent with id #{agent_id}") unless agent
10
+ agent.perform(Tantot::Strategy::Sidekiq.unmarshal(changes_by_model))
11
+ end
12
+ end
13
+
14
+ def run(agent, changes_by_model)
15
+ queue = agent.options[:queue] || Tantot.config.sidekiq_queue
16
+ ::Sidekiq::Client.push('class' => Tantot::Strategy::Sidekiq::Worker,
17
+ 'args' => [agent.id, Tantot::Strategy::Sidekiq.marshal(changes_by_model)],
18
+ 'queue' => queue)
19
+ end
20
+
21
+ def self.marshal(changes_by_model)
22
+ changes_by_model.each.with_object({}) do |(model_class, changes), hash|
23
+ hash[model_class.name] = changes
24
+ end
25
+ end
26
+
27
+ def self.unmarshal(changes_by_model)
28
+ changes_by_model.each.with_object({}) do |(model_class_name, changes_by_id), model_hash|
29
+ model_hash[model_class_name.constantize] = changes_by_id.each.with_object({}) do |(id, changes), change_hash|
30
+ change_hash[id.to_i] = changes
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,3 @@
1
1
  module Tantot
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -41,10 +41,12 @@ class BlockRun < ProfileRun
41
41
 
42
42
  def run
43
43
  RUNS.times do
44
- city = BlockRunCity.create! name: 'foo'
45
- city.name = 'bar'
46
- city.save
47
- city.destroy
44
+ Tantot.manager.run do
45
+ city = BlockRunCity.create! name: 'foo'
46
+ city.name = 'bar'
47
+ city.save
48
+ city.destroy
49
+ end
48
50
  end
49
51
  end
50
52
  end
@@ -71,10 +73,12 @@ class WatcherRun < ProfileRun
71
73
 
72
74
  def run
73
75
  RUNS.times do
74
- city = WatcherRunCity.create! name: 'foo'
75
- city.name = 'bar'
76
- city.save
77
- city.destroy
76
+ Tantot.manager.run do
77
+ city = WatcherRunCity.create! name: 'foo'
78
+ city.name = 'bar'
79
+ city.save
80
+ city.destroy
81
+ end
78
82
  end
79
83
  end
80
84
 
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe Tantot::Agent::Block do
4
+
5
+ context 'using a block' do
6
+ let(:value) { {changes: 0} }
7
+ let(:changes) { {obj: nil} }
8
+ before do
9
+ v = value
10
+ c = changes
11
+ stub_model(:city) do
12
+ watch {|changes| v[:changes] += 1; c[:obj] = changes}
13
+ end
14
+ end
15
+
16
+ it "should call the block" do
17
+ city = nil
18
+ Tantot.manager.run do
19
+ city = City.create!
20
+ end
21
+ expect(value[:changes]).to eq(1)
22
+ expect(changes[:obj]).to eq(Tantot::Changes::ById.new({city.id => {"id" => [nil, 1]}}))
23
+ end
24
+
25
+ it "call a single time if multiple changes occur" do
26
+ Tantot.manager.run do
27
+ 3.times { City.create! }
28
+ end
29
+ expect(value[:changes]).to eq(1)
30
+ expect(changes[:obj]).to eq(Tantot::Changes::ById.new({1=>{"id"=>[nil, 1]}, 2=>{"id"=>[nil, 2]}, 3=>{"id"=>[nil, 3]}}))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,211 @@
1
+ require "spec_helper"
2
+
3
+ describe Tantot::Agent do
4
+ describe "options" do
5
+ let(:watcher_instance) { double }
6
+
7
+ before do
8
+ stub_class("TestWatcher") { include Tantot::Watcher }
9
+ allow(TestWatcher).to receive(:new).and_return(watcher_instance)
10
+ end
11
+
12
+ context 'with an additional `if` statement' do
13
+ [:no, :some].each do |attribute_opt|
14
+ context "with #{attribute_opt.to_s} attributes" do
15
+ let(:condition) { double }
16
+ before do
17
+ c = condition
18
+ watch_params = [TestWatcher]
19
+ hash = {}
20
+ hash[:only] = :id if attribute_opt == :some
21
+ hash[:if] = -> { c.passed? }
22
+ watch_params << hash
23
+ stub_model(:city) do
24
+ watch(*watch_params)
25
+ end
26
+ end
27
+
28
+ it "should fail if the condition is false" do
29
+ Tantot.manager.run do
30
+ expect(condition).to receive(:passed?).once.and_return(false)
31
+ City.create!
32
+ expect(watcher_instance).not_to receive(:perform)
33
+ end
34
+ end
35
+
36
+ it "should pass if the condition is true" do
37
+ Tantot.manager.run do
38
+ expect(condition).to receive(:passed?).once.and_return(true)
39
+ City.create!
40
+ expect(watcher_instance).to receive(:perform)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'always:' do
48
+ context 'when watching everything' do
49
+ before do
50
+ stub_model(:city) do
51
+ belongs_to :country
52
+
53
+ watch TestWatcher, always: :country_id
54
+ end
55
+
56
+ stub_model(:country) do
57
+ has_many :cities
58
+ end
59
+ end
60
+
61
+ it "should watch all changes including the always field even when not changed" do
62
+ Tantot.manager.run do
63
+ city = City.create name: 'foo'
64
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"id" => [nil, city.id], "name" => [nil, "foo"], "country_id" => [nil]}}}))
65
+ end
66
+ end
67
+
68
+ it "should watch all changes including the always field even when changed" do
69
+ Tantot.manager.run do
70
+ country = Country.create
71
+ city = City.create name: 'foo', country: country
72
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"id" => [nil, city.id], "name" => [nil, "foo"], "country_id" => [nil, country.id]}}}))
73
+ end
74
+ end
75
+
76
+ it "should send the field value when destroyed" do
77
+ Tantot.manager.run do
78
+ country = Country.create
79
+ city = City.create name: 'foo', country: country
80
+ Tantot.manager.sweep(:bypass)
81
+
82
+ city.reload
83
+
84
+ city.destroy
85
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"country_id" => [country.id]}}}))
86
+ end
87
+ end
88
+ end
89
+
90
+ context "when watching specific attributes" do
91
+ before do
92
+ stub_model(:city) do
93
+ belongs_to :country
94
+
95
+ watch TestWatcher, only: :name, always: :country_id
96
+ end
97
+
98
+ stub_model(:country) do
99
+ has_many :cities
100
+ end
101
+ end
102
+
103
+ it "should watch all changes including the always field even when not changed" do
104
+ Tantot.manager.run do
105
+ city = City.create name: 'foo'
106
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, "foo"], "country_id" => [nil]}}}))
107
+ end
108
+ end
109
+
110
+ it "should watch all changes including the always field even when changed" do
111
+ Tantot.manager.run do
112
+ country = Country.create
113
+ city = City.create name: 'foo', country: country
114
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, "foo"], "country_id" => [nil, country.id]}}}))
115
+ end
116
+ end
117
+
118
+ it "should send the field value when destroyed" do
119
+ Tantot.manager.run do
120
+ country = Country.create
121
+ city = City.create name: 'foo', country: country
122
+ Tantot.manager.sweep(:bypass)
123
+
124
+ city.reload
125
+
126
+ city.destroy
127
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['foo'], "country_id" => [country.id]}}}))
128
+ end
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+ context 'on:' do
135
+ context ':create' do
136
+ before do
137
+ stub_model(:city) do
138
+ watch TestWatcher, on: :create
139
+ end
140
+ end
141
+
142
+ it "should only watch creation" do
143
+ city = nil
144
+ Tantot.manager.run do
145
+ city = City.create!
146
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"id" => [nil, city.id]}}}))
147
+ end
148
+ Tantot.manager.run do
149
+ city = City.find(city)
150
+ city.name = 'foo'
151
+ city.save
152
+ end
153
+ Tantot.manager.run do
154
+ city = City.find(city)
155
+ city.destroy
156
+ end
157
+ end
158
+ end
159
+
160
+ context ':update' do
161
+ before do
162
+ stub_model(:city) do
163
+ watch TestWatcher, on: :update
164
+ end
165
+ end
166
+
167
+ it "should only watch update" do
168
+ city = nil
169
+ Tantot.manager.run do
170
+ city = City.create!
171
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
172
+ end
173
+ Tantot.manager.run do
174
+ city = City.find(city)
175
+ city.name = 'foo'
176
+ city.save
177
+ end
178
+ Tantot.manager.run do
179
+ city = City.find(city)
180
+ city.destroy
181
+ end
182
+ end
183
+ end
184
+
185
+ context ':destroy' do
186
+ before do
187
+ stub_model(:city) do
188
+ watch TestWatcher, on: :destroy
189
+ end
190
+ end
191
+
192
+ it "should only watch destruction" do
193
+ city = nil
194
+ Tantot.manager.run do
195
+ city = City.create!
196
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {}}}))
197
+ end
198
+ Tantot.manager.run do
199
+ city = City.find(city)
200
+ city.name = 'foo'
201
+ city.save
202
+ end
203
+ Tantot.manager.run do
204
+ city = City.find(city)
205
+ city.destroy
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end