promiscuous 0.21.1 → 0.22

Sign up to get free protection for your applications and to get access to all the features.
data/bin/promiscuous CHANGED
@@ -4,7 +4,8 @@ begin
4
4
  require 'promiscuous'
5
5
  Promiscuous::CLI.new.run
6
6
  rescue => e
7
- $stderr.puts e.backtrace.join("\n")
7
+ $stderr.puts "#{e.class}: #{e.message}"
8
8
  $stderr.puts '-' * 80
9
- $stderr.puts e.message
9
+ $stderr.puts e.backtrace.join("\n")
10
+ exit 1
10
11
  end
data/lib/promiscuous.rb CHANGED
@@ -12,6 +12,7 @@ module Promiscuous
12
12
  autoload :Worker, 'promiscuous/worker'
13
13
  autoload :Ephemeral, 'promiscuous/ephemeral'
14
14
  autoload :CLI, 'promiscuous/cli'
15
+ autoload :Error, 'promiscuous/error'
15
16
 
16
17
  class << self
17
18
  def configure(&block)
@@ -28,16 +28,15 @@ module Promiscuous
28
28
  Promiscuous.warn "[connection] Lost connection. Reconnecting..."
29
29
  conn.periodically_reconnect(2)
30
30
 
31
- exception = StandardError.new 'Lost connection'
32
- Promiscuous::Config.error_handler.try(:call, exception)
33
-
34
- Promiscuous::Worker.pause # TODO XXX This doesn't belong here
31
+ exception = Promiscuous::Error::Connection.new 'Lost connection'
32
+ Promiscuous::Worker.stop # TODO XXX This doesn't belong here. hooks ?
33
+ Promiscuous::Config.error_notifier.try(:call, exception)
35
34
  end
36
35
  end
37
36
 
38
37
  connection.on_recovery do |conn|
39
38
  Promiscuous.warn "[connection] Reconnected"
40
- Promiscuous::Worker.resume # TODO XXX This doesn't belong here
39
+ Promiscuous::Worker.resume # TODO XXX This doesn't belong here. hooks ?
41
40
  end
42
41
 
43
42
  connection.on_error do |conn, conn_close|
@@ -55,6 +54,9 @@ module Promiscuous
55
54
  self.channel = nil
56
55
  end
57
56
 
57
+ # Always disconnect when shutting down to avoid reconnection
58
+ EM.add_shutdown_hook { Promiscuous::AMQP::RubyAMQP.disconnect }
59
+
58
60
  def self.connected?
59
61
  !!self.channel.try(:connection).try(:connected?)
60
62
  end
@@ -75,8 +77,7 @@ module Promiscuous
75
77
  info_msg = "(#{options[:exchange_name]}) #{options[:key]} -> #{options[:payload]}"
76
78
 
77
79
  unless channel.connection.connected?
78
- exception = StandardError.new 'Lost connection'
79
- raise Promiscuous::Publisher::Error.new(exception, info_msg)
80
+ raise Promiscuous::Error::Connection.new 'Not connected'
80
81
  end
81
82
 
82
83
  Promiscuous.info "[publish] #{info_msg}"
@@ -23,8 +23,7 @@ class Promiscuous::CLI
23
23
  %w(SIGTERM SIGINT).each do |signal|
24
24
  Signal.trap(signal) do
25
25
  print_status "Exiting..."
26
- Promiscuous::Worker.stop
27
- Promiscuous::AMQP.disconnect
26
+ Promiscuous::Worker.kill
28
27
  EM.stop
29
28
  end
30
29
  end
@@ -3,10 +3,21 @@ module Promiscuous::Common::Worker
3
3
 
4
4
  def initialize(options={})
5
5
  self.options = options
6
- self.stop = false
6
+ self.stopped = true
7
+ made_progress
8
+ end
9
+
10
+ def stop
11
+ self.stopped = true
12
+ end
13
+
14
+ def resume
15
+ self.stopped = false
7
16
  end
8
17
 
9
18
  def unit_of_work(type)
