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.
- 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
|