promiscuous 0.92.0 → 0.100.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/promiscuous.rb +2 -5
  3. data/lib/promiscuous/amqp.rb +1 -2
  4. data/lib/promiscuous/cli.rb +3 -43
  5. data/lib/promiscuous/config.rb +5 -7
  6. data/lib/promiscuous/error/dependency.rb +1 -3
  7. data/lib/promiscuous/publisher/context.rb +1 -1
  8. data/lib/promiscuous/publisher/context/base.rb +3 -34
  9. data/lib/promiscuous/publisher/model/base.rb +5 -25
  10. data/lib/promiscuous/publisher/model/mock.rb +5 -7
  11. data/lib/promiscuous/publisher/operation/active_record.rb +4 -69
  12. data/lib/promiscuous/publisher/operation/atomic.rb +1 -3
  13. data/lib/promiscuous/publisher/operation/base.rb +33 -123
  14. data/lib/promiscuous/publisher/operation/mongoid.rb +0 -67
  15. data/lib/promiscuous/publisher/operation/non_persistent.rb +0 -1
  16. data/lib/promiscuous/publisher/operation/transaction.rb +1 -3
  17. data/lib/promiscuous/railtie.rb +0 -31
  18. data/lib/promiscuous/subscriber.rb +1 -1
  19. data/lib/promiscuous/subscriber/{worker/message.rb → message.rb} +12 -40
  20. data/lib/promiscuous/subscriber/model/active_record.rb +1 -1
  21. data/lib/promiscuous/subscriber/model/base.rb +4 -4
  22. data/lib/promiscuous/subscriber/model/mongoid.rb +3 -3
  23. data/lib/promiscuous/subscriber/operation.rb +74 -3
  24. data/lib/promiscuous/subscriber/unit_of_work.rb +110 -0
  25. data/lib/promiscuous/subscriber/worker.rb +3 -7
  26. data/lib/promiscuous/subscriber/worker/eventual_destroyer.rb +2 -6
  27. data/lib/promiscuous/subscriber/worker/pump.rb +2 -11
  28. data/lib/promiscuous/version.rb +1 -1
  29. metadata +18 -36
  30. data/lib/promiscuous/error/missing_context.rb +0 -29
  31. data/lib/promiscuous/publisher/bootstrap.rb +0 -27
  32. data/lib/promiscuous/publisher/bootstrap/connection.rb +0 -25
  33. data/lib/promiscuous/publisher/bootstrap/data.rb +0 -127
  34. data/lib/promiscuous/publisher/bootstrap/mode.rb +0 -19
  35. data/lib/promiscuous/publisher/bootstrap/status.rb +0 -40
  36. data/lib/promiscuous/publisher/bootstrap/version.rb +0 -46
  37. data/lib/promiscuous/publisher/context/middleware.rb +0 -94
  38. data/lib/promiscuous/resque.rb +0 -12
  39. data/lib/promiscuous/sidekiq.rb +0 -15
  40. data/lib/promiscuous/subscriber/message_processor.rb +0 -4
  41. data/lib/promiscuous/subscriber/message_processor/base.rb +0 -54
  42. data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +0 -17
  43. data/lib/promiscuous/subscriber/message_processor/regular.rb +0 -238
  44. data/lib/promiscuous/subscriber/operation/base.rb +0 -66
  45. data/lib/promiscuous/subscriber/operation/bootstrap.rb +0 -60
  46. data/lib/promiscuous/subscriber/operation/regular.rb +0 -19
  47. data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +0 -333
@@ -1,9 +1,5 @@
1
1
  class Promiscuous::Railtie < Rails::Railtie
2
2
  initializer 'load promiscuous' do
3
- ActiveSupport.on_load(:action_controller) do
4
- include Promiscuous::Middleware::Controller
5
- end
6
-
7
3
  config.after_initialize do
8
4
  Promiscuous::Config.configure unless Promiscuous::Config.configured?
9
5
  Promiscuous::Loader.prepare
@@ -16,31 +12,4 @@ class Promiscuous::Railtie < Rails::Railtie
16
12
  end
17
13
  end
18
14
  end