19
+ # type is used by the new relic agent, by monkey patching.
20
+ # middleware?
10
21
  if defined?(Mongoid)
11
22
  Mongoid.unit_of_work { yield }
12
23
  else
@@ -18,5 +29,24 @@ module Promiscuous::Common::Worker
18
29
  !!options[:bareback]
19
30
  end
20
31
 
21
- included { attr_accessor :stop, :options }
32
+ def stop_for_a_while(reason)
33
+ stop
34
+ self.retry_timeout = self.retry_timeout * 2
35
+
36
+ if reason.inner.is_a? Promiscuous::Error::Connection
37
+ "will retry when the amqp connection comes back"
38
+ else
39
+ EM::Timer.new(self.retry_timeout) { resume }
40
+ "retrying in #{self.retry_timeout}s"
41
+ end
42
+ end
43
+
44
+ def made_progress
45
+ self.retry_timeout = 1
46
+ end
47
+
48
+ included do
49
+ attr_accessor :stopped, :options, :retry_timeout
50
+ alias_method :stopped?, :stopped
51
+ end
22
52
  end
@@ -1,6 +1,6 @@
1
1
  module Promiscuous
2
2
  module Config
3
- mattr_accessor :app, :logger, :error_handler, :backend, :server_uri, :queue_options
3
+ mattr_accessor :app, :logger, :error_notifier, :backend, :server_uri, :queue_options
4
4
 
5
5
  def self.backend=(value)
6
6
  @@backend = "Promiscuous::AMQP::#{value.to_s.camelize.gsub(/amqp/, 'AMQP')}".constantize unless value.nil?
@@ -17,5 +17,12 @@ module Promiscuous
17
17
 
18
18
  Promiscuous::AMQP.connect
19
19
  end
20
+
21
+ # TODO backward compatibility. to be removed.
22
+ def self.error_handler=(value)
23
+ ActiveSupport::Deprecation.behavior = :stderr
24
+ ActiveSupport::Deprecation.warn "'error_handler' is deprecated, please use 'error_notifier'", caller
25
+ self.error_notifier = value
26
+ end
20
27
  end
21
28
  end
@@ -0,0 +1,5 @@
1
+ module Promiscuous::Error
2
+ autoload :Connection, 'promiscuous/error/connection'
3
+ autoload :Publisher, 'promiscuous/error/publisher'
4
+ autoload :Subscriber, 'promiscuous/error/subscriber'
5
+ end
@@ -0,0 +1 @@
1
+ class Promiscuous::Error::Connection < RuntimeError; end
@@ -0,0 +1,25 @@
1
+ class Promiscuous::Error::Publisher < RuntimeError
2
+ attr_accessor :inner, :instance, :out_of_sync
3
+
4
+ def initialize(inner, options={})
5
+ super(nil)
6
+ inner = inner.inner if inner.is_a?(Promiscuous::Error::Publisher)
7
+ set_backtrace(inner.backtrace)
8
+ self.inner = inner
9
+ self.instance = options[:instance]
10
+ self.out_of_sync = options[:out_of_sync]
11
+ end
12
+
13
+ def message
14
+ msg = "#{inner.class}: #{inner.message}"
15
+ if instance
16
+ msg = "#{msg} while publishing #{instance.inspect}"
17
+ msg = "FATAL (out of sync) #{msg}" if out_of_sync
18
+ end
19
+ msg
20
+ end
21
+
22
+ def to_s
23
+ message
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ class Promiscuous::Error::Subscriber < RuntimeError
2
+ attr_accessor :inner, :payload
3
+
4
+ def initialize(inner, options={})
5
+ super(nil)
6
+ set_backtrace(inner.backtrace)
7
+ self.inner = inner
8
+ self.payload = options[:payload]
9
+ end
10
+
11
+ def message
12
+ "#{inner.class}: #{inner.message} while processing #{payload}"
13
+ end
14
+
15
+ def to_s
16
+ message
17
+ end
18
+ end
@@ -6,6 +6,8 @@ module Promiscuous::Publisher::AMQP
6
6
  exchange_name = Promiscuous::AMQP::EXCHANGE
