promiscuous 0.90.0 → 0.91.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/lib/promiscuous/amqp/bunny.rb +63 -36
  3. data/lib/promiscuous/amqp/fake.rb +3 -1
  4. data/lib/promiscuous/amqp/hot_bunnies.rb +26 -16
  5. data/lib/promiscuous/amqp/null.rb +1 -0
  6. data/lib/promiscuous/amqp.rb +12 -12
  7. data/lib/promiscuous/cli.rb +70 -29
  8. data/lib/promiscuous/config.rb +54 -29
  9. data/lib/promiscuous/convenience.rb +1 -1
  10. data/lib/promiscuous/dependency.rb +25 -6
  11. data/lib/promiscuous/error/connection.rb +11 -9
  12. data/lib/promiscuous/error/dependency.rb +8 -1
  13. data/lib/promiscuous/loader.rb +4 -2
  14. data/lib/promiscuous/publisher/bootstrap/connection.rb +25 -0
  15. data/lib/promiscuous/publisher/bootstrap/data.rb +127 -0
  16. data/lib/promiscuous/publisher/bootstrap/mode.rb +19 -0
  17. data/lib/promiscuous/publisher/bootstrap/status.rb +40 -0
  18. data/lib/promiscuous/publisher/bootstrap/version.rb +46 -0
  19. data/lib/promiscuous/publisher/bootstrap.rb +27 -0
  20. data/lib/promiscuous/publisher/context/base.rb +67 -0
  21. data/lib/promiscuous/{middleware.rb → publisher/context/middleware.rb} +16 -13
  22. data/lib/promiscuous/publisher/context/transaction.rb +36 -0
  23. data/lib/promiscuous/publisher/context.rb +4 -88
  24. data/lib/promiscuous/publisher/mock_generator.rb +9 -9
  25. data/lib/promiscuous/publisher/model/active_record.rb +7 -7
  26. data/lib/promiscuous/publisher/model/base.rb +29 -29
  27. data/lib/promiscuous/publisher/model/ephemeral.rb +5 -3
  28. data/lib/promiscuous/publisher/model/mock.rb +9 -5
  29. data/lib/promiscuous/publisher/model/mongoid.rb +5 -22
  30. data/lib/promiscuous/publisher/operation/active_record.rb +360 -0
  31. data/lib/promiscuous/publisher/operation/atomic.rb +167 -0
  32. data/lib/promiscuous/publisher/operation/base.rb +279 -474
  33. data/lib/promiscuous/publisher/operation/mongoid.rb +153 -145
  34. data/lib/promiscuous/publisher/operation/non_persistent.rb +28 -0
  35. data/lib/promiscuous/publisher/operation/proxy_for_query.rb +42 -0
  36. data/lib/promiscuous/publisher/operation/transaction.rb +85 -0
  37. data/lib/promiscuous/publisher/operation.rb +1 -1
  38. data/lib/promiscuous/publisher/worker.rb +7 -7
  39. data/lib/promiscuous/publisher.rb +1 -1
  40. data/lib/promiscuous/railtie.rb +20 -5
  41. data/lib/promiscuous/redis.rb +104 -56
  42. data/lib/promiscuous/subscriber/message_processor/base.rb +38 -0
  43. data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +17 -0
  44. data/lib/promiscuous/subscriber/message_processor/regular.rb +192 -0
  45. data/lib/promiscuous/subscriber/message_processor.rb +4 -0
  46. data/lib/promiscuous/subscriber/model/base.rb +20 -15
  47. data/lib/promiscuous/subscriber/model/mongoid.rb +4 -4
  48. data/lib/promiscuous/subscriber/model/observer.rb +16 -2
  49. data/lib/promiscuous/subscriber/operation/base.rb +68 -0
  50. data/lib/promiscuous/subscriber/operation/bootstrap.rb +54 -0
  51. data/lib/promiscuous/subscriber/operation/regular.rb +13 -0
  52. data/lib/promiscuous/subscriber/operation.rb +3 -166
  53. data/lib/promiscuous/subscriber/worker/message.rb +61 -35
  54. data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +90 -59
  55. data/lib/promiscuous/subscriber/worker/pump.rb +17 -5
  56. data/lib/promiscuous/subscriber/worker/recorder.rb +4 -1
  57. data/lib/promiscuous/subscriber/worker/runner.rb +49 -9
  58. data/lib/promiscuous/subscriber/worker/stats.rb +2 -2
  59. data/lib/promiscuous/subscriber/worker.rb +6 -0
  60. data/lib/promiscuous/subscriber.rb +1 -1
  61. data/lib/promiscuous/timer.rb +31 -18
  62. data/lib/promiscuous/version.rb +1 -1
  63. data/lib/promiscuous.rb +23 -3
  64. metadata +104 -89
  65. data/lib/promiscuous/subscriber/payload.rb +0 -34
