promiscuous 0.92.0 → 0.100.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 +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
|