7
7
  exchange_name += ".#{options[:personality]}" if options[:personality]
8
8
  Promiscuous::AMQP.publish(:exchange_name => exchange_name, :key => to, :payload => payload.to_json)
9
+ rescue Exception => e
10
+ raise Promiscuous::Error::Publisher.new(e, :instance => instance, :out_of_sync => true)
9
11
  end
10
12
 
11
13
  def payload
@@ -1,14 +1,17 @@
1
1
  class Promiscuous::Publisher::Worker
2
2
  include Promiscuous::Common::Worker
3
3
 
4
- attr_accessor :recovered
5
-
6
4
  def self.poll_delay
7
5
  # TODO Configurable globally
8
6
  # TODO Configurable per publisher
9
7
  1.second
10
8
  end
11
9
 
10
+ def initialize(options={})
11
+ super
12
+ check_indexes
13
+ end
14
+
12
15
  def check_indexes
13
16
  Promiscuous::Publisher::Mongoid::Defer.klasses.values.each do |klass|
14
17
  unless klass.collection.indexes.any? { |i| i['key'].keys.include? '_psp' }
@@ -17,41 +20,51 @@ class Promiscuous::Publisher::Worker
17
20
  end
18
21
  end
19
22
 
20
- def replicate
21
- check_indexes
22
- EM::PeriodicTimer.new(self.class.poll_delay) { self.replicate_once }
23
+ def resume
24
+ @timer ||= EM::PeriodicTimer.new(self.class.poll_delay) { self.replicate_once }
25
+ super
26
+ end
27
+
28
+ def stop
29
+ @timer.try(:cancel)
30
+ @timer = nil
31
+ super
23
32
  end
24
33
 
25
34
  def replicate_once
26
- return if self.stop
27
- begin
28
- self.unit_of_work('publisher') do
29
- Promiscuous::Publisher::Mongoid::Defer.klasses.values.each do |klass|
30
- replicate_collection(klass)
31
- end
32
- end
33
- rescue Exception => e
34
- self.stop = true unless bareback?
35
+ return if self.stopped?
35
36
 
36
- unless e.is_a?(Promiscuous::Publisher::Error)
37
- e = Promiscuous::Publisher::Error.new(e, nil)
38
- end
37
+ self.unit_of_work('publisher') do
38
+ maybe_rescue_instance
39
+ replicate_all_collections
40
+ end
41
+ rescue Exception => e
42
+ unless e.is_a?(Promiscuous::Error::Publisher)
43
+ e = Promiscuous::Error::Publisher.new(e)
44
+ end
39
45
 
40
- if self.recovered
41
- Promiscuous.warn "[publish] will retry #{e.instance.try(:id)} #{e} #{e.backtrace}"
42
- else
43
- Promiscuous.error "[publish] FATAL #{e.instance.try(:id)} #{e} #{e.backtrace}"
44
- end
46
+ retry_msg = stop_for_a_while(e)
47
+ Promiscuous.warn "[publish] (#{retry_msg}) #{e} #{e.backtrace.join("\n")}"
45
48
 
46
- Promiscuous::Config.error_handler.try(:call, e)
49
+ if e.out_of_sync
50
+ Promiscuous.error "[publish] WARNING out of sync on #{e.instance.inspect}"
51
+ @out_of_sync_instance = e.instance
47
52
  end
53
+
54
+ Promiscuous::Config.error_notifier.try(:call, e)
55
+ end
56
+
57
+ def replicate_all_collections
58
+ Promiscuous::Publisher::Mongoid::Defer.klasses.values.each do |klass|
59
+ replicate_collection(klass)
60
+ end
61
+ made_progress
48
62
  end
49
63
 
50
64
  def replicate_collection(klass)
51
65
  loop do
52
- break if self.stop
66
+ break if self.stopped?
53
67
 
54
- self.recovered = false
55
68
  instance = klass.where(:_psp => true).
