promiscuous 0.51.0 → 0.52.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,47 @@
1
+ require 'eventmachine'
2
+ require 'amqp'
3
+
1
4
  module Promiscuous::AMQP::RubyAMQP
2
- mattr_accessor :channel
5
+ class Synchronizer
6
+ def initialize
7
+ @mutex = Mutex.new
8
+ @condition = ConditionVariable.new
9
+ @signaled = false
10
+ end
11
+
12
+ def wait
13
+ @mutex.synchronize do
14
+ loop do
15
+ return if @signaled
16
+ @condition.wait(@mutex)
17
+ end
18
+ end
19
+ end
20
+
21
+ def signal
22
+ @mutex.synchronize do
23
+ @signaled = true
24
+ @condition.signal
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.maybe_start_event_machine
30
+ return if EM.reactor_running?
31
+
32
+ EM.error_handler { |e| Promiscuous::Config.error_notifier.try(:call, e) }
33
+ em_sync = Synchronizer.new
34
+ @event_machine_thread = Thread.new { EM.run { em_sync.signal } }
35
+ em_sync.wait
36
+ end
3
37
 
4
38
  def self.connect
5
- require 'amqp'
39
+ return if @connection
40
+
41
+ @channels = {}
42
+ @exchanges = {}
43
+
44
+ maybe_start_event_machine
6
45
 
7
46
  amqp_options = if Promiscuous::Config.amqp_url
8
47
  url = URI.parse(Promiscuous::Config.amqp_url)
@@ -19,67 +58,83 @@ module Promiscuous::AMQP::RubyAMQP
19
58
  }
20
59
  end
21
60
 
22
- connection = ::AMQP.connect(amqp_options)
23
- self.channel = ::AMQP::Channel.new(connection,
24
- :auto_recovery => true,
25
- :prefetch => Promiscuous::Config.prefetch)
26
-
27
- connection.on_tcp_connection_loss do |conn|
28
- unless conn.reconnecting?
29
- e = Promiscuous::AMQP.lost_connection_exception
30
- Promiscuous.warn "[amqp] #{e}. Reconnecting..."
31
- Promiscuous::Config.error_notifier.try(:call, e)
32
- conn.periodically_reconnect(2.seconds)
61
+ channel_sync = Synchronizer.new
62
+ ::AMQP.connect(amqp_options) do |connection|
63
+ @connection = connection
64
+ @connection.on_tcp_connection_loss do |conn|
65
+ unless conn.reconnecting?
66
+ e = Promiscuous::AMQP.lost_connection_exception
67
+ Promiscuous.warn "[amqp] #{e}. Reconnecting..."
68
+ Promiscuous::Config.error_notifier.try(:call, e)
69
+ conn.periodically_reconnect(2.seconds)
70
+ end
33
71
  end
34
- end
35
72
 
36
- connection.on_recovery do |conn|
37
- Promiscuous.warn "[amqp] Reconnected"
38
- Promiscuous::AMQP::RubyAMQP.channel.recover
39
- end
73
+ @connection.on_recovery do |conn|
74
+ Promiscuous.warn "[amqp] Reconnected"
75
+ @channels.values.each(&:recover) if conn == @connection
76
+ end
77
+
78
+ @connection.on_error do |conn, conn_close|
79
+ # No need to handle CONNECTION_FORCED since on_tcp_connection_loss takes
80
+ # care of it.
81
+ Promiscuous.warn "[amqp] #{conn_close.reply_text}"
82
+ end
40
83
 
41
- connection.on_error do |conn, conn_close|
42
- # No need to handle CONNECTION_FORCED since on_tcp_connection_loss takes
43
- # care of it.
44
- Promiscuous.warn "[amqp] #{conn_close.reply_text}"
84
+ get_channel(:master) { channel_sync.signal }
45
85
  end
86
+ channel_sync.wait
87
+ rescue Exception => e
88
+ self.disconnect
89
+ raise e
46
90
  end