@@ -1,7 +1,7 @@
1
1
  class Promiscuous::Railtie < Rails::Railtie
2
2
  initializer 'load promiscuous' do
3
- config.before_initialize do
4
- ActionController::Base.__send__(:include, Promiscuous::Middleware::Controller)
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include Promiscuous::Middleware::Controller
5
5
  end
6
6
 
7
7
  config.after_initialize do
@@ -17,13 +17,28 @@ class Promiscuous::Railtie < Rails::Railtie
17
17
  end
18
18
  end
19
19
 
20
+ # XXX Only Rails 3.x support
20
21
  console do
21
22
  class << IRB
22
23
  alias_method :start_without_promiscuous, :start
23
24
 
24
- def start
25
- ::Promiscuous::Middleware.with_context 'rails/console' do
26
- start_without_promiscuous
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
27
42
  end
28
43
  end
29
44
  end
@@ -3,11 +3,19 @@ require 'redis/distributed'
3
3
  require 'digest/sha1'
4
4
 
5
5
  module Promiscuous::Redis
6
- mattr_accessor :master, :slave
7
-
8
6
  def self.connect
9
7
  disconnect
10
- self.master = new_connection
8
+ @master = new_connection
9
+ end
10
+
11
+ def self.master
12
+ ensure_connected unless @master
13
+ @master
14
+ end
15
+
16
+ def self.slave
17
+ ensure_connected unless @slave
18
+ @slave
11
19
  end
12
20
 
13
21
  def self.ensure_slave
@@ -18,10 +26,10 @@ module Promiscuous::Redis
18
26
  end
19
27
 
20
28
  def self.disconnect
21
- self.master.quit if self.master
22
- self.slave.quit if self.slave
23
- self.master = nil
24
- self.slave = nil
29
+ @master.quit if @master
30
+ @slave.quit if @slave
31
+ @master = nil
32
+ @slave = nil
25
33
  end
26
34
 
27
35
  def self.new_connection(url=nil)
@@ -39,7 +47,8 @@ module Promiscuous::Redis
39
47
  end
40
48
 
41
49
  def self.new_blocking_connection
42
- # Remove the read/select loop in redis, it's weird and unecessary
50
+ # This removes the read/select loop in redis, it's weird and unecessary when
51
+ # blocking on the connection.
43
52
  new_connection.tap do |redis|
44
53
  redis.nodes.each do |node|
45
54
  node.client.connection.instance_eval do
@@ -53,14 +62,20 @@ module Promiscuous::Redis
53
62
  end
54
63
  end
55
64
 
56
- def self.lost_connection_exception
57
- Promiscuous::Error::Connection.new(:service => :redis)
65
+ def self.ensure_connected
66
+ Promiscuous.ensure_connected
67
+
68
+ @master.nodes.each do |node|
69
+ begin
70
+ node.ping
71
+ rescue Exception => e
72
+ raise lost_connection_exception(node, :inner => e)
73
+ end
74
+ end
58
75
  end
59
76
 
60
- def self.ensure_connected
61
- Promiscuous::Redis.master.ping
62
- rescue Exception
63
- raise lost_connection_exception
77
+ def self.lost_connection_exception(node, options={})
78
+ Promiscuous::Error::Connection.new("redis://#{node.location}", options)
64
79
  end
65
80
 
66
81
  class Script
@@ -78,16 +93,22 @@ module Promiscuous::Redis
78
93
  end
79
94
  raise e
80
95
  end
96
+
97
+ def to_s
98
+ @script
99
+ end
81
100
  end
82
101
 
83
102
  class Mutex
103
+ attr_reader :token
104
+
84
105
  def initialize(key, options={})
85
106
  # TODO remove old code with orig_key
86
107
  @orig_key = key.to_s
87
108
  @key = "#{key}:lock"
