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