47
91
 
48
- def self.disconnect
49
- if self.channel && self.channel.connection.connected?
50
- self.channel.connection.close
51
- self.channel.close
92
+ def self.get_channel(name, &block)
93
+ if @channels[name]
94
+ yield(@channels[name]) if block_given?
95
+ @channels[name]
96
+ else
97
+ options = {:auto_recovery => true, :prefetch => Promiscuous::Config.prefetch}
98
+ ::AMQP::Channel.new(@connection, options) do |channel|
99
+ @channels[name] = channel
100
+ get_exchange(name)
101
+ yield(channel) if block_given?
102
+ end
52
103
  end
53
- self.channel = nil
54
104
  end
55
105
 
56
- # Always disconnect when shutting down to avoid reconnection
57
- EM.add_shutdown_hook { Promiscuous::AMQP::RubyAMQP.disconnect }
58
-
59
- def self.connected?
60
- !!self.channel.try(:connection).try(:connected?)
106
+ def self.close_channel(name, &block)
107
+ EM.next_tick do
108
+ channel = @channels.try(:delete, name)
109
+ if channel
110
+ channel.close(&block)
111
+ else
112
+ block.call if block
113
+ end
114
+ end
61
115
  end
62
116
 
63
- def self.open_queue(options={}, &block)
64
- queue_name = options[:queue_name]
65
- bindings = options[:bindings]
117
+ def self.disconnect
118
+ @connection.close { EM.stop if @event_machine_thread } if @connection
119
+ @event_machine_thread.join if @event_machine_thread
120
+ @event_machine_thread = nil
121
+ @connection = nil
122
+ @channels = nil
123
+ @exchanges = nil
124
+ end
66
125
 
67
- queue = self.channel.queue(queue_name, Promiscuous::Config.queue_options)
68
- bindings.each do |binding|
69
- queue.bind(exchange(options[:exchange_name]), :routing_key => binding)
70
- Promiscuous.debug "[bind] #{queue_name} -> #{binding}"
71
- end
72
- block.call(queue) if block
126
+ def self.connected?
127
+ @connection.connected? if @connection
73
128
  end
74
129
 
75
130
  def self.publish(options={})
76
131
  EM.next_tick do
77
- exchange(options[:exchange_name]).
78
- publish(options[:payload], :routing_key => options[:key], :persistent => true)
132
+ get_exchange(:master).publish(options[:payload], :routing_key => options[:key], :persistent => true) do
133
+ end
79
134
  end
80
135
  end
81
136
 
82
- def self.exchange(name)
83
- channel.topic(name, :durable => true)
137
+ def self.get_exchange(name)
138
+ @exchanges[name] ||= get_channel(name).topic(Promiscuous::AMQP::EXCHANGE, :durable => true)
84
139
  end
85
140
  end
@@ -8,7 +8,9 @@ module Promiscuous::AMQP
8
8
  attr_accessor :backend
9
9
 
10
10
  def backend=(value)
11
- @backend = "Promiscuous::AMQP::#{value.to_s.camelize.gsub(/amqp/, 'AMQP')}".constantize unless value.nil?
11
+ disconnect if @backend
12
+ @backend = value.nil? ? nil : "Promiscuous::AMQP::#{value.to_s.camelize.gsub(/amqp/, 'AMQP')}".constantize
13
+ connect if @backend
12
14
  end
13
15
 
14
16
  def lost_connection_exception
@@ -14,7 +14,6 @@ class Promiscuous::CLI
14
14
  end
15
15
  end
16
16
  end
17
- trap_signals
18
17
 
19
18
  def publish
20
19
  options[:criterias].map { |criteria| eval(criteria) }.each do |criteria|
@@ -58,8 +57,8 @@ class Promiscuous::CLI
58
57
  options[:require] = file
59
58
  end
60
59
 
