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
@@ -0,0 +1,180 @@
1
+ require "spec_helper"
2
+
3
+ describe Tantot::Agent::Watcher do
4
+ let(:watcher_instance) { double }
5
+
6
+ before do
7
+ stub_class("TestWatcher") { include Tantot::Watcher }
8
+ allow(TestWatcher).to receive(:new).and_return(watcher_instance)
9
+ end
10
+
11
+ describe '.derive_watcher' do
12
+ class TestWatcher
13
+ include Tantot::Watcher
14
+ end
15
+
16
+ class WrongWatcher
17
+ end
18
+
19
+ module Foo
20
+ class BarWatcher
21
+ include Tantot::Watcher
22
+ end
23
+ end
24
+
25
+ specify { expect { described_class.derive_watcher('foo') }.to raise_error(Tantot::UnderivableWatcher) }
26
+ specify { expect { described_class.derive_watcher(WrongWatcher) }.to raise_error(Tantot::UnderivableWatcher) }
27
+ specify { expect(described_class.derive_watcher(TestWatcher)).to eq(TestWatcher) }
28
+ specify { expect(described_class.derive_watcher(Foo::BarWatcher)).to eq(Foo::BarWatcher) }
29
+ specify { expect(described_class.derive_watcher('test')).to eq(TestWatcher) }
30
+ specify { expect(described_class.derive_watcher('foo/bar')).to eq(Foo::BarWatcher) }
31
+ end
32
+
33
+ [true, false].each do |use_after_commit_callbacks|
34
+ context "using after_commit hooks: #{use_after_commit_callbacks}" do
35
+ before { allow(Tantot.config).to receive(:use_after_commit_callbacks).and_return(use_after_commit_callbacks) }
36
+
37
+ context "watching an attribute" do
38
+ before do
39
+ stub_model(:city) do
40
+ watch TestWatcher, only: :name
41
+ end
42
+ end
43
+
44
+ it "doesn't call back when the attribute doesn't change" do
45
+ Tantot.manager.run do
46
+ City.create
47
+ expect(watcher_instance).not_to receive(:perform)
48
+ end
49
+ end
50
+
51
+ it "calls back when the attribute changes (on creation)" do
52
+ Tantot.manager.run do
53
+ city = City.create name: 'foo'
54
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
55
+ end
56
+ end
57
+
58
+ it "calls back on model update" do
59
+ city = City.create!
60
+ city.reload
61
+ Tantot.manager.sweep(:bypass)
62
+
63
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
64
+ Tantot.manager.run do
65
+ city.name = "foo"
66
+ city.save
67
+ end
68
+ end
69
+
70
+ it "calls back on model destroy" do
71
+ city = City.create!(name: 'foo')
72
+ city.reload
73
+ Tantot.manager.sweep(:bypass)
74
+
75
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['foo']}}}))
76
+ Tantot.manager.run do
77
+ city.destroy
78
+ end
79
+ end
80
+
81
+ it "calls back once per model even when updated more than once" do
82
+ Tantot.manager.run do
83
+ city = City.create! name: 'foo'
84
+ city.name = 'bar'
85
+ city.save
86
+ city.name = 'baz'
87
+ city.save
88
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo', 'bar', 'baz']}}}))
89
+ end
90
+ end
91
+
92
+ it "allows to call a watcher mid-stream" do
93
+ Tantot.manager.run do
94
+ city = City.create name: 'foo'
95
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => [nil, 'foo']}}}))
96
+ Tantot.manager.sweep(:inline)
97
+ city.name = 'bar'
98
+ city.save
99
+ expect(watcher_instance).to receive(:perform).with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['foo', 'bar']}}}))
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ context "on multiple models" do
107
+ before do
108
+ stub_model(:city) do
109
+ watch TestWatcher, only: [:name, :country_id]
110
+ end
111
+ stub_model(:country) do
112
+ watch TestWatcher, only: [:country_code]
113
+ end
114
+ end
115
+
116
+ it "calls back once per watch when multiple watched models change" do
117
+ country = Country.create!(country_code: "CDN")
118
+ city = City.create!(name: "Quebec", country_id: country.id)
119
+ country.reload
120
+ city.reload
121
+ Tantot.manager.sweep(:bypass)
122
+
123
+ expect(watcher_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['Quebec', 'foo', 'bar'], "country_id" => [country.id, nil]}}, Country => {country.id => {"country_code" => ['CDN', 'US']}}}))
124
+ Tantot.manager.run do
125
+ city.name = "foo"
126
+ city.save
127
+ city.name = "bar"
128
+ city.save
129
+ city.country_id = nil
130
+ city.save
131
+ country.country_code = 'US'
132
+ country.save
133
+ city.destroy
134
+ end
135
+ end
136
+ end
137
+
138
+ context "with multiple watchers" do
139
+ let(:watchA_instance) { double }
140
+ let(:watchB_instance) { double }
141
+ before do
142
+ stub_class("TestWatcherA") { include Tantot::Watcher }
143
+ stub_class("TestWatcherB") { include Tantot::Watcher }
144
+ allow(TestWatcherA).to receive(:new).and_return(watchA_instance)
145
+ allow(TestWatcherB).to receive(:new).and_return(watchB_instance)
146
+ stub_model(:city) do
147
+ watch TestWatcherA, only: [:name, :country_id]
148
+ watch TestWatcherB, only: :rating
149
+ end
150
+ stub_model(:country) do
151
+ watch TestWatcherA, only: :country_code
152
+ watch TestWatcherB, only: [:name, :rating]
153
+ end
154
+ end
155
+
156
+ it "calls each watcher once for multiple models" do
157
+ country = Country.create!(country_code: "CDN")
158
+ city = City.create!(name: "Quebec", country_id: country.id, rating: 12)
159
+ country.reload
160
+ city.reload
161
+ expect(watchA_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"name" => ['Quebec', 'foo', 'bar'], "country_id" => [country.id, nil]}}, Country => {country.id => {"country_code" => ['CDN', 'US']}}}))
162
+ # WatchB receives the last value of rating since it has been destroyed
163
+ expect(watchB_instance).to receive(:perform).once.with(Tantot::Changes::ByModel.new({City => {city.id => {"rating" => [12]}}}))
164
+ Tantot.manager.sweep(:bypass)
165
+
166
+ Tantot.manager.run do
167
+ city.name = "foo"
168
+ city.save
169
+ city.name = "bar"
170
+ city.save
171
+ city.country_id = nil
172
+ city.save
173
+ country.country_code = 'US'
174
+ country.save
175
+ city.destroy
176
+ end
177
+ end
178
+ end
179
+
180
+ end
@@ -5,117 +5,320 @@ if defined?(::Chewy)
5
5
  describe Tantot::Extensions::Chewy do
