promiscuous 0.92.0 → 0.100.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/promiscuous.rb +2 -5
- data/lib/promiscuous/amqp.rb +1 -2
- data/lib/promiscuous/cli.rb +3 -43
- data/lib/promiscuous/config.rb +5 -7
- data/lib/promiscuous/error/dependency.rb +1 -3
- data/lib/promiscuous/publisher/context.rb +1 -1
- data/lib/promiscuous/publisher/context/base.rb +3 -34
- data/lib/promiscuous/publisher/model/base.rb +5 -25
- data/lib/promiscuous/publisher/model/mock.rb +5 -7
- data/lib/promiscuous/publisher/operation/active_record.rb +4 -69
- data/lib/promiscuous/publisher/operation/atomic.rb +1 -3
- data/lib/promiscuous/publisher/operation/base.rb +33 -123
- data/lib/promiscuous/publisher/operation/mongoid.rb +0 -67
- data/lib/promiscuous/publisher/operation/non_persistent.rb +0 -1
- data/lib/promiscuous/publisher/operation/transaction.rb +1 -3
- data/lib/promiscuous/railtie.rb +0 -31
- data/lib/promiscuous/subscriber.rb +1 -1
- data/lib/promiscuous/subscriber/{worker/message.rb → message.rb} +12 -40
- data/lib/promiscuous/subscriber/model/active_record.rb +1 -1
- data/lib/promiscuous/subscriber/model/base.rb +4 -4
- data/lib/promiscuous/subscriber/model/mongoid.rb +3 -3
- data/lib/promiscuous/subscriber/operation.rb +74 -3
- data/lib/promiscuous/subscriber/unit_of_work.rb +110 -0
- data/lib/promiscuous/subscriber/worker.rb +3 -7
- data/lib/promiscuous/subscriber/worker/eventual_destroyer.rb +2 -6
- data/lib/promiscuous/subscriber/worker/pump.rb +2 -11
- data/lib/promiscuous/version.rb +1 -1
- metadata +18 -36
- data/lib/promiscuous/error/missing_context.rb +0 -29
- data/lib/promiscuous/publisher/bootstrap.rb +0 -27
- data/lib/promiscuous/publisher/bootstrap/connection.rb +0 -25
- data/lib/promiscuous/publisher/bootstrap/data.rb +0 -127
- data/lib/promiscuous/publisher/bootstrap/mode.rb +0 -19
- data/lib/promiscuous/publisher/bootstrap/status.rb +0 -40
- data/lib/promiscuous/publisher/bootstrap/version.rb +0 -46
- data/lib/promiscuous/publisher/context/middleware.rb +0 -94
- data/lib/promiscuous/resque.rb +0 -12
- data/lib/promiscuous/sidekiq.rb +0 -15
- data/lib/promiscuous/subscriber/message_processor.rb +0 -4
- data/lib/promiscuous/subscriber/message_processor/base.rb +0 -54
- data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +0 -17
- data/lib/promiscuous/subscriber/message_processor/regular.rb +0 -238
- data/lib/promiscuous/subscriber/operation/base.rb +0 -66
- data/lib/promiscuous/subscriber/operation/bootstrap.rb +0 -60
- data/lib/promiscuous/subscriber/operation/regular.rb +0 -19
- data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +0 -333
@@ -1,60 +0,0 @@
|
|
1
|
-
class Promiscuous::Subscriber::Operation::Bootstrap < Promiscuous::Subscriber::Operation::Base
|
2
|
-
# TODO Here's what's left to do:
|
3
|
-
# - Automatic switching from pass1, pass2, live
|
4
|
-
# - Unbinding the bootstrap exchange when going live, and reset prefetch
|
5
|
-
# during the version bootstrap phase.
|
6
|
-
# - CLI interface and progress bars
|
7
|
-
|
8
|
-
def bootstrap_versions
|
9
|
-
operations = message.parsed_payload['operations']
|
10
|
-
|
11
|
-
operations.map { |op| op['keys'] }.flatten.map { |k| Promiscuous::Dependency.parse(k, :owner => message.app) }.group_by(&:redis_node).each do |node, deps|
|
12
|
-
node.mset(deps.map { |dep| [dep.key(:sub).join('rw').to_s, dep.version] }.flatten)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def bootstrap_data
|
17
|
-
create :on_already_created => proc { update }
|
18
|
-
end
|
19
|
-
|
20
|
-
def on_bootstrap_operation(wanted_operation, options={})
|
21
|
-
if operation == wanted_operation
|
22
|
-
yield
|
23
|
-
options[:always_postpone] ? message.postpone : message.ack
|
24
|
-
else
|
25
|
-
message.postpone
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def execute
|
30
|
-
case Promiscuous::Config.bootstrap
|
31
|
-
when :pass1
|
32
|
-
# The first thing to do is to receive and save an non atomic snapshot of
|
33
|
-
# the publisher's versions.
|
34
|
-
on_bootstrap_operation(:bootstrap_versions) { bootstrap_versions }
|
35
|
-
|
36
|
-
when :pass2
|
37
|
-
# Then we move on to save the raw data, but skipping the message if we get
|
38
|
-
# a mismatch on the version.
|
39
|
-
on_bootstrap_operation(:bootstrap_data) { bootstrap_data }
|
40
|
-
|
41
|
-
when :pass3
|
42
|
-
# Finally, we create the rows that we've skipped, we postpone them to make
|
43
|
-
# our lives easier. We'll detect the message as duplicates when re-processed.
|
44
|
-
# on_bootstrap_operation(:update, :always_postpone => true) { bootstrap_missing_data if model }
|
45
|
-
# TODO unbind the bootstrap exchange
|
46
|
-
else
|
47
|
-
raise "Invalid operation received: #{operation}"
|
48
|
-
end
|
49
|
-
rescue Exception => e
|
50
|
-
if Promiscuous::Config.ignore_exceptions
|
51
|
-
Promiscuous.warn "[receive] error while proceessing message but message still processed: #{e}\n#{e.backtrace.join("\n")}"
|
52
|
-
else
|
53
|
-
raise e
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def message_processor
|
58
|
-
@message_processor ||= Promiscuous::Subscriber::MessageProcessor::Bootstrap.current
|
59
|
-
end
|
60
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
class Promiscuous::Subscriber::Operation::Regular < Promiscuous::Subscriber::Operation::Base
|
2
|
-
def execute
|
3
|
-
case operation
|
4
|
-
when :create then create if model
|
5
|
-
when :update then update if model
|
6
|
-
when :destroy then destroy if model
|
7
|
-
end
|
8
|
-
rescue Exception => e
|
9
|
-
if Promiscuous::Config.ignore_exceptions && !e.is_a?(NameError)
|
10
|
-
Promiscuous.warn "[receive] error while proceessing message but message still processed: #{e}\n#{e.backtrace.join("\n")}"
|
11
|
-
else
|
12
|
-
raise e
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def message_processor
|
17
|
-
@message_processor ||= Promiscuous::Subscriber::MessageProcessor::Regular.current
|
18
|
-
end
|
19
|
-
end
|
@@ -1,333 +0,0 @@
|
|
1
|
-
module ::Containers; end
|
2
|
-
require 'containers/priority_queue'
|
3
|
-
|
4
|
-
class Promiscuous::Subscriber::Worker::MessageSynchronizer
|
5
|
-
RECONNECT_INTERVAL = 2
|
6
|
-
CLEANUP_INTERVAL = 100 # messages
|
7
|
-
QUEUE_MAX_AGE = 100 # messages
|
8
|
-
|
9
|
-
attr_accessor :redis, :node_synchronizers, :num_processed_messages
|
10
|
-
|
11
|
-
def initialize(root)
|
12
|
-
@root = root
|
13
|
-
@node_synchronizers = {}
|
14
|
-
@lock = Mutex.new
|
15
|
-
@reconnect_timer = Promiscuous::Timer.new("redis", RECONNECT_INTERVAL) { reconnect }
|
16
|
-
end
|
17
|
-
|
18
|
-
def connected?
|
19
|
-
!!@redis
|
20
|
-
end
|
21
|
-
|
22
|
-
def connect
|
23
|
-
@lock.synchronize do
|
24
|
-
return unless !connected?
|
25
|
-
|
26
|
-
@num_processed_messages = 0
|
27
|
-
redis = Promiscuous::Redis.new_blocking_connection
|
28
|
-
redis.nodes.each { |node| @node_synchronizers[node] = NodeSynchronizer.new(self, node) }
|
29
|
-
@redis = redis
|
30
|
-
|
31
|
-
@message_queue = Queue.new
|
32
|
-
@processor_thread = Thread.new { queue_process_main_loop }
|
33
|
-
end
|
34
|
-
# Do not recover messages while bootstrapping as there are a very large
|
35
|
-
# number of messages that remain un-acked. If bootstrap messages are missed
|
36
|
-
# these will be caught in the final phase of bootstrapping (see
|
37
|
-
# Promiscuous::Subscriber::Operation).
|
38
|
-
@root.pump.recover unless Promiscuous::Config.bootstrap
|
39
|
-
end
|
40
|
-
|
41
|
-
def disconnect
|
42
|
-
@lock.synchronize do
|
43
|
-
return unless connected?
|
44
|
-
|
45
|
-
@redis, redis = nil, @redis
|
46
|
-
@node_synchronizers.values.each { |node_synchronizer| node_synchronizer.stop_main_loop }
|
47
|
-
@node_synchronizers.clear
|
48
|
-
redis.quit
|
49
|
-
@processor_thread.kill
|
50
|
-
end
|
51
|
-
rescue Exception
|
52
|
-
end
|
53
|
-
|
54
|
-
def reconnect
|
55
|
-
self.disconnect
|
56
|
-
self.connect
|
57
|
-
@reconnect_timer.reset
|
58
|
-
Promiscuous.warn "[redis] Reconnected"
|
59
|
-
end
|
60
|
-
|
61
|
-
def rescue_connection(node, exception)
|
62
|
-
# TODO stop the pump to unack all messages
|
63
|
-
@reconnect_timer.start
|
64
|
-
|
65
|
-
e = Promiscuous::Redis.lost_connection_exception(node, :inner => exception)
|
66
|
-
Promiscuous.warn "[redis] #{e}. Reconnecting..."
|
67
|
-
Promiscuous::Config.error_notifier.call(e)
|
68
|
-
end
|
69
|
-
|
70
|
-
# process_when_ready() is called by the AMQP pump. This is what happens:
|
71
|
-
# 1. First, we subscribe to redis and wait for the confirmation.
|
72
|
-
# 2. Then we check if the version in redis is old enough to process the message.
|
73
|
-
# If not we bail out and rely on the subscription to kick the processing.
|
74
|
-
# Because we subscribed in advanced, we will not miss the notification.
|
75
|
-
def process_when_ready(msg)
|
76
|
-
unless Promiscuous::Config.consistency == :causal && msg.has_dependencies?
|
77
|
-
process_message!(msg)
|
78
|
-
return
|
79
|
-
end
|
80
|
-
|
81
|
-
# Dropped messages will be redelivered as we (re)connect
|
82
|
-
return unless self.redis
|
83
|
-
|
84
|
-
@message_queue.push([msg, msg.happens_before_dependencies.dup])
|
85
|
-
end
|
86
|
-
|
87
|
-
def queue_process_main_loop
|
88
|
-
loop do
|
89
|
-
msg, deps = @message_queue.pop
|
90
|
-
|
91
|
-
if dep = deps.pop
|
92
|
-
get_redis = dep.redis_node
|
93
|
-
subscriber_redis = dep.redis_node(@redis)
|
94
|
-
|
95
|
-
key = dep.key(:sub).join('rw').to_s
|
96
|
-
version = dep.version
|
97
|
-
node_synchronizer = @node_synchronizers[subscriber_redis]
|
98
|
-
node_synchronizer.on_version(subscriber_redis, get_redis, key, version, msg) { @message_queue.push([msg, deps]) }
|
99
|
-
else
|
100
|
-
process_message!(msg)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def process_message!(msg)
|
106
|
-
@root.runner.messages_to_process << msg
|
107
|
-
|
108
|
-
return unless msg.has_dependencies?
|
109
|
-
|
110
|
-
cleanup = false
|
111
|
-
@lock.synchronize do
|
112
|
-
@num_processed_messages += 1
|
113
|
-
cleanup = @num_processed_messages % CLEANUP_INTERVAL == 0
|
114
|
-
end
|
115
|
-
@node_synchronizers.values.each(&:cleanup_if_old) if @node_synchronizers && cleanup
|
116
|
-
end
|
117
|
-
|
118
|
-
def blocked_messages
|
119
|
-
@node_synchronizers.values
|
120
|
-
.map { |node_synchronizer| node_synchronizer.blocked_messages }
|
121
|
-
.flatten
|
122
|
-
.uniq
|
123
|
-
.sort_by { |msg| msg.timestamp }
|
124
|
-
end
|
125
|
-
|
126
|
-
class NodeSynchronizer
|
127
|
-
attr_accessor :node, :subscriptions, :root_synchronizer
|
128
|
-
|
129
|
-
def initialize(root_synchronizer, node)
|
130
|
-
@root_synchronizer = root_synchronizer
|
131
|
-
@node = node
|
132
|
-
@subscriptions = {}
|
133
|
-
@subscriptions_lock = Mutex.new
|
134
|
-
@thread = Thread.new { main_loop }
|
135
|
-
end
|
136
|
-
|
137
|
-
def main_loop
|
138
|
-
redis_client = @node.client
|
139
|
-
|
140
|
-
loop do
|
141
|
-
reply = redis_client.read
|
142
|
-
raise reply if reply.is_a?(Redis::CommandError)
|
143
|
-
type, subscription, arg = reply
|
144
|
-
|
145
|
-
case type
|
146
|
-
when 'subscribe'
|
147
|
-
notify_subscription(subscription)
|
148
|
-
when 'unsubscribe'
|
149
|
-
when 'message'
|
150
|
-
notify_key_change(subscription, arg)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
rescue EOFError, Errno::ECONNRESET => e
|
154
|
-
# Unwanted disconnection
|
155
|
-
@root_synchronizer.rescue_connection(redis_client, e) unless @stop
|
156
|
-
rescue Exception => e
|
157
|
-
unless @stop
|
158
|
-
Promiscuous.warn "[redis] #{e.class} #{e.message}"
|
159
|
-
Promiscuous.warn "[redis] #{e}\n#{e.backtrace.join("\n")}"
|
160
|
-
|
161
|
-
Promiscuous::Config.error_notifier.call(e)
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
def stop_main_loop
|
166
|
-
@stop = true
|
167
|
-
@thread.kill
|
168
|
-
end
|
169
|
-
|
170
|
-
def on_version(subscriber_redis, get_redis, key, version, message, &callback)
|
171
|
-
# subscriber_redis and get_redis are different connections to the
|
172
|
-
# same node.
|
173
|
-
if version == 0
|
174
|
-
callback.call
|
175
|
-
else
|
176
|
-
sub = get_subscription(subscriber_redis, get_redis, key)
|
177
|
-
sub.subscribe
|
178
|
-
sub.add_callback(Subscription::Callback.new(version, callback, message))
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
def blocked_messages
|
183
|
-
@subscriptions_lock.synchronize do
|
184
|
-
@subscriptions.values
|
185
|
-
.map(&:callbacks)
|
186
|
-
.map(&:next)
|
187
|
-
.compact
|
188
|
-
.map(&:message)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
def notify_subscription(key)
|
193
|
-
find_subscription(key).try(:finalize_subscription)
|
194
|
-
end
|
195
|
-
|
196
|
-
def notify_key_change(key, version)
|
197
|
-
find_subscription(key).try(:signal_version, version)
|
198
|
-
end
|
199
|
-
|
200
|
-
def remove_subscription(key)
|
201
|
-
@subscriptions_lock.synchronize do
|
202
|
-
@subscriptions.delete(key)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
def find_subscription(key)
|
207
|
-
@subscriptions_lock.synchronize do
|
208
|
-
@subscriptions[key]
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def get_subscription(subscriber_redis, get_redis, key)
|
213
|
-
@subscriptions_lock.synchronize do
|
214
|
-
@subscriptions[key] ||= Subscription.new(self, subscriber_redis, get_redis, key)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def cleanup_if_old
|
219
|
-
@subscriptions_lock.synchronize do
|
220
|
-
@subscriptions.values.each(&:cleanup_if_old)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
class Subscription
|
225
|
-
attr_accessor :node_synchronizer, :subscriber_redis, :get_redis, :key, :callbacks, :last_version
|
226
|
-
|
227
|
-
def initialize(node_synchronizer, subscriber_redis, get_redis, key)
|
228
|
-
self.node_synchronizer = node_synchronizer
|
229
|
-
self.subscriber_redis = subscriber_redis
|
230
|
-
self.get_redis = get_redis
|
231
|
-
self.key = key
|
232
|
-
|
233
|
-
@subscription_requested = false
|
234
|
-
# We use a priority queue that returns the smallest value first
|
235
|
-
@callbacks = Containers::PriorityQueue.new { |x, y| x < y }
|
236
|
-
@last_version = 0
|
237
|
-
@lock = Mutex.new
|
238
|
-
|
239
|
-
refresh_activity
|
240
|
-
end
|
241
|
-
|
242
|
-
def with_rescue_connection(node, &block)
|
243
|
-
block.call
|
244
|
-
rescue Exception => e
|
245
|
-
# TODO only catch exceptions related to network issues
|
246
|
-
node_synchronizer.root_synchronizer.rescue_connection(node, e)
|
247
|
-
end
|
248
|
-
|
249
|
-
def redis_exec_raw(node, *commands)
|
250
|
-
with_rescue_connection(node) { node.client.process([commands]) }
|
251
|
-
end
|
252
|
-
|
253
|
-
def total_num_processed_messages
|
254
|
-
node_synchronizer.root_synchronizer.num_processed_messages
|
255
|
-
end
|
256
|
-
|
257
|
-
def refresh_activity
|
258
|
-
@last_activity_at = total_num_processed_messages
|
259
|
-
end
|
260
|
-
|
261
|
-
def is_old?
|
262
|
-
delta = total_num_processed_messages - @last_activity_at
|
263
|
-
@callbacks.empty? && delta >= QUEUE_MAX_AGE
|
264
|
-
end
|
265
|
-
|
266
|
-
def cleanup_if_old
|
267
|
-
if is_old?
|
268
|
-
redis_exec_raw(subscriber_redis, :unsubscribe, key)
|
269
|
-
node_synchronizer.subscriptions.delete(key) # lock is already held
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
def subscribe
|
274
|
-
@lock.synchronize do
|
275
|
-
return if @subscription_requested
|
276
|
-
@subscription_requested = true
|
277
|
-
end
|
278
|
-
|
279
|
-
redis_exec_raw(subscriber_redis, :subscribe, key)
|
280
|
-
end
|
281
|
-
|
282
|
-
def finalize_subscription
|
283
|
-
v = with_rescue_connection(get_redis) { get_redis.get(key) }
|
284
|
-
signal_version(v)
|
285
|
-
end
|
286
|
-
|
287
|
-
def signal_version(current_version)
|
288
|
-
current_version = current_version.to_i
|
289
|
-
@lock.synchronize do
|
290
|
-
return if current_version < @last_version
|
291
|
-
@last_version = current_version
|
292
|
-
end
|
293
|
-
|
294
|
-
loop do
|
295
|
-
next_cb = nil
|
296
|
-
@lock.synchronize do
|
297
|
-
next_cb = @callbacks.next
|
298
|
-
return unless next_cb && next_cb.can_perform?(@last_version)
|
299
|
-
@callbacks.pop
|
300
|
-
end
|
301
|
-
next_cb.perform
|
302
|
-
end
|
303
|
-
end
|
304
|
-
|
305
|
-
def add_callback(callback)
|
306
|
-
refresh_activity
|
307
|
-
|
308
|
-
can_perform_immediately = false
|
309
|
-
@lock.synchronize do
|
310
|
-
if callback.can_perform?(@last_version)
|
311
|
-
can_perform_immediately = true
|
312
|
-
else
|
313
|
-
@callbacks.push(callback, callback.version)
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
callback.perform if can_perform_immediately
|
318
|
-
end
|
319
|
-
|
320
|
-
class Callback < Struct.new(:version, :callback, :message)
|
321
|
-
# message is just here for debugging, not used in the happy path
|
322
|
-
def can_perform?(current_version)
|
323
|
-
# The message synchronizer takes care of happens before dependencies.
|
324
|
-
current_version >= self.version
|
325
|
-
end
|
326
|
-
|
327
|
-
def perform
|
328
|
-
callback.call
|
329
|
-
end
|
330
|
-
end
|
331
|
-
end
|
332
|
-
end
|
333
|
-
end
|