19
-
20
- # XXX Only Rails 3.x support
21
- console do
22
- class << IRB
23
- alias_method :start_without_promiscuous, :start
24
-
25
- def start(*args, &block)
26
- return start_without_promiscuous(*args, &block) if Promiscuous::Publisher::Context.current
27
- Promiscuous::Middleware.with_context 'rails/console' do
28
- start_without_promiscuous(*args, &block)
29
- end
30
- end
31
- end
32
-
33
- if defined?(Pry)
34
- class << Pry
35
- alias_method :start_without_promiscuous, :start
36
-
37
- def start(*args, &block)
38
- return start_without_promiscuous(*args, &block) if Promiscuous::Publisher::Context.current
39
- Promiscuous::Middleware.with_context 'rails/console' do
40
- start_without_promiscuous(*args, &block)
41
- end
42
- end
43
- end
44
- end
45
- end
46
15
  end
@@ -1,6 +1,6 @@
1
1
  module Promiscuous::Subscriber
2
2
  extend Promiscuous::Autoload
3
- autoload :Worker, :MessageProcessor, :Model, :Operation
3
+ autoload :Worker, :UnitOfWork, :Message, :Model, :Operation, :Message
4
4
 
5
5
  extend ActiveSupport::Concern
6
6
 
@@ -1,4 +1,4 @@
1
- class Promiscuous::Subscriber::Worker::Message
1
+ class Promiscuous::Subscriber::Message
2
2
  attr_accessor :payload, :parsed_payload
3
3
 
4
4
  def initialize(payload, options={})
@@ -11,10 +11,6 @@ class Promiscuous::Subscriber::Worker::Message
11
11
  @parsed_payload ||= MultiJson.load(payload)
12
12
  end
13
13
 
14
- def context
15
- parsed_payload['context']
16
- end
17
-
18
14
  def app
19
15
  parsed_payload['app']
20
16
  end
@@ -30,19 +26,18 @@ class Promiscuous::Subscriber::Worker::Message
30
26
  def dependencies
31
27
  @dependencies ||= begin
32
28
  dependencies = parsed_payload['dependencies'] || {}
33
- deps = dependencies['read'].to_a.map { |dep| Promiscuous::Dependency.parse(dep, :type => :read, :owner => app) } +
34
- dependencies['write'].to_a.map { |dep| Promiscuous::Dependency.parse(dep, :type => :write, :owner => app) }
29
+ deps = dependencies['write'].to_a.map { |dep| Promiscuous::Dependency.parse(dep, :type => :write, :owner => app) }
35
30
 
36
31
  deps
37
32
  end
38
33
  end
39
34
 
40
- def write_dependencies
41
- @write_dependencies ||= dependencies.select(&:write?)
35
+ def types
36
+ @parsed_payload['types']
42
37
  end
43
38
 
44
- def read_dependencies
45
- @read_dependencies ||= dependencies.select(&:read?)
39
+ def write_dependencies
40
+ @write_dependencies ||= dependencies.select(&:write?)
46
41
  end
47
42
 
48
43
  def happens_before_dependencies
@@ -60,12 +55,8 @@ class Promiscuous::Subscriber::Worker::Message
60
55
  dependencies.present?
61
56
  end
62
57
 
63
- def was_during_bootstrap?
64
- !!parsed_payload['was_during_bootstrap']
65
- end
66
-
67
58
  def to_s
68
- "#{app}/#{context} -> #{happens_before_dependencies.join(', ')}"
59
+ "#{app} -> #{write_dependencies.join(', ')}"
69
60
  end
70
61
 
71
62
  def ack
@@ -79,34 +70,15 @@ class Promiscuous::Subscriber::Worker::Message
79
70
  Promiscuous::Config.error_notifier.call(e)
80
71
  end
81
72
 
82
- def postpone
83
- # Only used during bootstrapping
84
- @metadata.postpone
85
- rescue Exception => e
86
- # We don't care if we fail
87
- Promiscuous.warn "[receive] (postpone) Some exception happened, but it's okay: #{e}\n#{e.backtrace.join("\n")}"
88
- Promiscuous::Config.error_notifier.call(e)
89
- end
90
-
91
- def unit_of_work(type, &block)
92
- Promiscuous.context { yield }
93
- ensure
94
- if defined?(ActiveRecord)
95
- ActiveRecord::Base.clear_active_connections!
96
- end
97
- end
98
-
99
73
  def process