6
6
 
7
7
  [nil, :self, :class_method, :block].product([:some, :all]).each do |backreference_opt, attribute_opt|
8
- it "should update indexes using backreference: #{backreference_opt.inspect}, attributes: #{attribute_opt}" do
9
- chewy_type = double
8
+ context "should update indexes using backreference: #{backreference_opt.inspect}, attributes: #{attribute_opt}" do
9
+ let(:chewy_type) { double }
10
10
 
11
- watch_index_params = ['foo']
12
- watch_index_params << :id if attribute_opt == :some
11
+ before do
12
+ watch_index_options = {}
13
+ watch_index_params = ['foo']
14
+ watch_index_options[:only] = :id if attribute_opt == :some
13
15
 
14
- block_callback = proc do |changes|
15
- self.yielded_changes = changes
16
- [1, 2, 3]
17
- end
18
-
19
- case backreference_opt
20
- when nil, :block
21
- when :self
22
- watch_index_params << {method: :self}
23
- when :class_method
24
- watch_index_params << {method: :class_get_ids}
25
- end
26
-
27
- stub_model(:city) do
28
- class_attribute :yielded_changes
29
-
30
- if backreference_opt == :block
31
- watch_index(*watch_index_params, &block_callback)
32
- else
33
- watch_index(*watch_index_params)
34
- end
35
-
36
- def self.class_get_ids(changes)
16
+ block_callback = proc do |changes|
37
17
  self.yielded_changes = changes
38
18
  [1, 2, 3]
39
19
  end
40
- end
41
20
 