61
- opts.on "-r", "--recovery [TIMEOUT]", "Run in recovery mode. Defaults to 10 seconds before recovering" do |timeout|
62
- Promiscuous::Config.recovery_timeout = (timeout || 10).to_i
60
+ opts.on "-r", "--recovery", "Run in recovery mode" do
61
+ Promiscuous::Config.recovery = true
63
62
  end
64
63
 
65
64
  opts.on "-p", "--prefetch [NUM]", "Number of messages to prefetch" do |prefetch|
@@ -120,6 +119,7 @@ class Promiscuous::CLI
120
119
  end
121
120
 
122
121
  def run
122
+ trap_signals
123
123
  case options[:action]
124
124
  when :publish then publish
125
125
  when :subscribe then subscribe
@@ -1,26 +1,34 @@
1
1
  module Promiscuous::Config
2
2
  mattr_accessor :app, :logger, :error_notifier, :backend, :amqp_url,
3
3
  :redis_url, :queue_options, :heartbeat, :bareback,
4
- :recovery_timeout, :prefetch
4
+ :recovery, :prefetch
5
5
 
6
6
  def self.backend=(value)
7
7
  @@backend = value
8
- Promiscuous::AMQP.backend = value unless value.nil?
8
+ Promiscuous::AMQP.backend = value
9
9
  end
10
10
 
11
11
  def self.reset
12
+ Promiscuous::AMQP.backend = nil
12
13
  class_variables.each { |var| class_variable_set(var, nil) }
13
14
  end
14
15
 
15
16
  def self.configure(&block)
16
17
  block.call(self)
17
18
 
18
- self.backend ||= defined?(EventMachine) && EventMachine.reactor_running? ? :rubyamqp : :bunny
19
+ self.app ||= Rails.application.class.parent_name.underscore rescue nil if defined?(Rails)
20
+ unless self.app
21
+ raise "Promiscuous.configure: please give a name to your app with \"config.app = 'your_app_name'\""
22
+ end
23
+ self.backend ||= :rubyamqp # amqp connection is done in Promiscuous::AMQP
24
+ Promiscuous::Redis.connect
19
25
  self.logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
20
26
  self.queue_options ||= {:durable => true, :arguments => {'x-ha-policy' => 'all'}}
21
27
  self.heartbeat ||= 60
22
28
  self.prefetch ||= 1000
29
+ end
23
30
 
24
- Promiscuous.connect
31
+ def self.configured?
32
+ self.app != nil
25
33
  end
26
34
  end
@@ -1,3 +1,6 @@
1
+ module ::Containers; end
2
+ require 'containers/priority_queue'
3
+
1
4
  class Promiscuous::Subscriber::Worker::MessageSynchronizer
2
5
  include Celluloid::IO
3
6
 
@@ -53,7 +56,7 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
53
56
  main_loop!
54
57
 
55
58
  Promiscuous.warn "[redis] Reconnected"
56
- EM.next_tick { Promiscuous::AMQP::RubyAMQP.channel.recover }
59
+ Celluloid::Actor[:pump].recover
57
60
  end
58
61
  rescue
59
62
  reconnect_later
@@ -75,7 +78,7 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
75
78
  find_subscription(subscription).finalize_subscription
76
79
  when 'unsubscribe'
77
80
  when 'message'
78
- find_subscription(subscription).maybe_perform_callbacks(arg)
81
+ find_subscription(subscription).signal_version(arg)
79
82
  end
80
83
  end
81
84
  rescue EOFError
@@ -124,39 +127,21 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
124
127
  end
125
128
 
126
129
  def maybe_recover
127
- recovery_timeout = Promiscuous::Config.recovery_timeout
128
- return unless recovery_timeout
130
+ return unless Promiscuous::Config.recovery
129
131
 
130
- reset_recovery_timer
131
- if should_recover?
132
+ if @queued_messages == Promiscuous::Config.prefetch
132
133
  # We've reached the amount of messages the amqp queue is willing to give us.