100
- unit_of_work(context) do
101
- if Promiscuous::Config.bootstrap
102
- Promiscuous::Subscriber::MessageProcessor::Bootstrap.process(self)
103
- else
104
- Promiscuous::Subscriber::MessageProcessor::Regular.process(self)
105
- end
106
- end
74
+ Promiscuous::Subscriber::UnitOfWork.process(self)
107
75
  rescue Exception => orig_e
108
76
  e = Promiscuous::Error::Subscriber.new(orig_e, :payload => payload)
109
77
  Promiscuous.warn "[receive] #{payload} #{e}\n#{e.backtrace.join("\n")}"
110
78
  Promiscuous::Config.error_notifier.call(e)
79
+ ensure
80
+ if defined?(ActiveRecord)
81
+ ActiveRecord::Base.clear_active_connections!
82
+ end
111
83
  end
112
84
  end
@@ -3,7 +3,7 @@ module Promiscuous::Subscriber::Model::ActiveRecord
3
3
  include Promiscuous::Subscriber::Model::Base
4
4
 
5
5
  included do
6
- if Promiscuous::Config.consistency == :eventual && !self.columns.collect(&:name).include?("_v")
6
+ if !self.columns.collect(&:name).include?("_v")
7
7
  raise <<-help
8
8
  #{self} must include a _v column. Create the following migration:
9
9
  change_table :#{self.table_name} do |t|
@@ -2,10 +2,10 @@ module Promiscuous::Subscriber::Model::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def __promiscuous_eventual_consistency_update(operation)
5
- return true unless Promiscuous::Config.consistency == :eventual
6
- return true unless operation.message.has_dependencies?
5
+ return true unless self.respond_to?(:attributes)
6
+ return true unless operation.version
7
7
 
8
- version = operation.message_processor.instance_dep.version
8
+ version = operation.version
9
9
  generation = operation.message.generation
10
10
  version = (generation << 50) | version
11
11
 
@@ -30,7 +30,7 @@ module Promiscuous::Subscriber::Model::Base
30
30
  value = payload.attributes[attr]
31
31
  update = true
32
32
 
33
- attr_payload = Promiscuous::Subscriber::Operation::Regular.new(value)
33
+ attr_payload = Promiscuous::Subscriber::Operation.new(value)
34
34
  if model = attr_payload.model
35
35
  # Nested subscriber
36
36
  old_value = __send__(attr)
@@ -9,7 +9,7 @@ module Promiscuous::Subscriber::Model::Mongoid
9
9
  end
10
10
 
11
11
  included do
12
- field :_v if Promiscuous::Config.consistency == :eventual
12
+ field :_v
13
13
  end
14
14
 
15
15
  module ClassMethods
@@ -77,7 +77,7 @@ module Promiscuous::Subscriber::Model::Mongoid
77
77
  new_e['existed'] = true
78
78
  old_e.instance_variable_set(:@keep, true)
79
79
 
80
- payload = Promiscuous::Subscriber::Operation::Regular.new(new_e)
80
+ payload = Promiscuous::Subscriber::Operation.new(new_e)
81
81
  old_e.__promiscuous_update(payload, :old_value => old_e)
82
82
  end
83
83
  end
@@ -89,7 +89,7 @@ module Promiscuous::Subscriber::Model::Mongoid
89
89
 
90
90
  # create all the new ones
91
91
  new_embeddeds.reject { |new_e| new_e['existed'] }.each do |new_e|
92
- payload = Promiscuous::Subscriber::Operation::Regular.new(new_e)
92
+ payload = Promiscuous::Subscriber::Operation.new(new_e)
93
93
  new_e_instance = payload.model.__promiscuous_fetch_new(payload.id)
94
94
  new_e_instance.__promiscuous_update(payload)
95
95
  options[:parent].__send__(old_embeddeds.metadata[:name]) << new_e_instance