42
- city1 = city2 = nil
21
+ case backreference_opt
22
+ when nil, :block
23
+ when :self
24
+ watch_index_options[:method] = :self
25
+ when :class_method
26
+ watch_index_options[:method] = :class_get_ids
27
+ end
43
28
 
44
- Tantot.collector.run do
45
- city1 = City.create!
46
- city2 = City.create!
29
+ watch_index_params << watch_index_options
47
30
 
48
- # Stub the integration point between us and Chewy
49
- expect(Chewy).to receive(:derive_type).with('foo').and_return(chewy_type)
31
+ stub_model(:city) do
32
+ class_attribute :yielded_changes
50
33
 
51
- # Depending on backreference
52
- case backreference_opt
53
- when nil, :self
54
- # Implicit and self reference will update with the created model id
55
- expect(chewy_type).to receive(:update_index).with([city1.id, city2.id], {})
56
- when :class_method, :block
57
- # Validate that the returned ids are updated
58
- expect(chewy_type).to receive(:update_index).with([1, 2, 3], {})
34
+ if backreference_opt == :block
35
+ watch_index(*watch_index_params, &block_callback)
36
+ else
37
+ watch_index(*watch_index_params)
38
+ end
39
+
40
+ def self.class_get_ids(changes)
41
+ self.yielded_changes = changes
42
+ [1, 2, 3]
43
+ end
59
44
  end
60
45
  end
61
46
 
62
- # Make sure the callbacks received the changes
63
- if [:class_method, :block].include?(backreference_opt)
64
- expect(City.yielded_changes).to eq(Tantot::Changes::ById.new({city1.id => {"id" => [nil, city1.id]}, city2.id => {"id" => [nil, city2.id]}}))
65
- end
47
+ it "should update accordingly" do
48
+ city1 = city2 = nil
49
+
50
+ Tantot.manager.run do
51
+ city1 = City.create!(name: 'foo')
52
+ city2 = City.create!(name: 'bar')
53
+
54
+ # Stub the integration point between us and Chewy
55
+ expect(Chewy).to receive(:derive_type).with('foo').and_return(chewy_type)
56
+
57
+ # Depending on backreference
58
+ case backreference_opt
59
+ when nil, :self
60
+ # Implicit and self reference will update with the created model id
61
+ expect(chewy_type).to receive(:update_index).with([city1.id, city2.id], {})
62
+ when :class_method, :block
63
+ # Validate that the returned ids are updated
64
+ expect(chewy_type).to receive(:update_index).with([1, 2, 3], {})
65
+ end
66
+ end
67
+
68
+ # Make sure the callbacks received the changes
69
+ if [:class_method, :block].include?(backreference_opt)
70
+ if attribute_opt == :some
71
+ expect(City.yielded_changes).to eq(Tantot::Changes::ById.new({city1.id => {"id" => [nil, city1.id]}, city2.id => {"id" => [nil, city2.id]}}))
72
+ else
73
+ expect(City.yielded_changes).to eq(Tantot::Changes::ById.new({city1.id => {"id" => [nil, city1.id], "name" => [nil, 'foo']}, city2.id => {"id" => [nil, city2.id], "name" => [nil, 'bar']}}))
74
+ end
75
+ end
66
76
 
77
+ end
67
78
  end
68
79
  end
69
80
 
70
- it "should allow registering an index watch on self (all attributes, destroy)" do
71
- chewy_type = double
81
+ context "allow registering an index watch on self (all attributes, destroy)" do
82
+ let(:chewy_type) { double }
72
83
 
73
- stub_model(:city) do
74
- watch_index 'foo'
84
+ before do
85
+ stub_model(:city) do
86
+ watch_index 'foo'
87
+ end
75
88
  end
76
89
 
77
- city = City.create!
78
- Tantot.collector.sweep(:bypass)
90
+ it "should update accordingly" do
91
+ city = City.create!
92
+ Tantot.manager.sweep(:bypass)
79
93
 
80
- Tantot.collector.run do
81
- city.destroy
94
+ Tantot.manager.run do
95
+ city.destroy
82
96
 
83
- expect(Chewy).to receive(:derive_type).with('foo').and_return(chewy_type)
84
- expect(chewy_type).to receive(:update_index).with([city.id], {})
97
+ expect(Chewy).to receive(:derive_type).with('foo').and_return(chewy_type)
98
+ expect(chewy_type).to receive(:update_index).with([city.id], {})
99
+ end
85
100
  end
