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
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
|