133
134
  # We also know that we are not processing messages (@queued_messages is
134
135
  # decremented before we send the message to the runners).
135
-
136
- Promiscuous.warn "[receive] Recovering in #{recovery_timeout} seconds..."
137
- @recover_timer = after(recovery_timeout) { reset_recovery_timer; recover }
136
+ recover
138
137
  end
139
138
  end
140
139
 
141
- def reset_recovery_timer
142
- @recover_timer.try(:reset)
143
- @recover_timer = nil
144
- end
145
-
146
- def should_recover?
147
- @queued_messages == Promiscuous::Config.prefetch
148
- end
149
-
150
140
  def recover
151
- return unless should_recover?
152
-
153
141
  global_key = Promiscuous::Redis.sub_key('global')
154
142
  current_version = Promiscuous::Redis.get(global_key).to_i
155
143
 
156
- global_callbacks = get_subscription(global_key).callbacks
157
- expected_next_msg_version = global_callbacks.values.map(&:version).min
158
- version_to_allow_progress = expected_next_msg_version - 1
159
-
144
+ version_to_allow_progress = get_subscription(global_key).callbacks.next.version - 1
160
145
  num_messages_to_skip = version_to_allow_progress - current_version
161
146
 
162
147
  if num_messages_to_skip > 0
@@ -182,8 +167,6 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
182
167
  Promiscuous::Redis.set(global_key, version_to_allow_progress)
183
168
  Promiscuous::Redis.publish(global_key, version_to_allow_progress)
184
169
  end
185
- ensure
186
- reset_recovery_timer
187
170
  end
188
171
 
189
172
  def process_message!(msg)
@@ -193,9 +176,9 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
193
176
 
194
177
  def on_version(key, version, &callback)
195
178
  return unless @subscriptions
196
- cb = Subscription::Callback.new(version, callback)
197
- get_subscription(key).subscribe.add_callback(version, cb)
198
- cb.current_version = Promiscuous::Redis.get(key)
179
+ sub = get_subscription(key).subscribe
180
+ sub.add_callback(Subscription::Callback.new(version, callback))
181
+ sub.signal_version(Promiscuous::Redis.get(key))
199
182
  end
200
183
 
201
184
  def find_subscription(key)
@@ -216,7 +199,8 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
216
199
 
217
200
  @subscription_requested = false
218
201
  @subscribed_to_redis = false
219
- @callbacks = {}
202
+ # We use a priority queue that returns the smallest value first
203
+ @callbacks = Containers::PriorityQueue.new { |x, y| x < y }
220
204
  end
221
205
 
222
206
  def subscribe
@@ -244,59 +228,36 @@ class Promiscuous::Subscriber::Worker::MessageSynchronizer
244
228
  # TODO parent.redis_client_call(:unsubscribe, key)
245
229
  end
246
230
 
247
- def add_callback(version, callback)
248
- callback.subscription = self
249
- @callbacks[callback.token] = callback
250
- end
231
+ def signal_version(current_version)
232
+ current_version = current_version.to_i
233
+ loop do
234
+ next_cb = @callbacks.next
235
+ return unless next_cb && next_cb.can_perform?(current_version)
251
236
 
252
- def remove_callback(token)
253
- !!@callbacks.delete(token)
254
- # TODO unsubscribe after a while?
237
+ @callbacks.pop
238
+ next_cb.perform
239
+ end
255
240
  end
256
241
 
257
- def maybe_perform_callbacks(current_version)
258
- @callbacks.values.each do |cb|
259
- cb.current_version = current_version
260
- end
242
+ def add_callback(callback)
243
+ callback.subscription = self
244
+ @callbacks.push(callback, callback.version)
261
245
  end
262
246
 
263
247
  class Callback