86
101
  end
87
102
 
88
- it "should allow registering an index watch on self (all attributes, destroy, block)" do
89
- chewy_type = double
103
+ context "allow registering an index watch on self (all attributes, destroy, block)" do
104
+ let(:chewy_type) { double }
90
105
 
91
- stub_model(:city) do
92
- watch_index 'foo' do |changes|
93
- changes.ids
106
+ before do
107
+ stub_model(:city) do
108
+ watch_index 'foo' do |changes|
109
+ changes.ids
110
+ end
94
111
  end
95
112
  end
96
113
 
97
- city = City.create!
98
- Tantot.collector.sweep(:bypass)
114
+ it "should update accordingly" do
115
+ city = City.create!
116
+ Tantot.manager.sweep(:bypass)
99
117
 
100
- Tantot.collector.run do
101
- city.destroy
118
+ Tantot.manager.run do
119
+ city.destroy
102
120
 
103
- expect(Chewy).to receive(:derive_type).with('foo').and_return(chewy_type)
104
- expect(chewy_type).to receive(:update_index).with([city.id], {})
121
+ expect(Chewy).to receive(:derive_type).with('foo').and_return(chewy_type)
122
+ expect(chewy_type).to receive(:update_index).with([city.id], {})
123
+ end
105
124
  end
106
125
  end
107
126
 
108
- it "should allow returning nothing in a callback" do
109
- stub_model(:city) do
110
- watch_index('foo') { 1 if false }
111
- watch_index('bar') { [] }
112
- watch_index('baz') { nil }
127
+ context "allow returning nothing in a callback" do
128
+ before do
129
+ stub_model(:city) do
130
+ watch_index('foo') { 1 if false }
131
+ watch_index('bar') { [] }
132
+ watch_index('baz') { nil }
133
+ end
113
134
  end
114
135
 
115
- Tantot.collector.run do
116
- City.create!
136
+ it "should update accordingly" do
137
+ Tantot.manager.run do
138
+ City.create!
117
139
 
