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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f6ad6fe3fb7afb2054c68fe8625cc99ea85c327
4
- data.tar.gz: 2196c42e206c184669e3429323b4d77774db068e
3
+ metadata.gz: 9977ff915ab335570ae85a05425204dc31625a21
4
+ data.tar.gz: f0c7baed8f6eb5a773ac7cd79b79c9b377cb90ab
5
5
  SHA512:
6
- metadata.gz: 757def8ab9f5e5fa8846b95a6aec91d7f12c2181c51c01680e4fb683cfe61debfe5f71f45e0560da93f43a88c87e2d01d24b1e0c5c338bda469628287bdbe97a
7
- data.tar.gz: 9c0c6e845e61607d91e0513d6ce8cf6226b577b416e3aee6fdba89e3be4cde58c7bb1c52a4186313d85623f6db07a96df4716a8c81f619bcf72908a4a44b6fe9
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/watcher'
12
- require 'tantot/performer'
13
- require 'tantot/formatter'
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 derive_watcher(name)
32
- watcher =
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 registry
54
- Tantot::Registry.instance
37
+ def agent_registry
38
+ Tantot::Agent::Registry.instance
55
39
  end
56
40
 
57
41
  def logger
@@ -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
@@ -17,8 +17,7 @@ module Tantot
17
17
  end
18
18
 
19
19
  def for_attribute(attribute, compact = true)
20
- changes = @changes_by_id.values.collect {|changes_by_attribute| changes_by_attribute[attribute.to_s]}.flatten.uniq
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 :performer, :format, :use_after_commit_callbacks, :sweep_on_push, :sidekiq_queue
5
+ attr_accessor :strategy, :format, :use_after_commit_callbacks, :sweep_on_push, :sidekiq_queue
6
6
 
7
7
  def initialize
8
- @performer = :inline
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
@@ -1,4 +1,10 @@
1
1
  module Tantot
2
2
  class UnderivableWatcher < StandardError
3
3
  end
4
+
5
+ class UnresolvableAgent < StandardError
6
+ end
7
+
8
+ class AgentNotFound < StandardError
9
+ end
4
10
  end
@@ -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
- watch('tantot/extensions/chewy/chewy', *args)
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 performer: :chewy
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 (method && method.to_sym == :self) || (!method && !block)
55
- # Simply extract keys from changes
56
- changes_by_id.keys
57
- elsif method
58
- # We need to call `method`.
59
- # Try to find it on the class. If so, call it once with all changes.
60
- # There is no API to call per-instance since objects can be already destroyed
61
- # when using the sidekiq performer
62
- model.send(method, changes_by_id)
63
- elsif block
64
- # Since we can be post-destruction of the model, we can't load models here
65
- # Thus, the signature of the block callback is |changes| which are all
66
- # the changes to all the models
67
- model.instance_exec(changes_by_id, &block)
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, options)
124
+ ::Chewy.derive_type(reference).update_index(backreference, {})
77
125
  end
78
126
  end
79
127
 
@@ -5,7 +5,7 @@ if defined?(::Grape)
5
5
 
6
6
  class GrapeMiddleware < Grape::Middleware::Base
7
7
  def call!(env)
8
- Tantot.collector.run do
8
+ Tantot.manager.run do
9
9
  @app_response = super(env)
10
10
  end
11
11
  end
@@ -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