tantot 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9977ff915ab335570ae85a05425204dc31625a21
|
4
|
+
data.tar.gz: f0c7baed8f6eb5a773ac7cd79b79c9b377cb90ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5391976b3520c289a2d30f10a348c8a7ab280ed7a59b0af2b4834a768aeae0cfe15b3e9e015e983ca110106e05f8e492b27571c11e68046635e17c88a489ad6
|
7
|
+
data.tar.gz: 1fcc978f3c7ccdac880879e0f05fe6c158c8f4f9a87058bea6b71071477bc2d776fc3bc9bbf922851e5055cb0ae6030f1d02a29ab000d667c372bc7f2f0f842b
|
data/lib/tantot.rb
CHANGED
@@ -6,12 +6,10 @@ require 'singleton'
|
|
6
6
|
|
7
7
|
require 'tantot/errors'
|
8
8
|
require 'tantot/config'
|
9
|
-
require 'tantot/registry'
|
10
9
|
require 'tantot/changes'
|
11
|
-
require 'tantot/
|
12
|
-
require 'tantot/
|
13
|
-
require 'tantot/
|
14
|
-
require 'tantot/collector'
|
10
|
+
require 'tantot/agent'
|
11
|
+
require 'tantot/strategy'
|
12
|
+
require 'tantot/manager'
|
15
13
|
require 'tantot/observe'
|
16
14
|
|
17
15
|
require 'tantot/extensions/chewy'
|
@@ -28,30 +26,16 @@ module Tantot
|
|
28
26
|
class << self
|
29
27
|
attr_writer :logger
|
30
28
|
|
31
|
-
def
|
32
|
-
|
33
|
-
if name.is_a?(Class)
|
34
|
-
name
|
35
|
-
else
|
36
|
-
class_name = "#{name.camelize}Watcher"
|
37
|
-
watcher = class_name.safe_constantize
|
38
|
-
raise Tantot::UnderivableWatcher, "Can not find watcher named `#{class_name}`" unless watcher
|
39
|
-
watcher
|
40
|
-
end
|
41
|
-
raise Tantot::UnderivableWatcher, "Watcher class does not include Tantot::Watcher: #{watcher}" unless watcher.included_modules.include?(Tantot::Watcher)
|
42
|
-
watcher
|
43
|
-
end
|
44
|
-
|
45
|
-
def collector
|
46
|
-
Thread.current[:tantot_collector] ||= Tantot::Collector::Manager.new
|
29
|
+
def manager
|
30
|
+
Thread.current[:tantot_manager] ||= Tantot::Manager.new
|
47
31
|
end
|
48
32
|
|
49
33
|
def config
|
50
34
|
Tantot::Config.instance
|
51
35
|
end
|
52
36
|
|
53
|
-
def
|
54
|
-
Tantot::Registry.instance
|
37
|
+
def agent_registry
|
38
|
+
Tantot::Agent::Registry.instance
|
55
39
|
end
|
56
40
|
|
57
41
|
def logger
|
data/lib/tantot/agent.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'tantot/agent/base'
|
2
|
+
require 'tantot/agent/block'
|
3
|
+
require 'tantot/agent/watcher'
|
4
|
+
require 'tantot/agent/registry'
|
5
|
+
|
6
|
+
module Tantot
|
7
|
+
module Agent
|
8
|
+
|
9
|
+
AGENT_CLASSES = [Tantot::Agent::Block, Tantot::Agent::Watcher]
|
10
|
+
|
11
|
+
def self.resolve!(watch)
|
12
|
+
agent_classes = AGENT_CLASSES.collect {|klass| [klass, klass.identify(watch)]}.reject {|_klass, id| id.nil?}
|
13
|
+
raise UnresolvableAgent("Can't resolve agent for watch: #{watch.inspect}") unless agent_classes.any?
|
14
|
+
raise UnresolvableAgent("More than one agent manages watch: #{watch.inspect}") if agent_classes.many?
|
15
|
+
agent_classes.first
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Tantot
|
2
|
+
module Agent
|
3
|
+
class Base
|
4
|
+
attr_reader :id, :watches, :stash
|
5
|
+
|
6
|
+
def self.identify(watch)
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(id)
|
11
|
+
@id = id
|
12
|
+
@watches = []
|
13
|
+
@stash = Hash.new do |model_hash, model|
|
14
|
+
model_hash[model] = Hash.new do |instance_id_hash, instance_id|
|
15
|
+
instance_id_hash[instance_id] = Hash.new do |attribute_hash, attribute|
|
16
|
+
attribute_hash[attribute] = []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def options
|
23
|
+
@watches.first.options
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_watch(watch)
|
27
|
+
watch.agent = self
|
28
|
+
setup_watch(watch)
|
29
|
+
@watches.push(watch)
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_watch(watch)
|
33
|
+
# nop
|
34
|
+
end
|
35
|
+
|
36
|
+
def push(watch, instance, changes_by_attribute)
|
37
|
+
Tantot.logger.debug do
|
38
|
+
mutate = changes_by_attribute.size.zero? ? 'destroy' : "#{changes_by_attribute.size} mutations(s)"
|
39
|
+
"[Tantot] [Collecting] [#{self.class.name.demodulize}] #{mutate} on <#{instance.class.name}:#{instance.id}> for <#{debug_id}>"
|
40
|
+
end
|
41
|
+
attribute_hash = @stash[watch.model][instance.id]
|
42
|
+
changes_by_attribute.each do |attr, changes|
|
43
|
+
attribute_hash[attr] |= changes
|
44
|
+
end
|
45
|
+
sweep if Tantot.config.sweep_on_push
|
46
|
+
end
|
47
|
+
|
48
|
+
def sweep(strategy_name = nil)
|
49
|
+
if @stash.any?
|
50
|
+
strategy = Tantot::Strategy.resolve(strategy_name || options[:strategy] || Tantot.config.strategy).new
|
51
|
+
Tantot.logger.debug { "[Tantot] [Strategy] [#{self.class.name.demodulize}] [#{strategy.class.name.demodulize}] [#{debug_id}] #{debug_stash}" }
|
52
|
+
strategy.run(self, @stash)
|
53
|
+
@stash.clear
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def debug_id
|
58
|
+
raise NotImplementedError
|
59
|
+
end
|
60
|
+
|
61
|
+
def debug_stash
|
62
|
+
"#{@stash.collect {|model, changes_by_id| debug_changes_for_model(model, changes_by_id)}.join(" & ")})"
|
63
|
+
end
|
64
|
+
|
65
|
+
def debug_changes_for_model(model, changes_by_id)
|
66
|
+
"#{model.name}#{changes_by_id.keys.inspect}"
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Tantot
|
2
|
+
module Agent
|
3
|
+
class Block < Base
|
4
|
+
def self.identify(watch)
|
5
|
+
if watch.block.present?
|
6
|
+
"#{watch.model.to_s}|#{watch.options.inspect}"
|
7
|
+
else
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform(changes_by_model)
|
13
|
+
# Block agent always has only one watch
|
14
|
+
block = watches.first.block
|
15
|
+
model = watches.first.model
|
16
|
+
# Skip the model part of the changes since it will always be on a
|
17
|
+
# single model, and wrap it in the ById helper.
|
18
|
+
model.instance_exec(Tantot::Changes::ById.new(changes_by_model.values.first), &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug_block(block)
|
22
|
+
location, line = block.source_location
|
23
|
+
short_path = defined?(Rails) ? Pathname.new(location).relative_path_from(Rails.root).to_s : location
|
24
|
+
"block @ #{short_path}##{line}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def debug_id
|
28
|
+
debug_block(watches.first.block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tantot
|
2
|
+
module Agent
|
3
|
+
class Registry
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@agents = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def register(watch)
|
11
|
+
agent_class, watch_id = Tantot::Agent.resolve!(watch)
|
12
|
+
agent = @agents.fetch(watch_id.to_s) do
|
13
|
+
agent_class.new(watch_id).tap {|new_agent| @agents[watch_id.to_s] = new_agent}
|
14
|
+
end
|
15
|
+
agent.add_watch(watch)
|
16
|
+
agent
|
17
|
+
end
|
18
|
+
|
19
|
+
def agent(agent_id)
|
20
|
+
@agents[agent_id.to_s]
|
21
|
+
end
|
22
|
+
|
23
|
+
def each_agent
|
24
|
+
@agents.values.each do |agent|
|
25
|
+
yield agent
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear
|
30
|
+
@agents.clear
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'tantot/watcher'
|
2
|
+
|
3
|
+
module Tantot
|
4
|
+
module Agent
|
5
|
+
class Watcher < Base
|
6
|
+
def self.identify(watch)
|
7
|
+
if watch.watcher.present?
|
8
|
+
derive_watcher(watch.watcher)
|
9
|
+
else
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.derive_watcher(name)
|
15
|
+
watcher =
|
16
|
+
if name.is_a?(Class)
|
17
|
+
name
|
18
|
+
else
|
19
|
+
class_name = "#{name.camelize}Watcher"
|
20
|
+
watcher = class_name.safe_constantize
|
21
|
+
raise Tantot::UnderivableWatcher, "Can not find watcher named `#{class_name}`" unless watcher
|
22
|
+
watcher
|
23
|
+
end
|
24
|
+
raise Tantot::UnderivableWatcher, "Watcher class does not include Tantot::Watcher: #{watcher}" unless watcher.included_modules.include?(Tantot::Watcher)
|
25
|
+
watcher
|
26
|
+
end
|
27
|
+
|
28
|
+
def watcher
|
29
|
+
# The id of the agent is the watcher class (see self#identify)
|
30
|
+
id
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup_watch(watch)
|
34
|
+
watch.options.reverse_merge!(watcher.watcher_options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform(changes_by_model)
|
38
|
+
watcher.new.perform(Tantot::Changes::ByModel.new(changes_by_model))
|
39
|
+
end
|
40
|
+
|
41
|
+
def debug_id
|
42
|
+
id.name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/tantot/changes.rb
CHANGED
@@ -17,8 +17,7 @@ module Tantot
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def for_attribute(attribute, compact = true)
|
20
|
-
|
21
|
-
compact ? changes.compact : changes
|
20
|
+
@changes_by_id.values.collect {|changes_by_attribute| changes_by_attribute[attribute.to_s]}.flatten.uniq.tap {|changes| changes.compact! if compact}
|
22
21
|
end
|
23
22
|
|
24
23
|
def ids
|
@@ -39,7 +38,7 @@ module Tantot
|
|
39
38
|
@changes_by_model = changes_by_model
|
40
39
|
end
|
41
40
|
|
42
|
-
delegate :==, :keys, :count, :size, to: :changes_by_model
|
41
|
+
delegate :==, :keys, :values, :count, :size, to: :changes_by_model
|
43
42
|
alias_method :models, :keys
|
44
43
|
|
45
44
|
def ==(other)
|
data/lib/tantot/config.rb
CHANGED
@@ -2,10 +2,10 @@ module Tantot
|
|
2
2
|
class Config
|
3
3
|
include Singleton
|
4
4
|
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :strategy, :format, :use_after_commit_callbacks, :sweep_on_push, :sidekiq_queue
|
6
6
|
|
7
7
|
def initialize
|
8
|
-
@
|
8
|
+
@strategy = :inline
|
9
9
|
@format = :compact
|
10
10
|
@use_after_commit_callbacks = true
|
11
11
|
@sweep_on_push = false
|
data/lib/tantot/errors.rb
CHANGED
@@ -11,7 +11,27 @@ module Tantot
|
|
11
11
|
# watch_index 'index#type', attribute, attribute, {method: [:self | :method | ignore and pass a block | ignore and don't pass a block, equivalent of :self]} [block]
|
12
12
|
def watch_index(type_name, *args, &block)
|
13
13
|
options = args.extract_options!
|
14
|
-
|
14
|
+
watch_options = {}
|
15
|
+
watch_options[:only] = options[:only] if options[:only]
|
16
|
+
|
17
|
+
if options[:association]
|
18
|
+
reflection = self.reflect_on_association(options[:association])
|
19
|
+
raise ArgumentError.new("Association #{options[:association]} not found on #{self.class.name}") unless reflection
|
20
|
+
case reflection.macro
|
21
|
+
when :belongs_to
|
22
|
+
watch_options[:always] = reflection.foreign_key
|
23
|
+
when :has_one, :has_many
|
24
|
+
if reflection.options[:through]
|
25
|
+
if reflection.through_reflection.belongs_to?
|
26
|
+
watch_options[:always] = reflection.through_reflection.foreign_key
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
raise NotImplementedError.new("Association of type #{reflection.macro} not yet supported")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
watch('tantot/extensions/chewy/chewy', *args, watch_options)
|
15
35
|
Tantot::Extensions::Chewy.register_watch(self, type_name, options, block)
|
16
36
|
end
|
17
37
|
end
|
@@ -26,7 +46,7 @@ module Tantot
|
|
26
46
|
class ChewyWatcher
|
27
47
|
include Tantot::Watcher
|
28
48
|
|
29
|
-
watcher_options
|
49
|
+
watcher_options strategy: :chewy
|
30
50
|
|
31
51
|
def perform(changes_by_model)
|
32
52
|
changes_by_model.each do |model, changes_by_id|
|
@@ -46,25 +66,53 @@ module Tantot
|
|
46
66
|
|
47
67
|
watch_args_array.each do |watch_args|
|
48
68
|
method = watch_args[:method]
|
49
|
-
options = watch_args[:options]
|
50
69
|
block = watch_args[:block]
|
70
|
+
options = watch_args[:options]
|
71
|
+
association = options[:association]
|
51
72
|
|
52
73
|
# Find ids to update
|
53
74
|
backreference =
|
54
|
-
if
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
75
|
+
if association
|
76
|
+
reflection = model.reflect_on_association(association)
|
77
|
+
reflection.check_validity!
|
78
|
+
case reflection.macro
|
79
|
+
when :belongs_to
|
80
|
+
changes_by_id.for_attribute(reflection.foreign_key)
|
81
|
+
when :has_one, :has_many
|
82
|
+
if reflection.options[:through]
|
83
|
+
through_query =
|
84
|
+
case reflection.through_reflection.macro
|
85
|
+
when :belongs_to
|
86
|
+
reflection.through_reflection.klass.where(reflection.through_reflection.klass.primary_key => changes_by_id.for_attribute(reflection.through_reflection.foreign_key))
|
87
|
+
when :has_many, :has_one
|
88
|
+
reflection.through_reflection.klass.where(reflection.through_reflection.foreign_key => changes_by_id.ids)
|
89
|
+
end
|
90
|
+
case reflection.source_reflection.macro
|
91
|
+
when :belongs_to
|
92
|
+
through_query.pluck(reflection.source_reflection.foreign_key)
|
93
|
+
when :has_many
|
94
|
+
reflection.source_reflection.klass.where(reflection.source_reflection.foreign_key => (through_query.ids)).ids
|
95
|
+
end
|
96
|
+
else
|
97
|
+
reflection.klass.where(reflection.foreign_key => changes_by_id.ids).ids
|
98
|
+
end
|
99
|
+
end
|
100
|
+
else
|
101
|
+
if (method && method.to_sym == :self) || (!method && !block)
|
102
|
+
# Simply extract keys from changes
|
103
|
+
changes_by_id.keys
|
104
|
+
elsif method
|
105
|
+
# We need to call `method`.
|
106
|
+
# Try to find it on the class. If so, call it once with all changes.
|
107
|
+
# There is no API to call per-instance since objects can be already destroyed
|
108
|
+
# when using the sidekiq performer
|
109
|
+
model.send(method, changes_by_id)
|
110
|
+
elsif block
|
111
|
+
# Since we can be post-destruction of the model, we can't load models here
|
112
|
+
# Thus, the signature of the block callback is |changes| which are all
|
113
|
+
# the changes to all the models
|
114
|
+
model.instance_exec(changes_by_id, &block)
|
115
|
+
end
|
68
116
|
end
|
69
117
|
|
70
118
|
if backreference
|
@@ -73,7 +121,7 @@ module Tantot
|
|
73
121
|
# Make sure there are any backreferences
|
74
122
|
if backreference.any?
|
75
123
|
Tantot.logger.debug { "[Tantot] [Chewy] [update_index] #{reference} (#{backreference.count} objects): #{backreference.inspect}" }
|
76
|
-
::Chewy.derive_type(reference).update_index(backreference,
|
124
|
+
::Chewy.derive_type(reference).update_index(backreference, {})
|
77
125
|
end
|
78
126
|
end
|
79
127
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Tantot
|
2
|
+
class Manager
|
3
|
+
def run(&block)
|
4
|
+
yield
|
5
|
+
ensure
|
6
|
+
sweep
|
7
|
+
end
|
8
|
+
|
9
|
+
def sweep(strategy_name = nil)
|
10
|
+
Tantot.agent_registry.each_agent {|agent| agent.sweep(strategy_name)}
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform(context, changes_by_model)
|
14
|
+
collector = resolve!(context)
|
15
|
+
Tantot.logger.debug { "[Tantot] [Run] [#{collector.class.name.demodulize}] #{collector.debug_perform(context, changes)}" }
|
16
|
+
collector.perform(context, changes_by_model)
|
17
|
+
end
|
18
|
+
|
19
|
+
def marshal(watch, changes)
|
20
|
+
collector.marshal(context, changes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def unmarshal(context, changes)
|
24
|
+
context.deep_symbolize_keys!
|
25
|
+
collector_class = context[:collector_class].constantize
|
26
|
+
collector = @watches[collector_class] || @watches[collector_class] = collector_class.new
|
27
|
+
collector.unmarshal(context, changes)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|