118
- expect(Chewy).not_to receive(:derive_type)
140
+ expect(Chewy).not_to receive(:derive_type)
141
+ end
142
+ end
143
+ end
144
+
145
+ context "association" do
146
+ context "simple" do
147
+ let(:chewy_type) { double }
148
+ before do
149
+ stub_model(:city) do
150
+ belongs_to :country
151
+
152
+ has_many :streets
153
+
154
+ watch_index 'country#countries', only: :name, association: :country
155
+ end
156
+
157
+ stub_model(:country) do
158
+ has_many :cities
159
+
160
+ watch_index 'city#cities', only: :name, association: :cities
161
+ end
162
+ end
163
+
164
+ it "allows automatically watching a :belongs_to association as a backreference" do
165
+ Tantot.manager.run do
166
+ country1 = Country.create! id: 111
167
+ country2 = Country.create! id: 222
168
+ city = City.create id: 999, country: country1
169
+ city.reload
170
+ Tantot.manager.sweep(:bypass)
171
+
172
+ city.country = country2
173
+ city.save
174
+
175
+ expect(Chewy).to receive(:derive_type).with('country#countries').and_return(chewy_type)
176
+ expect(chewy_type).to receive(:update_index).with([country1.id, country2.id], {})
177
+ end
178
+ end
179
+
180
+ it "allows automatically watching an :has_many association as a backreference" do
181
+ Tantot.manager.run do
182
+ country = Country.create! id: 111
183
+ country.cities.create id: 990
184
+ country.cities.create id: 991
185
+ country.reload
186
+ Tantot.manager.sweep(:bypass)
187
+
188
+ country.name = "foo"
189
+ country.save
190
+
191
+ expect(Chewy).to receive(:derive_type).with('city#cities').and_return(chewy_type)
192
+ expect(chewy_type).to receive(:update_index).with([990, 991], {})
193
+ end
194
+ end
195
+ end
196
+
197
+ context "through" do
198
+ context "has_many through -> has_many -> belongs_to" do
199
+ let(:chewy_type) { double }
200
+ before do
201
+ stub_model(:color) do
202
+ belongs_to :group
203
+ end
204
+
205
+ stub_model(:user) do
206
+ has_many :memberships
207
+ has_many :groups, through: :memberships
208
+
209
+ watch_index 'groups#group', only: :username, association: :groups
210
+ end
211
+
212
+ stub_model(:membership) do
213
+ belongs_to :user
214
+ belongs_to :group
215
+
216
+ has_many :colors, through: :group
217
+ end
218
+
219
+ stub_model(:group) do
220
+ has_many :colors
221
+ has_many :memberships
222
+ has_many :users, through: :memberships
223
+ end
224
+ end
225
+
226
+ it 'updates accordingly' do
227
+ Tantot.manager.run do
228
+
229
+ user = User.create! id: 111
230
+ group = Group.create! id: 999
231
+ Membership.create! id: 555, user: user, group: group
232
+ Tantot.manager.sweep(:bypass)
233
+
234
+ user.username = 'foo'
235
+ user.save
236
+
237
+ expect(Chewy).to receive(:derive_type).with('groups#group').and_return(chewy_type)
238
+ expect(chewy_type).to receive(:update_index).with([999], {})
239
+ end
240
+ end
241
+ end
242
+
243
+ context "has_many through -> has_many -> has_many" do
244
+ let(:chewy_type) { double }
245
+ before do
246
+ stub_model(:street) do
247
+ belongs_to :city
248
+ end
249
+
250
+ stub_model(:city) do
251
+ belongs_to :country
252
+
253
+ has_many :streets
254
+ end
255
+
256
+ stub_model(:country) do
257
+ has_many :cities
258
+ has_many :streets, through: :cities
259
+
260
+ watch_index 'streets#street', only: :name, association: :streets
261
+ end
262
+ end
263
+
264
+ it "updates accordingly" do
265
+ Tantot.manager.run do
266
+ country = Country.create! id: 111
267
+ city = City.create! id: 999, country: country
268
+ Street.create! id: 555, city: city
269
+ country.reload
270
+ Tantot.manager.sweep(:bypass)
271
+
272
+ country.name = "foo"
273
+ country.save
274
+
275
+ expect(Chewy).to receive(:derive_type).with('streets#street').and_return(chewy_type)
276
+ expect(chewy_type).to receive(:update_index).with([555], {})
277
+ end
278
+ end
279
+ end
280
+
281
+ context "has_many through -> belongs_to -> has_many" do
282
+ let(:chewy_type) { double }
283
+ before do
284
+ stub_model(:color) do
285
+ belongs_to :group
286
+ end
287
+
288
+ stub_model(:user) do
289
+ has_many :memberships
290
+ end
291
+
292
+ stub_model(:group) do
293
+ has_many :memberships
294
+ has_many :colors
295
+ end
296
+
297
+ stub_model(:membership) do
298
+ belongs_to :group
299
+
300
+ has_many :colors, through: :group
301
+
302
+ watch_index 'colors#color', association: :colors
303
+ end
304
+ end
305
+
306
+ it "updates accordingly" do
307
+ Tantot.manager.run do
308
+ group = Group.create! id: 111
309
+ group.colors.create! id: 222, name: 'red'
310
+ membership = Membership.create id: 333, group: group
311
+ Tantot.manager.sweep(:bypass)
312
+ membership.reload
313
+
314
+ membership.name = "foo"
315
+ membership.save
316
+
317
+ expect(Chewy).to receive(:derive_type).with('colors#color').and_return(chewy_type)
318
+ expect(chewy_type).to receive(:update_index).with([222], {})
319
+ end
320
+ end
321
+ end
119
322
  end
120
323
  end
121
324
  end
@@ -123,13 +326,12 @@ if defined?(::Chewy)
123
326
  describe "Chewy.strategy" do
124
327
  before do
125
328
  allow(Tantot.config).to receive(:sweep_on_push).and_return(true)
126
- end
127
-
128
- it "should bypass if Chewy.strategy is :bypass" do
129
329
  stub_model(:city) do
130
330
  watch_index('foo')
131
331
  end
332
+ end
132
333
 
334
+ it "should bypass if Chewy.strategy is :bypass" do
133
335
  expect(Chewy).not_to receive(:derive_type)
134
336
  Chewy.strategy :bypass do
135
337
  City.create!