88
- @timeout = options[:timeout]
89
- @sleep = options[:sleep]
90
- @expire = options[:expire]
109
+ @timeout = options[:timeout].to_i
110
+ @sleep = options[:sleep].to_f
111
+ @expire = options[:expire].to_i
91
112
  @lock_set = options[:lock_set]
92
113
  @node = options[:node]
93
114
  raise "Which node?" unless @node
@@ -102,74 +123,101 @@ module Promiscuous::Redis
102
123
  end
103
124
 
104
125
  def lock
105
- if @timeout > 0
106
- # Blocking mode
107
- result = false
108
- start_at = Time.now
109
- while Time.now - start_at < @timeout
110
- break if result = try_lock
111
- sleep @sleep
112
- end
113
- result
114
- else
115
- # Non-blocking mode
116
- try_lock
126
+ result = false
127
+ start_at = Time.now
128
+ while Time.now - start_at < @timeout
129
+ break if result = try_lock
130
+ sleep @sleep
117
131
  end
132
+ result
118
133
  end
119
134
 
120
135
  def try_lock
136
+ raise "You are trying to lock an already locked mutex" if @token
137
+
121
138
  now = Time.now.to_i
122
- @expires_at = now + @expire + 1
123
- @token = Random.rand(1000000000)
124
139
 
125
140
  # This script loading is not thread safe (touching a class variable), but
126
141
  # that's okay, because the race is harmless.
127
142
  @@lock_script ||= Promiscuous::Redis::Script.new <<-SCRIPT
128
143
  local key = KEYS[1]
129
- local lock_set = KEYS[2]
144
+ local token_key = KEYS[2]
145
+ local lock_set = KEYS[3]
130
146
  local now = tonumber(ARGV[1])
131
- local orig_key = ARGV[2]
132
- local expires_at = tonumber(ARGV[3])
133
- local token = ARGV[4]
134
- local lock_value = expires_at .. ':' .. token
135
- local old_value = redis.call('get', key)
147
+ local expires_at = tonumber(ARGV[2])
148
+ local orig_key = ARGV[3]
149
+
150
+ local prev_expires_at = tonumber(redis.call('hget', key, 'expires_at'))
151
+ if prev_expires_at and prev_expires_at > now then
152
+ return {false, nil}
153
+ end
154
+
155
+ local next_token = redis.call('incr', 'promiscuous:next_token')
136
156
 
137
- if old_value and tonumber(old_value:match("([^:]*):"):rep(1)) > now then return false end
138
- redis.call('set', key, lock_value)
139
- if lock_set then redis.call('zadd', lock_set, now, orig_key) end
157
+ redis.call('hmset', key, 'expires_at', expires_at, 'token', next_token)
140
158
 
141
- if old_value then return 'recovered' else return true end
159
+ if lock_set then
160
+ redis.call('zadd', lock_set, now, orig_key)
161
+ end
162
+
163
+ if prev_expires_at then
164
+ return {'recovered', next_token}
165
+ else
166
+ return {true, next_token}
167
+ end
142
168
  SCRIPT
143
- result = @@lock_script.eval(@node, :keys => [@key, @lock_set], :argv => [now, @orig_key, @expires_at, @token])
144
- return :recovered if result == 'recovered'
145
- !!result
169
+ result, @token = @@lock_script.eval(@node, :keys => [@key, 'promiscuous:next_token', @lock_set].compact,
170
+ :argv => [now, now + @expire, @orig_key])
171
+ result == 'recovered' ? :recovered : !!result
172
+ end
173
+
174
+ def extend
175
+ now = Time.now.to_i
176
+ @@extend_script ||= Promiscuous::Redis::Script.new <<-SCRIPT
177
+ local key = KEYS[1]
178
+ local expires_at = tonumber(ARGV[1])
179
+ local token = ARGV[2]
180
+
181
+ if redis.call('hget', key, 'token') == token then
182
+ redis.call('hset', key, 'expires_at', expires_at)
183
+ return true
184
+ else
185
+ return false
186
+ end
187
+ SCRIPT
188
+ !!@@extend_script.eval(@node, :keys => [@key].compact, :argv => [now + @expire, @token])
146
189
  end
147
190
 
148
191
  def unlock
192
+ raise "You are trying to unlock a non locked mutex" unless @token
193
+
149
194
  # Since it's possible that the operations in the critical section took a long time,