264
- # Tokens are only used so that the callback can find and remove itself
265
- # in the callback list.
266
- @next_token = 0
267
- def self.get_next_token
268
- @next_token += 1
269
- end
270
-
271
248
  attr_accessor :subscription, :version, :callback, :token
272
249
 
273
250
  def initialize(version, callback)
274
- @current_version = 0
275
251
  self.version = version
276
252
  self.callback = callback
277
- @token = self.class.get_next_token
278
253
  end
279
254
 
280
- def current_version=(value)
281
- @current_version = value.to_i
282
- maybe_perform
283
- value
284
- end
285
-
286
- private
287
-
288
- def can_perform?
289
- @current_version + 1 >= self.version
255
+ def can_perform?(current_version)
256
+ current_version + 1 >= self.version
290
257
  end
291
258
 
292
259
  def perform
293
- # removing the callback can happen only once, ensuring that the
294
- # callback is called at most once.
295
- callback.call if subscription.remove_callback(@token)
296
- end
297
-
298
- def maybe_perform
299
- perform if can_perform?
260
+ callback.call
300
261
  end
301
262
  end
302
263
  end
@@ -3,71 +3,48 @@ require 'eventmachine'
3
3
  class Promiscuous::Subscriber::Worker::Pump
4
4
  include Celluloid
5
5
 
6
- def initialize
7
- # signals do not work when initializing with Celluloid
8
- # I wish ruby had semaphores, it would make much more sense.
9
- @initialize_mutex = Mutex.new
10
- @initialization_done = ConditionVariable.new
11
-
12
- @em_thread = Thread.new { EM.run { start } }
6
+ attr_accessor :subscribe_sync
13
7
 
14
- # The event machine thread will unlock us
15
- wait_for_initialization
16
- raise @exception if @exception
17
- end
8
+ def initialize
9
+ # We need to subscribe to everything to keep up with the version tracking
10
+ queue_name = "#{Promiscuous::Config.app}.promiscuous"
11
+ bindings = ['*']
18
12
 
19
- def wait_for_initialization
20
- @initialize_mutex.synchronize do
21
- @initialization_done.wait(@initialize_mutex)
13
+ unless Promiscuous::Config.backend == :rubyamqp
14
+ raise "you must use the ruby_amqp backend"
22
15
  end
23
- end
24
-
25
- def finalize_initialization
26
- @initialize_mutex.synchronize do
27
- @initialization_done.signal
16
+ Promiscuous::AMQP.ensure_connected
17
+
18
+ @subscribe_sync = Promiscuous::AMQP::RubyAMQP::Synchronizer.new
19
+ Promiscuous::AMQP::RubyAMQP.get_channel(:pump) do |channel|
20
+ @channel = channel
21
+ # TODO channel.on_error ?
22
+
23
+ queue = channel.queue(queue_name, Promiscuous::Config.queue_options)
24
+ exchange = Promiscuous::AMQP::RubyAMQP.get_exchange(:pump)
25
+ bindings.each do |binding|
26
+ queue.bind(exchange, :routing_key => binding)
27
+ Promiscuous.debug "[bind] #{queue_name} -> #{binding}"
28
+ end
29
+ queue.subscribe(:ack => true, :confirm => proc { @subscribe_sync.signal }, &method(:process_payload))
28
30
  end
31
+ @subscribe_sync.wait
29
32
  end
30
33
 
31
34
  def finalize
32
- @dont_reconnect = true
33
- EM.next_tick do
34
- Promiscuous::AMQP.disconnect
35
- EM.stop
35
+ channel_sync = Promiscuous::AMQP::RubyAMQP::Synchronizer.new
36
+ Promiscuous::AMQP::RubyAMQP.close_channel(:pump) do
37
+ channel_sync.signal
36
38
  end
37
- @em_thread.join
38
- rescue
39
- # Let amqp die like a pro
39
+ channel_sync.wait
40
40
  end
41
41
 
