promiscuous 0.90.0 → 0.91.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 (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