150
- # we can't just simply release the lock. The unlock method checks if @expires_at
151
- # remains the same, and do not release when the lock timestamp was overwritten.
152
- @@unlock_script ||= Promiscuous::Redis::Script.new <<-SCRIPT
195
+ # we can't just simply release the lock. The unlock method checks if the unique @token
196
+ # remains the same, and do not release if the lock token was overwritten.
197
+ @@unlock_script ||= Script.new <<-LUA
153
198
  local key = KEYS[1]
154
199
  local lock_set = KEYS[2]
155
- local orig_key = ARGV[1]
156
- local expires_at = ARGV[2]
157
- local token = ARGV[3]
158
- local lock_value = expires_at .. ':' .. token
200
+ local token = ARGV[1]
201
+ local orig_key = ARGV[2]
159
202
 
160
- if redis.call('get', key) == lock_value then
203
+ if redis.call('hget', key, 'token') == token then
161
204
  redis.call('del', key)
162
- if lock_set then redis.call('zrem', lock_set, orig_key) end
205
+ if lock_set then
206
+ redis.call('zrem', lock_set, orig_key)
207
+ end
163
208
  return true
164
209
  else
165
210
  return false
166
211
  end
167
- SCRIPT
168
- @@unlock_script.eval(@node, :keys => [@key, @lock_set], :argv => [@orig_key, @expires_at, @token])
212
+ LUA
213
+ result = @@unlock_script.eval(@node, :keys => [@key, @lock_set].compact, :argv => [@token, @orig_key])
214
+ @token = nil
215
+ !!result
169
216
  end
170
217
 
171
218
  def still_locked?
172
- @node.get(@key) == "#{@expires_at}:#{@token}"
219
+ raise "You never locked that mutex" unless @token
220
+ @node.hget(@key, 'token').to_i == @token
173
221
  end
174
222
  end
175
223
  end