@@ -1,4 +1,75 @@
1
- module Promiscuous::Subscriber::Operation
2
- extend Promiscuous::Autoload
3
- autoload :Base, :Regular, :Bootstrap
1
+ class Promiscuous::Subscriber::Operation
2
+ attr_accessor :model, :id, :operation, :attributes, :version
3
+ delegate :message, :to => :unit_of_work
4
+
5
+ def initialize(payload)
6
+ if payload.is_a?(Hash)
7
+ self.id = payload['id']
8
+ self.operation = payload['operation'].try(:to_sym)
9
+ self.attributes = payload['attributes']
10
+ self.version = payload['version']
11
+ self.model = self.get_subscribed_model(payload) if payload['types']
12
+ end
13
+ end
14
+
15
+ def get_subscribed_model(payload)
16
+ [message.app, '*'].each do |app|
17
+ app_mapping = Promiscuous::Subscriber::Model.mapping[app] || {}
18
+ payload['types'].to_a.each do |ancestor|
19
+ model = app_mapping[ancestor]
20
+ return model if model
21
+ end
22
+ end
23
+ nil
24
+ end
25
+
26
+ def warn(msg)
27
+ Promiscuous.warn "[receive] #{msg} #{message.payload}"
28
+ end
29
+
30
+ def create(options={})
31
+ model.__promiscuous_fetch_new(id).tap do |instance|
32
+ instance.__promiscuous_update(self)
33
+ instance.save!
34
+ end
35
+ rescue Exception => e
36
+ if model.__promiscuous_duplicate_key_exception?(e)
37
+ options[:on_already_created] ||= proc { warn "ignoring already created record" }
38
+ options[:on_already_created].call
39
+ else
40
+ raise e
41
+ end
42
+ end
43
+
44
+ def update(should_create_on_failure=true)
45
+ model.__promiscuous_fetch_existing(id).tap do |instance|
46
+ if instance.__promiscuous_eventual_consistency_update(self)
47
+ instance.__promiscuous_update(self)
48
+ instance.save!
49
+ end
50
+ end
51
+ rescue model.__promiscuous_missing_record_exception
52
+ warn "upserting"
53
+ create :on_already_created => proc { update(false) if should_create_on_failure }
54
+ end
55
+
56
+ def destroy
57
+ if message.dependencies.present?
58
+ Promiscuous::Subscriber::Worker::EventualDestroyer.postpone_destroy(model, id)
59
+ else
60
+ model.__promiscuous_fetch_existing(id).destroy
61
+ end
62
+ end
63
+
64
+ def execute
65
+ case operation
66
+ when :create then create
67
+ when :update then update
68
+ when :destroy then destroy
69
+ end
70
+ end
71
+
72
+ def unit_of_work
73
+ @unit_of_work ||= Promiscuous::Subscriber::UnitOfWork.current
74
+ end
4
75
  end