56
69
  find_and_modify({'$unset' => {:_psp => 1}}, :bypass_promiscuous => true)
57
70
  break unless instance
@@ -62,24 +75,27 @@ class Promiscuous::Publisher::Worker
62
75
 
63
76
  def replicate_instance(instance)
64
77
  instance.promiscuous_sync
78
+ made_progress
65
79
  rescue Exception => e
66
- # We failed publishing. Best effort recover.
67
- if e.is_a?(Promiscuous::Publisher::Error)
68
- e.instance = instance
69
- else
70
- e = Promiscuous::Publisher::Error.new(e, instance)
71
- end
80
+ out_of_sync = requeue_instance(instance)
81
+ raise Promiscuous::Error::Publisher.new(e, :instance => instance,
82
+ :out_of_sync => out_of_sync)
83
+ end
72
84
 
73
- begin
74
- # The following update will set the _psp flag to true again, effectively
75
- # requeuing the publish action.
76
- instance.class.where(instance.atomic_selector).update({})
77
- self.recovered = true
78
- rescue
79
- # Swallow exception of a failed recovery, the log file will have a FATAL entry.
80
- # The user needs to manually resync.
81
- end
85
+ def requeue_instance(instance)
86
+ # The following update will set the _psp flag to true again,
87
+ # effectively requeuing the publish action.
88
+ instance.class.where(instance.atomic_selector).update({})
89
+ false
90
+ rescue Exception
91
+ # Swallow exception of a failed recovery.
92
+ # The user needs to manually resync.
93
+ true
94
+ end
82
95
 
83
- raise e
96
+ def maybe_rescue_instance
97
+ return unless @out_of_sync_instance
98
+ replicate_instance(@out_of_sync_instance)
99
+ @out_of_sync_instance = nil
84
100
  end
85
101
  end
@@ -1,33 +1,50 @@
1
1
  class Promiscuous::Subscriber::Worker
2
2
  include Promiscuous::Common::Worker
3
3
 
4
- def replicate
5
- Promiscuous::AMQP.open_queue(queue_bindings) do |queue|
6
- queue.subscribe :ack => true do |metadata, payload|
7
- # Note: This code always runs on the root Fiber,
8
- # so ordering is always preserved
9
- begin
10
- unless self.stop
11
- Promiscuous.info "[receive] #{payload}"
12
- parsed_payload = JSON.parse(payload)
13
- queue = parsed_payload['__amqp__']
14
- self.unit_of_work(queue) { Promiscuous::Subscriber.process(parsed_payload) }
15
- metadata.ack
16
- end
17
- rescue Exception => e
18
- e = Promiscuous::Subscriber::Error.new(e, payload)
19
-
20
- if bareback?
21
- metadata.ack
22
- else
23
- self.stop = true
24
- Promiscuous::AMQP.disconnect
25
- end
26
- Promiscuous.error "[receive] FATAL #{"skipping " if bareback?}#{e} #{e.backtrace.join("\n")}"
27
- Promiscuous::Config.error_handler.try(:call, e)
28
- end
4
+ def resume
5
+ if @queue
6
+ # XXX TODO we should not access to the channel like this.
7
+ # The abstraction is leaking.
8
+ # Actually, we actually want one channel per worker.
9
+
10
+ # The following tells rabbitmq to resend the unacked messages
11
+ Promiscuous::AMQP::RubyAMQP.channel.recover
12
+ else
13
+ Promiscuous::AMQP.open_queue(queue_bindings) do |queue|
14
+ @queue = queue
15
+ @queue.subscribe({:ack => true}, &method(:process_payload))
29
16
  end
30
17
  end
