promiscuous 0.92.0 → 0.100.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/promiscuous.rb +2 -5
  3. data/lib/promiscuous/amqp.rb +1 -2
  4. data/lib/promiscuous/cli.rb +3 -43
  5. data/lib/promiscuous/config.rb +5 -7
  6. data/lib/promiscuous/error/dependency.rb +1 -3
  7. data/lib/promiscuous/publisher/context.rb +1 -1
  8. data/lib/promiscuous/publisher/context/base.rb +3 -34
  9. data/lib/promiscuous/publisher/model/base.rb +5 -25
  10. data/lib/promiscuous/publisher/model/mock.rb +5 -7
  11. data/lib/promiscuous/publisher/operation/active_record.rb +4 -69
  12. data/lib/promiscuous/publisher/operation/atomic.rb +1 -3
  13. data/lib/promiscuous/publisher/operation/base.rb +33 -123
  14. data/lib/promiscuous/publisher/operation/mongoid.rb +0 -67
  15. data/lib/promiscuous/publisher/operation/non_persistent.rb +0 -1
  16. data/lib/promiscuous/publisher/operation/transaction.rb +1 -3
  17. data/lib/promiscuous/railtie.rb +0 -31
  18. data/lib/promiscuous/subscriber.rb +1 -1
  19. data/lib/promiscuous/subscriber/{worker/message.rb → message.rb} +12 -40
  20. data/lib/promiscuous/subscriber/model/active_record.rb +1 -1
  21. data/lib/promiscuous/subscriber/model/base.rb +4 -4
  22. data/lib/promiscuous/subscriber/model/mongoid.rb +3 -3
  23. data/lib/promiscuous/subscriber/operation.rb +74 -3
  24. data/lib/promiscuous/subscriber/unit_of_work.rb +110 -0
  25. data/lib/promiscuous/subscriber/worker.rb +3 -7
  26. data/lib/promiscuous/subscriber/worker/eventual_destroyer.rb +2 -6
  27. data/lib/promiscuous/subscriber/worker/pump.rb +2 -11
  28. data/lib/promiscuous/version.rb +1 -1
  29. metadata +18 -36
  30. data/lib/promiscuous/error/missing_context.rb +0 -29
  31. data/lib/promiscuous/publisher/bootstrap.rb +0 -27
  32. data/lib/promiscuous/publisher/bootstrap/connection.rb +0 -25
  33. data/lib/promiscuous/publisher/bootstrap/data.rb +0 -127
  34. data/lib/promiscuous/publisher/bootstrap/mode.rb +0 -19
  35. data/lib/promiscuous/publisher/bootstrap/status.rb +0 -40
  36. data/lib/promiscuous/publisher/bootstrap/version.rb +0 -46
  37. data/lib/promiscuous/publisher/context/middleware.rb +0 -94
  38. data/lib/promiscuous/resque.rb +0 -12
  39. data/lib/promiscuous/sidekiq.rb +0 -15
  40. data/lib/promiscuous/subscriber/message_processor.rb +0 -4
  41. data/lib/promiscuous/subscriber/message_processor/base.rb +0 -54
  42. data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +0 -17
  43. data/lib/promiscuous/subscriber/message_processor/regular.rb +0 -238
  44. data/lib/promiscuous/subscriber/operation/base.rb +0 -66
  45. data/lib/promiscuous/subscriber/operation/bootstrap.rb +0 -60
  46. data/lib/promiscuous/subscriber/operation/regular.rb +0 -19
  47. 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