promiscuous 0.21.1 → 0.22

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