promiscuous 0.90.0 → 0.91.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/promiscuous/amqp/bunny.rb +63 -36
- data/lib/promiscuous/amqp/fake.rb +3 -1
- data/lib/promiscuous/amqp/hot_bunnies.rb +26 -16
- data/lib/promiscuous/amqp/null.rb +1 -0
- data/lib/promiscuous/amqp.rb +12 -12
- data/lib/promiscuous/cli.rb +70 -29
- data/lib/promiscuous/config.rb +54 -29
- data/lib/promiscuous/convenience.rb +1 -1
- data/lib/promiscuous/dependency.rb +25 -6
- data/lib/promiscuous/error/connection.rb +11 -9
- data/lib/promiscuous/error/dependency.rb +8 -1
- data/lib/promiscuous/loader.rb +4 -2
- data/lib/promiscuous/publisher/bootstrap/connection.rb +25 -0
- data/lib/promiscuous/publisher/bootstrap/data.rb +127 -0
- data/lib/promiscuous/publisher/bootstrap/mode.rb +19 -0
- data/lib/promiscuous/publisher/bootstrap/status.rb +40 -0
- data/lib/promiscuous/publisher/bootstrap/version.rb +46 -0
- data/lib/promiscuous/publisher/bootstrap.rb +27 -0
- data/lib/promiscuous/publisher/context/base.rb +67 -0
- data/lib/promiscuous/{middleware.rb → publisher/context/middleware.rb} +16 -13
- data/lib/promiscuous/publisher/context/transaction.rb +36 -0
- data/lib/promiscuous/publisher/context.rb +4 -88
- data/lib/promiscuous/publisher/mock_generator.rb +9 -9
- data/lib/promiscuous/publisher/model/active_record.rb +7 -7
- data/lib/promiscuous/publisher/model/base.rb +29 -29
- data/lib/promiscuous/publisher/model/ephemeral.rb +5 -3
- data/lib/promiscuous/publisher/model/mock.rb +9 -5
- data/lib/promiscuous/publisher/model/mongoid.rb +5 -22
- data/lib/promiscuous/publisher/operation/active_record.rb +360 -0
- data/lib/promiscuous/publisher/operation/atomic.rb +167 -0
- data/lib/promiscuous/publisher/operation/base.rb +279 -474
- data/lib/promiscuous/publisher/operation/mongoid.rb +153 -145
- data/lib/promiscuous/publisher/operation/non_persistent.rb +28 -0
- data/lib/promiscuous/publisher/operation/proxy_for_query.rb +42 -0
- data/lib/promiscuous/publisher/operation/transaction.rb +85 -0
- data/lib/promiscuous/publisher/operation.rb +1 -1
- data/lib/promiscuous/publisher/worker.rb +7 -7
- data/lib/promiscuous/publisher.rb +1 -1
- data/lib/promiscuous/railtie.rb +20 -5
- data/lib/promiscuous/redis.rb +104 -56
- data/lib/promiscuous/subscriber/message_processor/base.rb +38 -0
- data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +17 -0
- data/lib/promiscuous/subscriber/message_processor/regular.rb +192 -0
- data/lib/promiscuous/subscriber/message_processor.rb +4 -0
- data/lib/promiscuous/subscriber/model/base.rb +20 -15
- data/lib/promiscuous/subscriber/model/mongoid.rb +4 -4
- data/lib/promiscuous/subscriber/model/observer.rb +16 -2
- data/lib/promiscuous/subscriber/operation/base.rb +68 -0
- data/lib/promiscuous/subscriber/operation/bootstrap.rb +54 -0
- data/lib/promiscuous/subscriber/operation/regular.rb +13 -0
- data/lib/promiscuous/subscriber/operation.rb +3 -166
- data/lib/promiscuous/subscriber/worker/message.rb +61 -35
- data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +90 -59
- data/lib/promiscuous/subscriber/worker/pump.rb +17 -5
- data/lib/promiscuous/subscriber/worker/recorder.rb +4 -1
- data/lib/promiscuous/subscriber/worker/runner.rb +49 -9
- data/lib/promiscuous/subscriber/worker/stats.rb +2 -2
- data/lib/promiscuous/subscriber/worker.rb +6 -0
- data/lib/promiscuous/subscriber.rb +1 -1
- data/lib/promiscuous/timer.rb +31 -18
- data/lib/promiscuous/version.rb +1 -1
- data/lib/promiscuous.rb +23 -3
- metadata +104 -89
- data/lib/promiscuous/subscriber/payload.rb +0 -34
data/lib/promiscuous/railtie.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Promiscuous::Railtie < Rails::Railtie
|
2
2
|
initializer 'load promiscuous' do
|
3
|
-
|
4
|
-
|
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
|
-
|
26
|
-
|
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
|
data/lib/promiscuous/redis.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
#
|
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.
|
57
|
-
Promiscuous
|
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.
|
61
|
-
Promiscuous::
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
144
|
+
local token_key = KEYS[2]
|
145
|
+
local lock_set = KEYS[3]
|
130
146
|
local now = tonumber(ARGV[1])
|
131
|
-
local
|
132
|
-
local
|
133
|
-
|
134
|
-
local
|
135
|
-
|
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
|
-
|
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
|
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,
|
144
|
-
|
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 @
|
151
|
-
# remains the same, and do not release
|
152
|
-
@@unlock_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
|
156
|
-
local
|
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('
|
203
|
+
if redis.call('hget', key, 'token') == token then
|
161
204
|
redis.call('del', key)
|
162
|
-
if lock_set then
|
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
|
-
|
168
|
-
@@unlock_script.eval(@node, :keys => [@key, @lock_set], :argv => [@
|
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
|
-
|
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
|
@@ -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
|
-
|
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::
|
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 '
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
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.
|
60
|
-
@subscribe_as = options[:as].to_s if options[:as]
|
60
|
+
self.subscribe_from ||= options[:from].try(:to_s) || "*"
|
61
61
|
|
62
|
-
(
|
62
|
+
self.register_klass(options)
|
63
63
|
end
|
64
64
|
|
65
|
-
def
|
66
|
-
|
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
|
43
|
+
class EmbeddedDocs
|
44
44
|
include Promiscuous::Subscriber::Model::Base
|
45
45
|
|
46
|
-
subscribe :
|
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::
|
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::
|
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
|
-
|
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
|