18
+
19
+ super
20
+ end
21
+
22
+ def process_payload(metadata, payload)
23
+ return if self.stopped?
24
+
25
+ # Note: This code always runs on the root Fiber,
26
+ # so ordering is always preserved
27
+
28
+ Promiscuous.info "[receive] #{payload}"
29
+ parsed_payload = JSON.parse(payload)
30
+ queue = parsed_payload['__amqp__']
31
+
32
+ self.unit_of_work(queue) { Promiscuous::Subscriber.process(parsed_payload) }
33
+
34
+ metadata.ack
35
+ made_progress
36
+ rescue Exception => e
37
+ e = Promiscuous::Error::Subscriber.new(e, :payload => payload)
38
+
39
+ if bareback?
40
+ Promiscuous.error "[receive] (bareback, don't care) #{e} #{e.backtrace.join("\n")}"
41
+ metadata.ack
42
+ else
43
+ retry_msg = stop_for_a_while(e)
44
+ Promiscuous.warn "[receive] (#{retry_msg}) #{e} #{e.backtrace.join("\n")}"
45
+ end
46
+
47
+ Promiscuous::Config.error_notifier.try(:call, e)
31
48
  end
32
49
 
33
50
  def queue_bindings
@@ -1,3 +1,3 @@
1
1
  module Promiscuous
2
- VERSION = '0.21.1'
2
+ VERSION = '0.22'
3
3
  end
@@ -6,21 +6,21 @@ module Promiscuous::Worker
6
6
  options[:action] ||= [:publish, :subscribe]
7
7
  actions = [options[:action]].flatten
8
8
 
9
- self.workers << Promiscuous::Publisher::Worker.new(options) if :publish.in? actions
10
- self.workers << Promiscuous::Subscriber::Worker.new(options) if :subscribe.in? actions
11
- self.workers.each { |w| w.replicate }
9
+ self.workers << Promiscuous::Publisher::Worker.new(options).tap { |w| w.resume } if :publish.in? actions
10
+ self.workers << Promiscuous::Subscriber::Worker.new(options).tap { |w| w.resume } if :subscribe.in? actions
12
11
  end
13
12
 
14
- def self.stop
15
- self.workers.each { |w| w.stop = true }
16
- self.workers.clear
13
+ def self.kill
14
+ stop
15
+ # TODO FIXME We should wait for them to be idle
16
+ workers.clear
17
17
  end
18
18
 
19
- def self.pause
20
- self.workers.each { |w| w.stop = true }
19
+ def self.stop
20
+ workers.each(&:stop)
21
21
  end
22
22
 
23
23
  def self.resume
24
- self.workers.each { |w| w.stop = false }
24
+ workers.each(&:resume)
25
25
  end
26
26
  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.21.1
4
+ version: '0.22'
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-01-04 00:00:00.000000000 Z
13
+ date: 2013-01-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -120,14 +120,11 @@ files:
120
120
  - lib/promiscuous/amqp/null.rb
121
121
  - lib/promiscuous/amqp/bunny.rb
122
122
  - lib/promiscuous/amqp/rubyamqp.rb
123
- - lib/promiscuous/common/lint.rb
124
123
  - lib/promiscuous/common/lint/base.rb
124
+ - lib/promiscuous/common/lint.rb
125
125
  - lib/promiscuous/common/class_helpers.rb
126
126
  - lib/promiscuous/common/options.rb
127
127
  - lib/promiscuous/common/worker.rb
128
- - lib/promiscuous/loader.rb
129
- - lib/promiscuous/publisher/envelope.rb
130
- - lib/promiscuous/publisher/lint.rb
131
128
  - lib/promiscuous/publisher/lint/amqp.rb
132
129
  - lib/promiscuous/publisher/lint/attributes.rb
133
130
  - lib/promiscuous/publisher/lint/base.rb
@@ -137,22 +134,19 @@ files:
137
134
  - lib/promiscuous/publisher/mongoid/defer_embedded.rb
138
135
  - lib/promiscuous/publisher/mongoid/embedded_many.rb
139
136
  - lib/promiscuous/publisher/mongoid/defer.rb
137
+ - lib/promiscuous/publisher/envelope.rb
138
+ - lib/promiscuous/publisher/lint.rb
140
139
  - lib/promiscuous/publisher/polymorphic.rb
141
140
  - lib/promiscuous/publisher/mock.rb
142
141
  - lib/promiscuous/publisher/attributes.rb