@@ -0,0 +1,110 @@
1
+ class Promiscuous::Subscriber::UnitOfWork
2
+ attr_accessor :message
3
+ delegate :dependencies, :to => :message
4
+
5
+ def initialize(message)
6
+ self.message = message
7
+ end
8
+
9
+ def app
10
+ message.parsed_payload['app']
11
+ end
12
+
13
+ def operations
14
+ message.parsed_payload['operations'].
15
+ each_with_index.
16
+ map { |op, i| Promiscuous::Subscriber::Operation.new(op.merge('version' => message.dependencies[i].try(&:version))) }
17
+ end
18
+
19
+ def self.process(*args)
20
+ raise "Same thread is processing a message?" if self.current
21
+
22
+ begin
23
+ self.current = new(*args)
24
+ self.current.process_message
25
+ ensure
26
+ self.current = nil
27
+ end
28
+ end
29
+
30
+ def self.current
31
+ Thread.current[:promiscuous_message_processor]
32
+ end
33
+
34
+ def self.current=(value)
35
+ Thread.current[:promiscuous_message_processor] = value
36
+ end
37
+
38
+ def process_message
39
+ begin
40
+ on_message
41
+ rescue Exception => e
42
+ @fail_count ||= 0; @fail_count += 1
43
+
44
+ if @fail_count <= Promiscuous::Config.max_retries
45
+ Promiscuous.warn("[receive] #{e.message} #{@fail_count.ordinalize} retry: #{@message}")
46
+ sleep @fail_count ** 2
47
+ process_message
48
+ else
49
+ raise e
50
+ end
51
+ end
52
+ end
53
+
54
+ def instance_dep
55
+ @instance_dep ||= dependencies.first
56
+ end
57
+
58
+ def master_node
59
+ @master_node ||= instance_dep.redis_node
60
+ end
61
+
62
+ LOCK_OPTIONS = { :timeout => 1.5.minute, # after 1.5 minute, we give up
63
+ :sleep => 0.1, # polling every 100ms.
64
+ :expire => 1.minute } # after one minute, we are considered dead
65
+
66
+ def with_instance_locked(&block)
67
+ return yield unless message.has_dependencies?
68
+
69
+ lock_options = LOCK_OPTIONS.merge(:node => master_node)
70
+ mutex = Promiscuous::Redis::Mutex.new(instance_dep.key(:sub).to_s, lock_options)
71
+
72
+ unless mutex.lock
73
+ raise Promiscuous::Error::LockUnavailable.new(mutex.key)
74
+ end
75
+
76
+ begin
77
+ yield
78
+ ensure
79
+ unless mutex.unlock
80
+ # TODO Be safe in case we have a duplicate message and lost the lock on it
81
+ raise "The subscriber lost the lock during its operation. It means that someone else\n"+
82
+ "received a duplicate message, and we got screwed.\n"
83
+ end
84
+ end
85
+ end
86
+
87
+ # XXX Used for hooking into e.g. by promiscuous-newrelic
88
+ def execute_operation(operation)
89
+ operation.execute
90
+ end
91
+
92
+ def on_message
93
+ with_instance_locked do
94
+ with_transaction do
95
+ self.operations.each { |op| execute_operation(op) if op.model }
96
+ end
97
+ end
98
+ message.ack
99
+ end
100
+
101
+ private
102
+
103
+ def with_transaction(&block)
104
+ if defined?(ActiveRecord::Base)
105
+ ActiveRecord::Base.transaction { yield }
106
+ else
107
+ yield
108
+ end
109
+ end
110
+ end
@@ -1,20 +1,17 @@
1
1
  class Promiscuous::Subscriber::Worker
2
2
  extend Promiscuous::Autoload
3
- autoload :Message, :Pump, :MessageSynchronizer, :Runner, :Stats, :Recorder,
4
- :EventualDestroyer
3
+ autoload :Pump, :Runner, :Stats, :Recorder, :EventualDestroyer
5
4
 
6
- attr_accessor :message_synchronizer, :pump, :runner, :stats, :eventual_destroyer
5
+ attr_accessor :pump, :runner, :stats, :eventual_destroyer
7
6
 
8
7
  def initialize
9
- @message_synchronizer = MessageSynchronizer.new(self)
10
8
  @pump = Pump.new(self)
11
9
  @runner = Runner.new(self)
12
10
  @stats = Stats.new
13
- @eventual_destroyer = EventualDestroyer.new if Promiscuous::Config.consistency == :eventual
11
+ @eventual_destroyer = EventualDestroyer.new
14
12
  end
15
13
 
16
14
  def start
17
- @message_synchronizer.connect
18
15
  @pump.connect
19
16
  @runner.start
20
17
  @stats.connect
@@ -25,7 +22,6 @@ class Promiscuous::Subscriber::Worker
25
22
  @stats.disconnect
26
23
  @runner.stop
27
24
  @pump.disconnect
28
- @message_synchronizer.disconnect
29
25
  @eventual_destroyer.try(:stop)
30
26
  end
31
27
 
@@ -8,18 +8,14 @@ class Promiscuous::Subscriber::Worker::EventualDestroyer
8
8
  @thread = nil
9
9
  end
10
10
 
11
- def self.destroy_timeout
12
- 1.hour
13
- end
14
-
15
11
  def self.check_every
16
- (10 + rand(10)).minutes
12
+ Promiscuous::Config.destroy_check_interval + rand(Promiscuous::Config.destroy_check_interval)
17
13
  end
18
14
 
19
15
  def main_loop
20
16
  loop do
21
17
  begin
22
- PendingDestroy.next(self.class.destroy_timeout).each(&:perform)
18
+ PendingDestroy.next(Promiscuous::Config.destroy_timeout).each(&:perform)
23
19
  rescue Exception => e
24
20
  Promiscuous.warn "[eventual destroyer] #{e}\n#{e.backtrace.join("\n")}"
25
21
  Promiscuous::Config.error_notifier.call(e)