@@ -0,0 +1,38 @@
1
+ class Promiscuous::Subscriber::MessageProcessor::Base
2
+ attr_accessor :message
3
+
4
+ def initialize(message)
5
+ self.message = message
6
+ end
7
+
8
+ def operations
9
+ message.parsed_payload['operations'].map { |op| operation_class.new(op) }
10
+ end
11
+
12
+ def self.process(*args)
13
+ raise "Same thread is processing a message?" if self.current
14
+
15
+ begin
16
+ self.current = new(*args)
17
+ self.current.on_message
18
+ ensure
19
+ self.current = nil
20
+ end
21
+ end
22
+
23
+ def self.current
24
+ Thread.current[:promiscuous_message_processor]
25
+ end
26
+
27
+ def self.current=(value)
28
+ Thread.current[:promiscuous_message_processor] = value
29
+ end
30
+
31
+ def on_message
32
+ raise "Must be implemented"
33
+ end
34
+
35
+ def operation_class
36
+ raise "Must be implemented"
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ class Promiscuous::Subscriber::MessageProcessor::Bootstrap < Promiscuous::Subscriber::MessageProcessor::Base
2
+ def on_message
3
+ if bootstrap_operation?
4
+ operations.each(&:execute)
5
+ else
6
+ # Postpone message by doing nothing
7
+ end
8
+ end
9
+
10
+ def bootstrap_operation?
11
+ operations.first.try(:operation) =~ /bootstrap/
12
+ end
13
+
14
+ def operation_class
15
+ Promiscuous::Subscriber::Operation::Bootstrap
16
+ end
17
+ end
@@ -0,0 +1,192 @@
1
+ class Promiscuous::Subscriber::MessageProcessor::Regular < Promiscuous::Subscriber::MessageProcessor::Base
2
+ delegate :write_dependencies, :read_dependencies, :dependencies, :to => :message
3
+
4
+ def nodes_with_deps
5
+ @nodes_with_deps ||= dependencies.group_by(&:redis_node)
6
+ end
7
+
8
+ def instance_dep
9
+ @instance_dep ||= write_dependencies.first
10
+ end
11
+
12
+ def master_node
13
+ @master_node ||= instance_dep.redis_node
14
+ end
15
+
16
+ def master_node_with_deps
17
+ @master_node_with_deps ||= nodes_with_deps.select { |node| node == master_node }.first
18
+ end
19
+
20
+ def secondary_nodes_with_deps
21
+ @secondary_nodes_with_deps ||= nodes_with_deps.reject { |node| node == master_node }.to_a
22
+ end
23
+
24
+ def recovery_key
25
+ # We use a recovery_key unique to the operation to avoid any trouble of
26
+ # touching another operation.
27
+ @recovery_key ||= instance_dep.key(:sub).join(instance_dep.version).to_s
28
+ end
29
+
30
+ def get_current_instance_version
31
+ master_node.get(instance_dep.key(:sub).join('rw').to_s).to_i
32
+ end
33
+
34
+ # XXX TODO Code is not tolerant to losing a lock.
35
+
36
+ def update_dependencies_on_node(node_with_deps, options={})
37
+ # Read and write dependencies are not handled the same way:
38
+ # * Read dependencies are just incremented (which allow parallelization).
39
+ # * Write dependencies are set to be max(current_version, received_version).
40
+ # This allow the version bootstrapping process to be non-atomic.
41
+ # Publishers upgrade their reads dependencies to write dependencies
42
+ # during bootstrapping to permit the mechanism to function properly.
43
+
44
+ # TODO Evaluate the performance hit of this heavy mechanism, and see if it's
45
+ # worth optimizing it for the non-bootstrap case.
46
+
47
+ node = node_with_deps[0]
48
+ r_deps = node_with_deps[1].select(&:read?)
49
+ w_deps = node_with_deps[1].select(&:write?)
50
+
51
+ if options[:only_write_dependencies]
52
+ r_deps = []
53
+ end
54
+
55
+ argv = []
56
+ argv << MultiJson.dump([r_deps.map { |dep| dep.key(:sub) },
57
+ w_deps.map { |dep| dep.key(:sub) },
58
+ w_deps.map { |dep| dep.version }])
59
+ argv << recovery_key if options[:with_recovery]
60
+
61
+ @@update_script_secondary ||= Promiscuous::Redis::Script.new <<-SCRIPT
62
+ local _args = cjson.decode(ARGV[1])
63
+ local read_deps = _args[1]
64
+ local write_deps = _args[2]
65
+ local write_versions = _args[3]
66
+ local recovery_key = ARGV[2]
67
+
68
+ if recovery_key and redis.call('exists', recovery_key) == 1 then
69
+ return
70
+ end
71
+
72
+ for i, _key in ipairs(read_deps) do
73
+ local key = _key .. ':rw'
74
+ local v = redis.call('incr', key)
75
+ redis.call('publish', key, v)
76
+ end
77
+
78
+ for i, _key in ipairs(write_deps) do
79
+ local key = _key .. ':rw'
80
+ local v = write_versions[i]
81
+ local current_version = tonumber(redis.call('get', key)) or 0
82
+ if current_version < v then
83
+ redis.call('set', key, v)
84
+ redis.call('publish', key, v)
85
+ end
86
+ end
87
+
88
+ if recovery_key then
89
+ redis.call('set', recovery_key, 'done')
90
+ end
91
+ SCRIPT
92
+
93
+ @@update_script_secondary.eval(node, :argv => argv)
94
+ end
95
+
96
+ def update_dependencies_master(options={})
97
+ update_dependencies_on_node(master_node_with_deps, options)
98
+ end
99
+
100
+ def update_dependencies_secondaries(options={})
101
+ secondary_nodes_with_deps.each do |node_with_deps|
102
+ update_dependencies_on_node(node_with_deps, options.merge(:with_recovery => true))
103
+ after_secondary_update_hook
104
+ end
105
+ end
106
+
107
+ def after_secondary_update_hook
108
+ # Hook only used for testing
109
+ end
110
+
111
+ def cleanup_dependency_secondaries
112
+ secondary_nodes_with_deps.each do |node, deps|
113
+ node.del(recovery_key)
114
+ end
115
+ end
116
+
117
+ def update_dependencies(options={})
118
+ # With multi nodes, we have to do a 2pc for the lock recovery mechanism:
119
+ # 1) We do the secondaries first, with a recovery token.
120
+ # 2) Then we do the master.
121
+ # 3) Then we cleanup the recovery token on secondaries.
122
+ update_dependencies_secondaries(options)
123
+ update_dependencies_master(options)
124
+ cleanup_dependency_secondaries
125
+ end
126
+
127
+ def check_for_duplicated_message
128
+ unless instance_dep.version >= get_current_instance_version + 1
129
+ # We happen to get a duplicate message, or we are recovering a dead
130
+ # worker. During regular operations, we just need to cleanup the 2pc (from
131
+ # the dead worker), and ack the message to rabbit.
132
+ # TODO Test cleanup
133
+ cleanup_dependency_secondaries
134
+
135
+ # But, if the message was generated during bootstrap, we don't really know
136
+ # if the other dependencies are up to date (because of the non-atomic
137
+ # bootstrapping process), so we do the max() trick (see in update_dependencies_on_node).
138
+ # Since such messages can come arbitrary late, we never really know if we
139
+ # can assume regular operations, thus we always assume that such message
140
+ # can originate from the bootstrapping period.
141
+ # Note that we are not in the happy path. Such duplicates messages are
142
+ # seldom: either (1) the publisher recovered a payload that didn't need
143
+ # recovery, or (2) a subscriber worker died after # update_dependencies_master,
144
+ # but before the message acking).
145
+ # It is thus okay to assume the worse and be inefficient.
146
+ update_dependencies(:only_write_dependencies => true)
147
+
148
+ message.ack
149
+
150
+ raise Promiscuous::Error::AlreadyProcessed
151
+ end
152
+ end
153
+
154
+ LOCK_OPTIONS = { :timeout => 1.5.minute, # after 1.5 minute, we give up
155
+ :sleep => 0.1, # polling every 100ms.
156
+ :expire => 1.minute } # after one minute, we are considered dead
157
+
158
+ def synchronize_and_update_dependencies
159
+ if Promiscuous::Config.bootstrap
160
+ else
161
+ lock_options = LOCK_OPTIONS.merge(:node => master_node)
162
+ mutex = Promiscuous::Redis::Mutex.new(instance_dep.key(:sub).to_s, lock_options)
163
+
164
+ unless mutex.lock
165
+ raise Promiscuous::Error::LockUnavailable.new(mutex.key)
166
+ end
167
+
168
+ begin
169
+ check_for_duplicated_message
170
+ yield
171
+ update_dependencies
172
+ message.ack
173
+ ensure
174
+ unless mutex.unlock
175
+ # TODO Be safe in case we have a duplicate message and lost the lock on it
176
+ raise "The subscriber lost the lock during its operation. It means that someone else\n"+
177
+ "received a duplicate message, and we got screwed.\n"
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ def on_message
184
+ self.synchronize_and_update_dependencies do
185
+ self.operations.each(&:execute)
186
+ end
187
+ end
188
+
189
+ def operation_class
190
+ Promiscuous::Subscriber::Operation::Regular
191
+ end
192
+ end
@@ -0,0 +1,4 @@
1
+ module Promiscuous::Subscriber::MessageProcessor
2
+ extend Promiscuous::Autoload
3
+ autoload :Base, :Regular, :Bootstrap
4
+ end
@@ -4,20 +4,23 @@ module Promiscuous::Subscriber::Model::Base
4
4
  def __promiscuous_update(payload, options={})