42
- def force_use_ruby_amqp
43
- Promiscuous::AMQP.disconnect
44
- Promiscuous::Config.backend = :rubyamqp
45
- Promiscuous::AMQP.connect
46
- end
47
-
48
- def start
49
- force_use_ruby_amqp
50
- Promiscuous::AMQP.open_queue(queue_bindings) do |queue|
51
- queue.subscribe :ack => true do |metadata, payload|
52
- process_payload(metadata, payload)
53
- end
54
- end
55
- rescue Exception => @exception
56
- ensure
57
- finalize_initialization
42
+ def recover
43
+ EM.next_tick { @channel.recover }
58
44
  end
59
45
 
60
46
  def process_payload(metadata, payload)
61
47
  msg = Promiscuous::Subscriber::Worker::Message.new(metadata, payload)
62
48
  Celluloid::Actor[:message_synchronizer].process_when_ready(msg)
63
49
  end
64
-
65
- def queue_bindings
66
- queue_name = "#{Promiscuous::Config.app}.promiscuous"
67
- exchange_name = Promiscuous::AMQP::EXCHANGE
68
-
69
- # We need to subscribe to everything to keep up with the version tracking
70
- bindings = ['*']
71
- {:exchange_name => exchange_name, :queue_name => queue_name, :bindings => bindings}
72
- end
73
50
  end
@@ -1,3 +1,3 @@
1
1
  module Promiscuous
2
- VERSION = '0.51.0'
2
+ VERSION = '0.52.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: promiscuous
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.51.0
4
+ version: 0.52.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-02-05 00:00:00.000000000 Z
13
+ date: 2013-02-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -156,6 +156,22 @@ dependencies:
156
156
  - - ~>
157
157
  - !ruby/object:Gem::Version
158
158
  version: 0.12.1
159
+ - !ruby/object:Gem::Dependency
160
+ name: algorithms
161
+ requirement: !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ~>
165
+ - !ruby/object:Gem::Version
166
+ version: 0.1.0
167
+ type: :runtime
168
+ prerelease: false
169
+ version_requirements: !ruby/object:Gem::Requirement
170
+ none: false
171
+ requirements:
172
+ - - ~>
173
+ - !ruby/object:Gem::Version
174
+ version: 0.1.0
159
175
  description: Replicate your Mongoid/ActiveRecord models across your applications
160
176
  email:
161
177
  - nicolas@viennot.biz
@@ -201,9 +217,9 @@ files:
201
217
  - lib/promiscuous/subscriber/mongoid/embedded_many.rb
202
218
  - lib/promiscuous/subscriber/mongoid/embedded.rb
203
219
  - lib/promiscuous/subscriber/worker/runner.rb
204
- - lib/promiscuous/subscriber/worker/pump.rb
205
220
  - lib/promiscuous/subscriber/worker/message.rb
206
221
  - lib/promiscuous/subscriber/worker/message_synchronizer.rb
222
+ - lib/promiscuous/subscriber/worker/pump.rb
207
223
  - lib/promiscuous/subscriber/active_record.rb
208
224
  - lib/promiscuous/subscriber/envelope.rb
209
225
  - lib/promiscuous/subscriber/upsert.rb
@@ -228,13 +244,13 @@ files:
228
244
  - lib/promiscuous/railtie.rb
229
245
  - lib/promiscuous/common.rb
230
246
  - lib/promiscuous/publisher.rb
231
- - lib/promiscuous/amqp.rb
232
247
  - lib/promiscuous/subscriber.rb
233
248
  - lib/promiscuous/redis.rb
234
249
  - lib/promiscuous/observer.rb
235
- - lib/promiscuous/cli.rb
236
250
  - lib/promiscuous/error.rb
237
251
  - lib/promiscuous/config.rb
252
+ - lib/promiscuous/amqp.rb
253
+ - lib/promiscuous/cli.rb
238
254
  - lib/promiscuous/version.rb
239
255
  - lib/promiscuous.rb
240
256
  - bin/promiscuous