143
- - lib/promiscuous/publisher/amqp.rb
144
142
  - lib/promiscuous/publisher/model.rb
145
143
  - lib/promiscuous/publisher/active_record.rb
146
144
  - lib/promiscuous/publisher/base.rb
147
145
  - lib/promiscuous/publisher/class.rb
148
146
  - lib/promiscuous/publisher/ephemeral.rb
149
147
  - lib/promiscuous/publisher/mongoid.rb
150
- - lib/promiscuous/publisher/error.rb
148
+ - lib/promiscuous/publisher/amqp.rb
151
149
  - lib/promiscuous/publisher/worker.rb
152
- - lib/promiscuous/subscriber/active_record.rb
153
- - lib/promiscuous/subscriber/envelope.rb
154
- - lib/promiscuous/subscriber/error.rb
155
- - lib/promiscuous/subscriber/lint.rb
156
150
  - lib/promiscuous/subscriber/lint/amqp.rb
157
151
  - lib/promiscuous/subscriber/lint/base.rb
158
152
  - lib/promiscuous/subscriber/lint/class.rb
@@ -161,6 +155,9 @@ files:
161
155
  - lib/promiscuous/subscriber/mongoid/embedded_many.rb
162
156
  - lib/promiscuous/subscriber/mongoid/embedded.rb
163
157
  - lib/promiscuous/subscriber/mongoid/versioning.rb
158
+ - lib/promiscuous/subscriber/active_record.rb
159
+ - lib/promiscuous/subscriber/envelope.rb
160
+ - lib/promiscuous/subscriber/lint.rb
164
161
  - lib/promiscuous/subscriber/upsert.rb
165
162
  - lib/promiscuous/subscriber/observer.rb
166
163
  - lib/promiscuous/subscriber/polymorphic.rb
@@ -171,16 +168,21 @@ files:
171
168
  - lib/promiscuous/subscriber/class.rb
172
169
  - lib/promiscuous/subscriber/mongoid.rb
173
170
  - lib/promiscuous/subscriber/worker.rb
171
+ - lib/promiscuous/error/connection.rb
172
+ - lib/promiscuous/error/publisher.rb
173
+ - lib/promiscuous/error/subscriber.rb
174
+ - lib/promiscuous/loader.rb
174
175
  - lib/promiscuous/observer.rb
175
176
  - lib/promiscuous/common.rb
176
177
  - lib/promiscuous/ephemeral.rb
177
178
  - lib/promiscuous/subscriber.rb
178
179
  - lib/promiscuous/publisher.rb
179
180
  - lib/promiscuous/railtie.rb
181
+ - lib/promiscuous/amqp.rb
182
+ - lib/promiscuous/error.rb
180
183
  - lib/promiscuous/config.rb
181
184
  - lib/promiscuous/cli.rb
182
185
  - lib/promiscuous/worker.rb
183
- - lib/promiscuous/amqp.rb
184
186
  - lib/promiscuous/version.rb
185
187
  - lib/promiscuous.rb
186
188
  - bin/promiscuous
@@ -1,18 +0,0 @@
1
- class Promiscuous::Publisher::Error < RuntimeError
2
- attr_accessor :inner, :instance
3
-
4
- def initialize(inner, instance)
5
- super(inner)
6
- set_backtrace(inner.backtrace)
7
- self.inner = inner
8
- self.instance = instance
9
- end
10
-
11
- def message
12
- "#{inner.message} while processing #{instance}"
13
- end
14
-
15
- def to_s
16
- message
17
- end
18
- end
@@ -1,18 +0,0 @@
1
- class Promiscuous::Subscriber::Error < RuntimeError
2
- attr_accessor :inner, :payload
3
-
4
- def initialize(inner, payload)
5
- super(inner)
6
- set_backtrace(inner.backtrace)
7
- self.inner = inner
8
- self.payload = payload
9
- end
10
-
11
- def message
12
- "#{inner.message} while processing #{payload}"
13
- end
14
-
15
- def to_s
16
- message
17
- end
18
- end