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.
- checksums.yaml +4 -4
- data/lib/tantot.rb +7 -23
- data/lib/tantot/agent.rb +19 -0
- data/lib/tantot/agent/base.rb +71 -0
- data/lib/tantot/agent/block.rb +32 -0
- data/lib/tantot/agent/registry.rb +34 -0
- data/lib/tantot/agent/watcher.rb +46 -0
- data/lib/tantot/changes.rb +2 -3
- data/lib/tantot/config.rb +2 -2
- data/lib/tantot/errors.rb +6 -0
- data/lib/tantot/extensions/chewy.rb +66 -18
- data/lib/tantot/extensions/grape/middleware.rb +1 -1
- data/lib/tantot/manager.rb +31 -0
- data/lib/tantot/observe.rb +36 -31
- data/lib/tantot/railtie.rb +5 -0
- data/lib/tantot/strategy.rb +24 -0
- data/lib/tantot/{performer → strategy}/bypass.rb +2 -2
- data/lib/tantot/strategy/chewy.rb +33 -0
- data/lib/tantot/strategy/inline.rb +9 -0
- data/lib/tantot/strategy/sidekiq.rb +36 -0
- data/lib/tantot/version.rb +1 -1
- data/performance/profile.rb +12 -8
- data/spec/collector/block_spec.rb +33 -0
- data/spec/collector/options_spec.rb +211 -0
- data/spec/collector/watcher_spec.rb +180 -0
- data/spec/extensions/chewy_spec.rb +280 -78
- data/spec/sidekiq_spec.rb +38 -58
- data/spec/spec_helper.rb +27 -2
- data/spec/tantot_spec.rb +0 -370
- metadata +19 -15
- data/lib/tantot/collector.rb +0 -70
- data/lib/tantot/collector/base.rb +0 -46
- data/lib/tantot/collector/block.rb +0 -69
- data/lib/tantot/collector/watcher.rb +0 -67
- data/lib/tantot/formatter.rb +0 -10
- data/lib/tantot/formatter/compact.rb +0 -9
- data/lib/tantot/formatter/detailed.rb +0 -9
- data/lib/tantot/performer.rb +0 -24
- data/lib/tantot/performer/chewy.rb +0 -31
- data/lib/tantot/performer/inline.rb +0 -9
- data/lib/tantot/performer/sidekiq.rb +0 -21
- data/lib/tantot/registry.rb +0 -11
data/lib/tantot/observe.rb
CHANGED
@@ -1,32 +1,37 @@
|
|
1
|
-
require '
|
1
|
+
require 'ostruct'
|
2
2
|
|
3
3
|
module Tantot
|
4
4
|
module Observe
|
5
5
|
module Helpers
|
6
|
-
def condition_proc(
|
7
|
-
attributes =
|
8
|
-
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(
|
15
|
+
def update_proc(watch)
|
16
16
|
proc do
|
17
|
-
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
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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(
|
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(
|
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
|
data/lib/tantot/railtie.rb
CHANGED
@@ -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
|
@@ -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,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
|
data/lib/tantot/version.rb
CHANGED
data/performance/profile.rb
CHANGED
@@ -41,10 +41,12 @@ class BlockRun < ProfileRun
|
|
41
41
|
|
42
42
|
def run
|
43
43
|
RUNS.times do
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|