5
5
  self.class.subscribed_attrs.map(&:to_s).each do |attr|
6
6
  unless payload.attributes.has_key?(attr)
7
- raise "Attribute '#{attr}' is missing from the payload"
7
+ "Attribute '#{attr}' is missing from the payload".tap do |error_msg|
8
+ Promiscuous.warn "[receive] #{error_msg}"
9
+ raise error_msg
10
+ end
8
11
  end
9
12
 
10
13
  value = payload.attributes[attr]
11
14
  update = true
12
15
 
13
- attr_payload = Promiscuous::Subscriber::Payload.new(value)
16
+ attr_payload = Promiscuous::Subscriber::Operation::Regular.new(value)
14
17
  if model = attr_payload.model
15
18
  # Nested subscriber
16
19
  old_value = __send__(attr)
17
20
  instance = old_value || model.__promiscuous_fetch_new(attr_payload.id)
18
21
 
19
22
  if instance.class != model
20
- # Because of the nasty trick with '__promiscuous__/embedded_many'
23
+ # Because of the nasty trick with 'promiscuous_embedded_many'
21
24
  instance = model.__promiscuous_fetch_new(attr_payload.id)
22
25
  end
23
26
 
@@ -46,29 +49,31 @@ module Promiscuous::Subscriber::Model::Base
46
49
 
47
50
  # TODO reject invalid options
48
51
 
49
- if attributes.present? && self.subscribe_from && options[:from] && self.subscribe_from != options[:from]
50
- raise 'Subscribing from different publishers is not supported yet'
51
- end
52
+ self.subscribe_foreign_key = options[:foreign_key] if options[:foreign_key]
53
+
54
+ ([self] + descendants).each { |klass| klass.subscribed_attrs |= attributes }
52
55
 
