promiscuous 0.51.0 → 0.52.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.
@@ -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