tantot 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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