53
- unless self.subscribe_from
54
- self.subscribe_from = options[:from] || ".*/#{self.name.underscore}"
55
- from_regexp = Regexp.new("^#{self.subscribe_from}$")
56
- Promiscuous::Subscriber::Model.mapping[from_regexp] = self
56
+ if self.subscribe_from && options[:from] && self.subscribe_from != options[:from]
57
+ raise 'Subscribing from different publishers is not supported yet'
57
58
  end
58
59
 
59
- self.subscribe_foreign_key = options[:foreign_key] if options[:foreign_key]
60
- @subscribe_as = options[:as].to_s if options[:as]
60
+ self.subscribe_from ||= options[:from].try(:to_s) || "*"
61
61
 
62
- ([self] + descendants).each { |klass| klass.subscribed_attrs |= attributes }
62
+ self.register_klass(options)
63
63
  end
64
64
 
65
- def subscribe_as
66
- @subscribe_as || name
65
+ def register_klass(options={})
66
+ subscribe_as = options[:as].try(:to_s) || self.name
67
+ return unless subscribe_as
68
+
69
+ Promiscuous::Subscriber::Model.mapping[self.subscribe_from] ||= {}
70
+ Promiscuous::Subscriber::Model.mapping[self.subscribe_from][subscribe_as] = self
67
71
  end
68
72
 
69
73
  def inherited(subclass)
70
74
  super
71
75
  subclass.subscribed_attrs = self.subscribed_attrs.dup
76
+ subclass.register_klass
72
77
  end
73
78
 
74
79
  class None; end
@@ -40,10 +40,10 @@ module Promiscuous::Subscriber::Model::Mongoid
40
40
  end
41
41
  end
42
42
 
43
- class EmbeddedMany
43
+ class EmbeddedDocs
44
44
  include Promiscuous::Subscriber::Model::Base
45
45
 
46
- subscribe :from => '__promiscuous__/embedded_many'
46
+ subscribe :as => 'Promiscuous::EmbeddedDocs'
47
47
 
48
48
  def __promiscuous_update(payload, options={})
49
49
  old_embeddeds = options[:old_value]
@@ -58,7 +58,7 @@ module Promiscuous::Subscriber::Model::Mongoid
58
58
  new_e['existed'] = true
59
59
  old_e.instance_variable_set(:@keep, true)
60
60
 
61
- payload = Promiscuous::Subscriber::Payload.new(new_e)
61
+ payload = Promiscuous::Subscriber::Operation::Regular.new(new_e)
62
62
  old_e.__promiscuous_update(payload, :old_value => old_e)
63
63
  end
64
64
  end
@@ -70,7 +70,7 @@ module Promiscuous::Subscriber::Model::Mongoid
70
70
 
71
71
  # create all the new ones
72
72
  new_embeddeds.reject { |new_e| new_e['existed'] }.each do |new_e|
73
- payload = Promiscuous::Subscriber::Payload.new(new_e)
73
+ payload = Promiscuous::Subscriber::Operation::Regular.new(new_e)
74
74
  new_e_instance = payload.model. __promiscuous_fetch_new(payload.id)
75
75
  new_e_instance.__promiscuous_update(payload)
76
76
  options[:parent].__send__(old_embeddeds.metadata[:name]) << new_e_instance
@@ -5,12 +5,26 @@ module Promiscuous::Subscriber::Model::Observer
5
5
  included do
6
6
  extend ActiveModel::Callbacks
7
7
  attr_accessor :id
8
- define_model_callbacks :create, :update, :destroy, :only => :after
8
+ define_model_callbacks :save, :create, :update, :destroy, :only => :after
9
9
  end
10
10
 
11
11
  def __promiscuous_update(payload, options={})
12
12
  super
13
- run_callbacks payload.operation
13
+ case payload.operation
14
+ when :create
15
+ run_callbacks :create
16
+ run_callbacks :save
17
+ when :update
18
+ run_callbacks :update
19
+ run_callbacks :save
20
+ when :destroy
21
+ run_callbacks :destroy
22
+ when :bootstrap_data
23
+ run_callbacks :create
24
+ run_callbacks :save
25
+ else
26
+ raise "Unknown operation #{payload.operation}"
27
+ end
14
28
  end
15
29
 
16
30
  def destroy