promiscuous 1.0.0.beta5 → 1.0.0.beta6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 80819caeaff72e0d2634206a082cd8ba0686d5f3
4
- data.tar.gz: ee744941f13a6dada05b5781f5e5600856ee00d4
3
+ metadata.gz: 2b5c5915b32dc2c76ffc02404b79933d27314a25
4
+ data.tar.gz: 3251ac561c92ae3e69f7ca002ea8a32236b604ee
5
5
  SHA512:
6
- metadata.gz: efbd90b25ff49949dd54ac5c2ee06300e5e06ff0cbd9c875d0608dadff15e682062874ec3a7c20ab10589706d1de5e5235e5ec6119246fb3b718577305e54096
7
- data.tar.gz: 0c55d26471375d3933f61bb24c8ee9490b6268db57e4d93d7ea98ecab12dfa2e9bfc3fd7fde099a9da9f43524936c5a91801960980834e139b8322e1d7fafa6a
6
+ metadata.gz: 7db223aa4a4ca2fd2a9af489468b610f5e00d55ba6b9ea9798c82eca7ae146bf1391b6c77940079feb103ab0ecb5a62d1aee301a8a576b7f29420024e1859aca
7
+ data.tar.gz: f60196280ba6bcca0b1a1011824f16193d0efcc52cd92615da89b2e712d4b2a9f0daf3ed5ad8b4af7bcaf447553bbba26ef6496da20d6c0af7b885d6ab655857
data/lib/promiscuous.rb CHANGED
@@ -23,7 +23,7 @@ module Promiscuous
23
23
  extend Promiscuous::Autoload
24
24
  autoload :Common, :Publisher, :Subscriber, :Observer, :Worker, :Ephemeral,
25
25
  :CLI, :Error, :Loader, :AMQP, :Redis, :ZK, :Config, :DSL, :Key,
26
- :Convenience, :Dependency, :Timer
26
+ :Convenience, :Dependency, :Timer, :Rabbit
27
27
 
28
28
  extend Promiscuous::DSL
29
29
 
@@ -2,8 +2,6 @@ module Promiscuous::AMQP
2
2
  extend Promiscuous::Autoload
3
3
  autoload :HotBunnies, :Bunny, :Null, :Fake, :File
4
4
 
5
- LIVE_EXCHANGE = 'promiscuous'
6
-
7
5
  class << self
8
6
  attr_accessor :backend
9
7
  attr_accessor :backend_class
@@ -45,22 +45,19 @@ class Promiscuous::AMQP::Bunny
45
45
  channel.basic_qos(options[:prefetch]) if options[:prefetch]
46
46
  raw_confirm_select(channel, &method(:on_confirm)) if options[:confirm]
47
47
 
48
- if options[:exchanges]
49
- exchanges = options[:exchanges].map do |exchange_name|
50
- channel.exchange(exchange_name, :type => :topic, :durable => true)
51
- end
52
- [connection, channel, exchanges]
53
- else
54
- exchange = channel.exchange(options[:exchange], :type => :topic, :durable => true)
55
- [connection, channel, exchange]
48
+ exchanges = {}
49
+ options[:exchanges].each do |exchange_name|
50
+ exchanges[exchange_name] = channel.exchange(exchange_name, :type => :topic, :durable => true)
56
51
  end
52
+ [connection, channel, exchanges]
57
53
  end
58
54
 
59
55
  def connect
60
- connection_options = { :url => Promiscuous::Config.publisher_amqp_url,
61
- :exchange => Promiscuous::Config.publisher_exchange,
62
- :confirm => true }
63
- @connection, @channel, @exchange = new_connection(connection_options)
56
+ connection_options = { :url => Promiscuous::Config.publisher_amqp_url,
57
+ :exchanges => [Promiscuous::Config.publisher_exchange,
58
+ Promiscuous::Config.sync_exchange],
59
+ :confirm => true }
60
+ @connection, @channel, @exchanges = new_connection(connection_options)
64
61
  end
65
62
 
66
63
  def disconnect
@@ -76,12 +73,13 @@ class Promiscuous::AMQP::Bunny
76
73
  end
77
74
 
78
75
  def raw_publish(options)
79
- options[:exchange].publish(options[:payload], :key => options[:key], :persistent => true)
76
+ @exchanges[options[:exchange]].publish(options[:payload], :key => options[:key], :persistent => true)
80
77
  end
81
78
 
82
79
  def publish(options={})
83
- options[:exchange] ||= @exchange
84
- Promiscuous.debug "[publish] #{options[:exchange].name}/#{options[:key]} #{options[:payload]}"
80
+ raise "Exchange '#{options[:exchange]}' not one of: #{@exchanges.keys}" unless @exchanges[options[:exchange]]
81
+
82
+ Promiscuous.debug "[publish] #{options[:exchange]}/#{options[:key]} #{options[:payload]}"
85
83
 
86
84
  @connection_lock.synchronize do
87
85
  tag = @channel.next_publish_seq_no if options[:on_confirm]
@@ -111,28 +109,73 @@ class Promiscuous::AMQP::Bunny
111
109
  @lock = Mutex.new
112
110
  @prefetch = Promiscuous::Config.prefetch
113
111
 
112
+ configure_rabbit
113
+
114
114
  connection_options = { :url => Promiscuous::Config.subscriber_amqp_url,
115
115
  :exchanges => options[:bindings].keys,
116
116
  :prefetch => @prefetch }
117
117
  @connection, @channel, exchanges = Promiscuous::AMQP.new_connection(connection_options)
118
118
 
119
- @queue = @channel.queue(Promiscuous::Config.queue_name, Promiscuous::Config.queue_options)
120
- exchanges.zip(options[:bindings].values).each do |exchange, bindings|
119
+ create_queues(@channel)
120
+
121
+ # Main queue binding
122
+ exchanges.keys.zip(options[:bindings].values).each do |exchange, bindings|
121
123
  bindings.each do |binding|
122
124
  @queue.bind(exchange, :routing_key => binding)
123
- Promiscuous.debug "[bind] #{exchange.name}/#{binding}/#{Promiscuous::Config.queue_name}"
125
+ Promiscuous.debug "[bind] #{exchange}/#{binding}/#{Promiscuous::Config.queue_name}"
124
126
  end
125
127
  end
126
128
 
129
+ # Error queue binding
130
+ @error_queue.bind(Promiscuous::Config.error_exchange, :routing_key => Promiscuous::Config.error_routing)
131
+
127
132
  @subscription = subscribe_queue(@queue, &block)
128
133
  end
129
134
 
135
+ def configure_rabbit
136
+ policy = {
137
+ "dead-letter-routing-key" => Promiscuous::Config.error_routing,
138
+ "dead-letter-exchange" => Promiscuous::Config.error_exchange
139
+ }.merge(Promiscuous::Config.queue_policy)
140
+
141
+ Promiscuous::Rabbit::Policy.set Promiscuous::Config.queue_name,
142
+ {
143
+ "pattern" => Promiscuous::Config.queue_name,
144
+ "apply-to" => "queues",
145
+ "definition" => policy
146
+ }
147
+
148
+ policy = {
149
+ "message-ttl" => Promiscuous::Config.error_ttl,
150
+ "dead-letter-routing-key" => Promiscuous::Config.retry_routing,
151
+ "dead-letter-exchange" => Promiscuous::Config.error_exchange
152
+ }.merge(Promiscuous::Config.queue_policy)
153
+ Promiscuous::Rabbit::Policy.set Promiscuous::Config.error_queue_name,
154
+ {
155
+ "pattern" => Promiscuous::Config.error_queue_name,
156
+ "apply-to" => "queues",
157
+ "definition" => policy
158
+ }
159
+ end
160
+
130
161
  def subscribe_queue(queue, &block)
131
- queue.subscribe(:ack => true) do |delivery_info, metadata, payload|
162
+ queue.subscribe(:manual_ack => true) do |delivery_info, metadata, payload|
132
163
  block.call(MetaData.new(self, delivery_info), payload)
133
164
  end
134
165
  end
135
166
 
167
+ def create_queues(channel)
168
+ @queue = channel.queue(Promiscuous::Config.queue_name,
169
+ Promiscuous::Config.queue_options)
170
+
171
+ @error_queue = channel.queue(Promiscuous::Config.error_queue_name,
172
+ Promiscuous::Config.queue_options)
173
+ end
174
+
175
+ def delete_queues
176
+ [@error_queue, @queue].each { |queue| queue.try(:delete) }
177
+ end
178
+
136
179
  class MetaData
137
180
  def initialize(subscriber, delivery_info)
138
181
  @subscriber = subscriber
@@ -143,8 +186,8 @@ class Promiscuous::AMQP::Bunny
143
186
  @subscriber.ack_message(@delivery_info.delivery_tag)
144
187
  end
145
188
 
146
- def postpone
147
- @subscriber.postpone_message
189
+ def nack
190
+ @subscriber.nack_message(@delivery_info.delivery_tag)
148
191
  end
149
192
  end
150
193
 
@@ -152,13 +195,8 @@ class Promiscuous::AMQP::Bunny
152
195
  @lock.synchronize { @channel.ack(tag) } if @channel
153
196
  end
154
197
 
155
- def postpone_message
156
- # Not using nacks, because the message gets sent back right away so this
157
- # is a no-op.
158
-
159
- # TODO: Even though the prefetch window is set to 10mil we should still
160
- # check that the unacked messages doesn't exceed this limit and increase
161
- # the prefetch window.
198
+ def nack_message(tag)
199
+ @lock.synchronize { @channel.nack(tag) } if @channel
162
200
  end
163
201
 
164
202
  def recover
@@ -20,7 +20,7 @@ class Promiscuous::AMQP::Fake
20
20
  end
21
21
 
22
22
  def publish(options={})
23
- Promiscuous.debug "[publish (fake)] #{options[:exchange].try(:name) || "default"}/#{options[:key]} #{options[:payload]}"
23
+ Promiscuous.debug "[publish (fake)] #{options[:exchange] || "default"}/#{options[:key]} #{options[:payload]}"
24
24
  @messages << options
25
25
  options[:on_confirm].try(:call)
26
26
  end
@@ -47,5 +47,8 @@ class Promiscuous::AMQP::Fake
47
47
 
48
48
  def disconnect
49
49
  end
50
+
51
+ def delete_queues
52
+ end
50
53
  end
51
54
  end
@@ -55,7 +55,7 @@ class Promiscuous::CLI
55
55
  bar = ProgressBar.create(:format => '%t |%b>%i| %c/%C %e', :title => title, :total => criteria.count)
56
56
  criteria.each do |doc|
57
57
  break if @stop
58
- doc.promiscuous.sync
58
+ doc.promiscuous.sync(Promiscuous::Config.sync_all_routing)
59
59
  bar.increment
60
60
  end
61
61
  end
@@ -1,12 +1,13 @@
1
1
  module Promiscuous::Config
2
2
  mattr_accessor :app, :backend, :amqp_url,
3
3
  :publisher_amqp_url, :subscriber_amqp_url, :publisher_exchange,
4
- :subscriber_exchanges, :queue_name, :queue_options,
5
- :redis_url, :redis_stats_url, :stats_interval,
6
- :socket_timeout, :heartbeat,
4
+ :subscriber_exchanges, :sync_exchange, :queue_name, :queue_options,
5
+ :redis_url, :redis_stats_url, :stats_interval, :error_queue_name,
6
+ :socket_timeout, :heartbeat, :sync_all_routing, :rabbit_mgmt_url,
7
7
  :prefetch, :recovery_timeout, :recovery_interval, :logger, :subscriber_threads,
8
- :version_field, :error_notifier, :transport_collection,
9
- :on_stats, :max_retries, :generation, :destroy_timeout, :destroy_check_interval
8
+ :version_field, :error_notifier, :transport_collection, :queue_policy, :test_mode,
9
+ :on_stats, :max_retries, :generation, :destroy_timeout, :destroy_check_interval,
10
+ :error_exchange, :error_routing, :retry_routing, :error_ttl, :transport_persistence
10
11
 
11
12
  def self.backend=(value)
12
13
  @@backend = value
@@ -31,18 +32,35 @@ module Promiscuous::Config
31
32
  end
32
33
  end
33
34
 
35
+ def self.best_transport_persistence
36
+ if defined?(Mongoid::Document)
37
+ self.transport_persistence = :mongoid
38
+ elsif defined?(ActiveRecord::Base)
39
+ self.transport_persistence = :active_record
40
+ end
41
+ end
42
+
34
43
  def self._configure(&block)
35
44
  block.call(self) if block
36
45
 
37
46
  self.app ||= Rails.application.class.parent_name.underscore rescue nil if defined?(Rails)
38
47
  self.backend ||= best_amqp_backend
39
48
  self.amqp_url ||= 'amqp://guest:guest@localhost:5672'
49
+ self.rabbit_mgmt_url ||= 'http://guest:guest@localhost:15672'
40
50
  self.publisher_amqp_url ||= self.amqp_url
41
51
  self.subscriber_amqp_url ||= self.amqp_url
42
- self.publisher_exchange ||= Promiscuous::AMQP::LIVE_EXCHANGE
43
- self.subscriber_exchanges ||= [Promiscuous::AMQP::LIVE_EXCHANGE]
44
- self.queue_name ||= "#{self.app}.promiscuous"
45
- self.queue_options ||= {:durable => true, :arguments => {'x-ha-policy' => 'all'}}
52
+ self.publisher_exchange ||= 'promiscuous'
53
+ self.sync_exchange ||= 'promiscuous.sync'
54
+ self.subscriber_exchanges ||= [self.publisher_exchange]
55
+ self.sync_all_routing ||= :__all__
56
+ self.queue_name ||= "#{self.app}.subscriber"
57
+ self.error_exchange ||= "#{self.app}.error"
58
+ self.error_queue_name ||= "#{self.app}.error"
59
+ self.error_routing ||= :__error__
60
+ self.retry_routing ||= :__retry__
61
+ self.error_ttl ||= 30000
62
+ self.queue_policy ||= { 'ha-mode' => 'all' }
63
+ self.queue_options ||= { :durable => true }
46
64
  self.redis_url ||= 'redis://localhost/'
47
65
  # TODO self.redis_slave_url ||= nil
48
66
  self.redis_stats_url ||= self.redis_url
@@ -62,6 +80,16 @@ module Promiscuous::Config
62
80
  self.generation ||= 0
63
81
  self.destroy_timeout ||= 1.hour
64
82
  self.destroy_check_interval ||= 10.minutes
83
+ self.transport_persistence ||= best_transport_persistence
84
+ self.test_mode = set_test_mode
85
+ end
86
+
87
+ def self.set_test_mode
88
+ if self.test_mode.nil?
89
+ defined?(Rails) ? Rails.env.test? ? true : false : false
90
+ else
91
+ self.test_mode
92
+ end
65
93
  end
66
94
 
67
95
  def self.configure(&block)
@@ -40,11 +40,15 @@ module Promiscuous::Publisher::Model::Base
40
40
  @instance.id
41
41
  end
42
42
 
43
- def sync(options={}, &block)
43
+ def sync(target)
44
44
  raise "Model cannot be dirty (have changes) when syncing" if @instance.changed?
45
+ raise "Model has to be reloaded if it was saved" if @instance.previous_changes.present?
45
46
 
46
47
  # We can use the ephemeral because both are mongoid and ephemerals are atomic operations.
47
- Promiscuous::Publisher::Operation::Ephemeral.new(:instance => @instance, :operation => :update).execute
48
+ Promiscuous::Publisher::Operation::Ephemeral.new(:instance => @instance,
49
+ :operation => :update,
50
+ :routing => target,
51
+ :exchange => Promiscuous::Config.sync_exchange).execute
48
52
  end
49
53
 
50
54
  end
@@ -23,7 +23,7 @@ module Promiscuous::Publisher::Model::Mongoid
23
23
  class PromiscuousMethods
24
24
  include Promiscuous::Publisher::Model::Base::PromiscuousMethodsBase
25
25
 
26
- def sync(options={}, &block)
26
+ def sync(target)
27
27
  raise "Use promiscuous.sync on the parent instance" if @instance.embedded?
28
28
  super
29
29
  end
@@ -59,6 +59,11 @@ class ActiveRecord::Base
59
59
  with_promiscuous_transaction_context { |tx| tx.commit }
60
60
  end
61
61
 
62
+ def supports_returning_statments?
63
+ @supports_returning_statments ||= ["ActiveRecord::ConnectionAdapters::PostgreSQLAdapter",
64
+ "ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter"].include?(self.class.name)
65
+ end
66
+
62
67
  alias_method :insert_without_promiscuous, :insert
63
68
  alias_method :update_without_promiscuous, :update
64
69
  alias_method :delete_without_promiscuous, :delete
@@ -152,10 +157,21 @@ class ActiveRecord::Base
152
157
  def db_operation_and_select
153
158
  # XXX This is only supported by Postgres and should be in the postgres driver
154
159
  @connection.transaction do
155
- @connection.exec_insert("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
156
- @instances = result.map do |row|
157
- instance = model.instantiate(row)
158
- instance
160
+ if @connection.supports_returning_statments?
161
+ @connection.exec_insert("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
162
+ @instances = result.map do |row|
163
+ instance = model.instantiate(row)
164
+ instance
165
+ end
166
+ end
167
+ else
168
+ @connection.exec_insert("#{@connection.to_sql(@arel, @binds)}", @operation_name, @binds)
169
+
170
+ id = @binds.select { |k,v| k.name == 'id' }.first.last rescue nil
171
+ id ||= @connection.instance_eval { @connection.last_id }
172
+ id.tap do |last_id|
173
+ result = @connection.exec_query("SELECT * FROM #{model.table_name} WHERE #{@pk} = #{last_id}")
174
+ @instances = result.map { |row| model.instantiate(row) }
159
175
  end
160
176
  end
161
177
  end
@@ -168,6 +184,7 @@ class ActiveRecord::Base
168
184
  def initialize(arel, name, binds, options={})
169
185
  super
170
186
  @operation = :update
187
+ return if Promiscuous.disabled?
171
188
  raise unless @arel.is_a?(Arel::UpdateManager)
172
189
  end
173
190
 
@@ -191,16 +208,31 @@ class ActiveRecord::Base
191
208
  (updated_fields_in_query.keys & model.published_db_fields).present?
192
209
  end
193
210
 
211
+ def sql_select_statment
212
+ arel = @arel.dup
213
+ arel.instance_eval { @ast = @ast.dup }
214
+ arel.ast.values = []
215
+ arel.to_sql.sub(/^UPDATE /, 'SELECT * FROM ')
216
+ end
217
+
194
218
  def db_operation_and_select
195
219
  # TODO this should be in the postgres driver (to also leverage the cache)
196
- @arel.ast.values << Arel::Nodes::SqlLiteral.new("\"#{Promiscuous::Config.version_field}\" = COALESCE(\"#{Promiscuous::Config.version_field}\", 0) + 1")
197
-
198
- @connection.exec_query("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
199
- @instances = result.map { |row| model.instantiate(row) }
200
- end.rows.size
220
+ @arel.ast.values << Arel::Nodes::SqlLiteral.new("#{Promiscuous::Config.version_field} = COALESCE(#{Promiscuous::Config.version_field}, 0) + 1")
221
+
222
+ if @connection.supports_returning_statments?
223
+ @connection.exec_query("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
224
+ @instances = result.map { |row| model.instantiate(row) }
225
+ end.rows.size
226
+ else
227
+ @connection.exec_update(@connection.to_sql(@arel, @binds), @operation_name, @binds).tap do
228
+ result = @connection.exec_query(sql_select_statment, @operation_name)
229
+ @instances = result.map { |row| model.instantiate(row) }
230
+ end
231
+ end
201
232
  end
202
233
 
203
234
  def execute(&db_operation)
235
+ return db_operation.call if Promiscuous.disabled?
204
236
  return db_operation.call unless model
205
237
  return db_operation.call unless any_published_field_changed?
206
238
  super
@@ -214,11 +246,20 @@ class ActiveRecord::Base
214
246
  raise unless @arel.is_a?(Arel::DeleteManager)
215
247
  end
216
248
 
249
+ def sql_select_statment
250
+ @connection.to_sql(@arel.dup, @binds.dup).sub(/^DELETE /, 'SELECT * ')
251
+ end
252
+
217
253
  def db_operation_and_select
218
- # XXX This is only supported by Postgres.
219
- @connection.exec_query("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
254
+ if @connection.supports_returning_statments?
255
+ @connection.exec_query("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
256
+ @instances = result.map { |row| model.instantiate(row) }
257
+ end.rows.size
258
+ else
259
+ result = @connection.exec_query(sql_select_statment, @operation_name, @binds)
220
260
  @instances = result.map { |row| model.instantiate(row) }
221
- end.rows.size
261
+ @connection.exec_delete(@connection.to_sql(@arel, @binds), @operation_name, @binds)
262
+ end
222
263
  end
223
264
  end
224
265
 
@@ -1,5 +1,5 @@
1
1
  class Promiscuous::Publisher::Operation::Base
2
- attr_accessor :operation, :recovering
2
+ attr_accessor :operation, :recovering, :routing, :exchange
3
3
 
4
4
  def initialize(options={})
5
5
  @operation = options[:operation]
@@ -44,8 +44,8 @@ class Promiscuous::Publisher::Operation::Base
44
44
  "Unknown database operation"
45
45
  end
46
46
 
47
- def create_transport_batch(operations)
48
- Promiscuous::Publisher::Transport::Batch.new.tap do |batch|
47
+ def create_transport_batch(operations, options={})
48
+ Promiscuous::Publisher::Transport::Batch.new(options).tap do |batch|
49
49
  operations.map do |operation|
50
50
  batch.add operation.operation, operation.instances
51
51
  end
@@ -2,6 +2,8 @@ class Promiscuous::Publisher::Operation::Ephemeral < Promiscuous::Publisher::Ope
2
2
  def initialize(options={})
3
3
  super
4
4
  @instance = options[:instance]
5
+ @routing = options[:routing]
6
+ @exchange = options[:exchange]
5
7
  end
6
8
 
7
9
  def instances
@@ -9,7 +11,7 @@ class Promiscuous::Publisher::Operation::Ephemeral < Promiscuous::Publisher::Ope
9
11
  end
10
12
 
11
13
  def execute_instrumented(query)
12
- create_transport_batch([self]).publish(true)
14
+ create_transport_batch([self], :exchange => @exchange, :routing => @routing).publish(true)
13
15
  end
14
16
 
15
17
  def increment_version_in_document
@@ -2,13 +2,20 @@ class Promiscuous::Publisher::Transport
2
2
  extend Promiscuous::Autoload
3
3
  autoload :Batch, :Worker, :Persistence
4
4
 
5
- class_attribute :persistence
5
+ def self.persistence
6
+ unless @persistence_key == Promiscuous::Config.transport_persistence
7
+ fix_inflections
8
+ @persistence = Persistence.const_get(Promiscuous::Config.transport_persistence.to_s.classify).new
9
+ @persistence_key = Promiscuous::Config.transport_persistence
10
+ end
11
+ @persistence
12
+ end
13
+
14
+ private
6
15
 
7
- if defined?(Mongoid::Document)
8
- self.persistence = Persistence::Mongoid.new
9
- elsif defined?(ActiveRecord::Base)
10
- self.persistence = Persistence::ActiveRecord.new
11
- else
12
- raise "Either Mongoid or ActiveRecord support required"
16
+ def self.fix_inflections
17
+ ActiveSupport::Inflector.inflections do |inflect|
18
+ inflect.uncountable "redis"
19
+ end
13
20
  end
14
21
  end
@@ -1,25 +1,25 @@
1
1
  class Promiscuous::Publisher::Transport::Batch
2
- attr_accessor :operations, :payload_attributes, :timestamp, :id
2
+ attr_accessor :operations, :payload_attributes, :timestamp, :id, :exchange, :routing
3
3
 
4
4
  SERIALIZER = MultiJson
5
5
 
6
6
  def self.load(id, dump)
7
- data = SERIALIZER.load(dump)
7
+ data = SERIALIZER.load(dump).with_indifferent_access
8
8
 
9
- batch = self.new
9
+ batch = self.new(data)
10
10
  batch.id = id
11
- batch.timestamp = data['timestamp']
12
- batch.payload_attributes = data['payload_attributes']
13
11
 
14
12
  data['operations'].each { |op_data| batch.operations << Operation.load(op_data) }
15
13
 
16
14
  batch
17
15
  end
18
16
 
19
- def initialize
17
+ def initialize(options={})
20
18
  self.operations = []
21
- self.payload_attributes = {}
22
- self.timestamp = Time.now
19
+ self.payload_attributes = options[:payload_attributes] || {}
20
+ self.exchange = options[:exchange] || Promiscuous::Config.publisher_exchange
21
+ self.routing = options[:routing] || Promiscuous::Config.sync_all_routing
22
+ self.timestamp = options[:timestamp] || Time.now
23
23
  end
24
24
 
25
25
  def add(type, instances)
@@ -39,7 +39,9 @@ class Promiscuous::Publisher::Transport::Batch
39
39
 
40
40
  begin
41
41
  if self.operations.present?
42
- Promiscuous::AMQP.publish(:key => Promiscuous::Config.app, :payload => self.payload,
42
+ Promiscuous::AMQP.publish(:exchange => self.exchange,
43
+ :key => self.routing.to_s,
44
+ :payload => self.payload,
43
45
  :on_confirm => method(:on_rabbitmq_confirm))
44
46
  else
45
47
  on_rabbitmq_confirm
@@ -79,7 +81,9 @@ class Promiscuous::Publisher::Transport::Batch
79
81
  {
80
82
  :operations => self.operations.map(&:dump),
81
83
  :payload_attributes => self.payload_attributes,
82
- :timestamp => self.timestamp
84
+ :timestamp => self.timestamp,
85
+ :exchange => self.exchange,
86
+ :routing => self.routing
83
87
  })
84
88
  end
85
89
 
@@ -1,6 +1,6 @@
1
1
  class Promiscuous::Publisher::Transport::Persistence
2
2
  extend Promiscuous::Autoload
3
- autoload :Mongoid, :ActiveRecord
3
+ autoload :Mongoid, :ActiveRecord, :Redis
4
4
 
5
5
  def save(batch)
6
6
  # Implemented by subclasses
@@ -2,19 +2,17 @@ class Promiscuous::Publisher::Transport::Persistence::ActiveRecord
2
2
  def save(batch)
3
3
  check_schema
4
4
 
5
- q = "INSERT INTO #{table} (\"batch\") " +
6
- "VALUES ('#{batch.dump}') RETURNING id"
5
+ q = "INSERT INTO #{table} (batch) " +
6
+ "VALUES ('#{batch.dump}')"
7
7
 
8
- result = connection.exec_query(q, 'Promiscuous Recovery Save')
9
-
10
- batch.id = result.rows.first.first.to_i
8
+ batch.id = connection.insert_sql(q, 'Promiscuous Recovery Save')
11
9
  end
12
10
 
13
11
  def expired
14
12
  check_schema
15
13
 
16
14
  q = "SELECT id, p.batch FROM #{table} p " +
17
- "WHERE at < current_timestamp - #{Promiscuous::Config.recovery_timeout} * INTERVAL '1 second'"
15
+ "WHERE at < CURRENT_TIMESTAMP - INTERVAL '#{Promiscuous::Config.recovery_timeout}' second"
18
16
 
19
17
  connection.exec_query(q, 'Promiscuous Recovery Expired').rows
20
18
  end
@@ -24,7 +22,7 @@ class Promiscuous::Publisher::Transport::Persistence::ActiveRecord
24
22
 
25
23
  q = "DELETE FROM #{table} WHERE id = #{batch.id}"
26
24
 
27
- connection.exec_query(q, 'Promiscuous Recovery Delete')
25
+ connection.exec_delete(q, 'Promiscuous Recovery Delete', [])
28
26
  end
29
27
 
30
28
  private
@@ -36,8 +34,8 @@ class Promiscuous::Publisher::Transport::Persistence::ActiveRecord
36
34
  raise <<-help
37
35
  Promiscuous requires the following migration to be run:
38
36
  create_table :_promiscuous do |t|
39
- t.string :batch
40
- t.timestamp :at, :default => :now
37
+ t.text :batch
38
+ t.timestamp :at, 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'
41
39
  end
42
40
  help
43
41
  end
@@ -0,0 +1,34 @@
1
+ class Promiscuous::Publisher::Transport::Persistence::Redis
2
+ def save(batch)
3
+ batch.id = SecureRandom.uuid
4
+
5
+ redis.multi do
6
+ redis.zadd(key, Time.now.utc.to_i, batch.id)
7
+ redis.set(key(batch.id), batch.dump)
8
+ end
9
+ end
10
+
11
+ def expired
12
+ redis.zrangebyscore(key, 0, Promiscuous::Config.recovery_timeout.seconds.ago.utc.to_i).map do |batch_id|
13
+ [batch_id, redis.get(key(batch_id))]
14
+ end
15
+ end
16
+
17
+ def delete(batch)
18
+ redis.multi do
19
+ redis.zrem(key, batch.id)
20
+ redis.del(key(batch.id))
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def redis
27
+ Promiscuous.ensure_connected
28
+ Promiscuous::Redis.connection
29
+ end
30
+
31
+ def key(id=nil)
32
+ Promiscuous::Key.new(:pub).join('transport').join(id)
33
+ end
34
+ end
@@ -30,7 +30,8 @@ class Promiscuous::Publisher::Transport::Worker
30
30
  Promiscuous::Config.error_notifier.call(e)
31
31
  end
32
32
  end
33
- ActiveRecord::Base.connection.close
33
+ ensure
34
+ ActiveRecord::Base.clear_active_connections!
34
35
  end
35
36
  end
36
37
 
@@ -0,0 +1,41 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module Promiscuous::Rabbit
5
+ class Policy
6
+ def self.set(queue, attributes)
7
+ uri = uri_for(queue)
8
+
9
+ request = Net::HTTP::Put.new(uri.request_uri, http_headers)
10
+ request.basic_auth(uri.user, uri.password) if uri.user
11
+ request.body = MultiJson.dump(attributes)
12
+
13
+ http = Net::HTTP.new(uri.host, uri.port)
14
+ response = http.request(request)
15
+
16
+ raise "Unable to connect to Rabbit #{response.body}" unless response.is_a?(Net::HTTPSuccess)
17
+ end
18
+
19
+ def self.delete(queue)
20
+ uri = uri_for(queue)
21
+
22
+ request = Net::HTTP::Delete.new(uri.request_uri, http_headers)
23
+ request.basic_auth(uri.user, uri.password) if uri.user
24
+
25
+ http = Net::HTTP.new(uri.host, uri.port)
26
+ response = http.request(request)
27
+
28
+ raise "Unable to connect to Rabbit #{response.body}" unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNotFound)
29
+ end
30
+
31
+ private
32
+
33
+ def self.uri_for(queue)
34
+ URI.parse("#{Promiscuous::Config.rabbit_mgmt_url}/api/policies/%2f/#{queue}-retry")
35
+ end
36
+
37
+ def self.http_headers
38
+ {'Content-Type' =>'application/json'}
39
+ end
40
+ end
41
+ end
@@ -42,6 +42,15 @@ class Promiscuous::Subscriber::Message
42
42
  Promiscuous::Config.error_notifier.call(e)
43
43
  end
44
44
 
45
+ def nack
46
+ Promiscuous.debug "[receive][failed] #{payload}"
47
+ @metadata.try(:nack)
48
+ rescue Exception => e
49
+ # We don't care if we fail, the message will be redelivered at some point
50
+ Promiscuous.warn "[receive] Some exception happened, but it's okay: #{e}\n#{e.backtrace.join("\n")}"
51
+ Promiscuous::Config.error_notifier.call(e)
52
+ end
53
+
45
54
  def process
46
55
  Promiscuous::Subscriber::UnitOfWork.process(self)
47
56
  rescue Exception => orig_e
@@ -33,7 +33,8 @@ module Promiscuous::Subscriber::Model::ActiveRecord
33
33
  end
34
34
 
35
35
  def __promiscuous_with_pooled_connection
36
- ActiveRecord::Base.connection_pool.with_connection { yield }
36
+ yield
37
+ ActiveRecord::Base.clear_active_connections!
37
38
  end
38
39
  end
39
40
  end
@@ -39,15 +39,9 @@ class Promiscuous::Subscriber::UnitOfWork
39
39
  begin
40
40
  on_message
41
41
  rescue Exception => e
42
- @fail_count ||= 0; @fail_count += 1
43
-
44
- if @fail_count <= Promiscuous::Config.max_retries
45
- Promiscuous.warn("[receive] #{e.message} #{@fail_count.ordinalize} retry: #{@message}")
46
- sleep @fail_count ** 2
47
- process_message
48
- else
49
- raise e
50
- end
42
+ Promiscuous::Config.error_notifier.call(e)
43
+ message.nack
44
+ raise e if Promiscuous::Config.test_mode
51
45
  end
52
46
  end
53
47
 
@@ -9,11 +9,18 @@ class Promiscuous::Subscriber::Worker::Pump
9
9
  def connect
10
10
  options = {}
11
11
  options[:bindings] = {}
12
- # We need to subscribe to everything to keep up with the version tracking
12
+ # We need to subscribe to everything to keep routing simple
13
13
  Promiscuous::Config.subscriber_exchanges.each do |exchange|
14
14
  options[:bindings][exchange] = ['*']
15
15
  end
16
16
 
17
+ # Subscribe to the sync exchange to make syncing not require any command
18
+ # line ops
19
+ options[:bindings][Promiscuous::Config.sync_exchange] = [Promiscuous::Config.app, Promiscuous::Config.sync_all_routing]
20
+
21
+ # Subscribe to the error exchange but only to retries
22
+ options[:bindings][Promiscuous::Config.error_exchange] = [Promiscuous::Config.retry_routing]
23
+
17
24
  subscribe(options, &method(:on_message))
18
25
  end
19
26
 
@@ -1,3 +1,3 @@
1
1
  module Promiscuous
2
- VERSION = '1.0.0.beta5'
2
+ VERSION = '1.0.0.beta6'
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: 1.0.0.beta5
4
+ version: 1.0.0.beta6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-07-03 00:00:00.000000000 Z
12
+ date: 2014-11-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -160,17 +160,19 @@ executables:
160
160
  extensions: []
161
161
  extra_rdoc_files: []
162
162
  files:
163
+ - bin/promiscuous
164
+ - lib/promiscuous.rb
165
+ - lib/promiscuous/amqp.rb
163
166
  - lib/promiscuous/amqp/bunny.rb
164
167
  - lib/promiscuous/amqp/fake.rb
165
168
  - lib/promiscuous/amqp/file.rb
166
- - lib/promiscuous/amqp/hot_bunnies.rb
167
169
  - lib/promiscuous/amqp/null.rb
168
- - lib/promiscuous/amqp.rb
169
170
  - lib/promiscuous/autoload.rb
170
171
  - lib/promiscuous/cli.rb
171
172
  - lib/promiscuous/config.rb
172
173
  - lib/promiscuous/convenience.rb
173
174
  - lib/promiscuous/dsl.rb
175
+ - lib/promiscuous/error.rb
174
176
  - lib/promiscuous/error/base.rb
175
177
  - lib/promiscuous/error/connection.rb
176
178
  - lib/promiscuous/error/lock_unavailable.rb
@@ -178,20 +180,21 @@ files:
178
180
  - lib/promiscuous/error/publisher.rb
179
181
  - lib/promiscuous/error/recovery.rb
180
182
  - lib/promiscuous/error/subscriber.rb
181
- - lib/promiscuous/error.rb
182
183
  - lib/promiscuous/key.rb
183
184
  - lib/promiscuous/loader.rb
184
185
  - lib/promiscuous/mongoid.rb
186
+ - lib/promiscuous/publisher.rb
187
+ - lib/promiscuous/publisher/context.rb
185
188
  - lib/promiscuous/publisher/context/base.rb
186
189
  - lib/promiscuous/publisher/context/transaction.rb
187
- - lib/promiscuous/publisher/context.rb
188
190
  - lib/promiscuous/publisher/mock_generator.rb
191
+ - lib/promiscuous/publisher/model.rb
189
192
  - lib/promiscuous/publisher/model/active_record.rb
190
193
  - lib/promiscuous/publisher/model/base.rb
191
194
  - lib/promiscuous/publisher/model/ephemeral.rb
192
195
  - lib/promiscuous/publisher/model/mock.rb
193
196
  - lib/promiscuous/publisher/model/mongoid.rb
194
- - lib/promiscuous/publisher/model.rb
197
+ - lib/promiscuous/publisher/operation.rb
195
198
  - lib/promiscuous/publisher/operation/active_record.rb
196
199
  - lib/promiscuous/publisher/operation/atomic.rb
197
200
  - lib/promiscuous/publisher/operation/base.rb
@@ -200,36 +203,34 @@ files:
200
203
  - lib/promiscuous/publisher/operation/non_persistent.rb
201
204
  - lib/promiscuous/publisher/operation/proxy_for_query.rb
202
205
  - lib/promiscuous/publisher/operation/transaction.rb
203
- - lib/promiscuous/publisher/operation.rb
206
+ - lib/promiscuous/publisher/transport.rb
204
207
  - lib/promiscuous/publisher/transport/batch.rb
208
+ - lib/promiscuous/publisher/transport/persistence.rb
205
209
  - lib/promiscuous/publisher/transport/persistence/active_record.rb
206
210
  - lib/promiscuous/publisher/transport/persistence/mongoid.rb
207
- - lib/promiscuous/publisher/transport/persistence.rb
211
+ - lib/promiscuous/publisher/transport/persistence/redis.rb
208
212
  - lib/promiscuous/publisher/transport/worker.rb
209
- - lib/promiscuous/publisher/transport.rb
210
213
  - lib/promiscuous/publisher/worker.rb
211
- - lib/promiscuous/publisher.rb
214
+ - lib/promiscuous/rabbit.rb
212
215
  - lib/promiscuous/railtie.rb
213
216
  - lib/promiscuous/redis.rb
217
+ - lib/promiscuous/subscriber.rb
214
218
  - lib/promiscuous/subscriber/message.rb
219
+ - lib/promiscuous/subscriber/model.rb
215
220
  - lib/promiscuous/subscriber/model/active_record.rb
216
221
  - lib/promiscuous/subscriber/model/base.rb
217
222
  - lib/promiscuous/subscriber/model/mongoid.rb
218
223
  - lib/promiscuous/subscriber/model/observer.rb
219
- - lib/promiscuous/subscriber/model.rb
220
224
  - lib/promiscuous/subscriber/operation.rb
221
225
  - lib/promiscuous/subscriber/unit_of_work.rb
226
+ - lib/promiscuous/subscriber/worker.rb
222
227
  - lib/promiscuous/subscriber/worker/eventual_destroyer.rb
223
228
  - lib/promiscuous/subscriber/worker/pump.rb
224
229
  - lib/promiscuous/subscriber/worker/recorder.rb
225
230
  - lib/promiscuous/subscriber/worker/runner.rb
226
231
  - lib/promiscuous/subscriber/worker/stats.rb
227
- - lib/promiscuous/subscriber/worker.rb
228
- - lib/promiscuous/subscriber.rb
229
232
  - lib/promiscuous/timer.rb
230
233
  - lib/promiscuous/version.rb
231
- - lib/promiscuous.rb
232
- - bin/promiscuous
233
234
  homepage: http://github.com/crowdtap/promiscuous
234
235
  licenses: []
235
236
  metadata: {}
@@ -249,8 +250,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
250
  version: 1.3.1
250
251
  requirements: []
251
252
  rubyforge_project:
252
- rubygems_version: 2.1.11
253
+ rubygems_version: 2.2.0
253
254
  signing_key:
254
255
  specification_version: 4
255
256
  summary: Replicate models across applications
256
257
  test_files: []
258
+ has_rdoc: false
@@ -1,66 +0,0 @@
1
- class Promiscuous::AMQP::HotBunnies < Promiscuous::AMQP::Bunny
2
- attr_accessor :connection
3
-
4
- def initialize_driver
5
- require 'hot_bunnies'
6
- end
7
-
8
- # TODO auto reconnect
9
-
10
- def raw_new_connection(options={})
11
- ::HotBunnies.connect(:uri => options[:url],
12
- :heartbeat_interval => Promiscuous::Config.heartbeat,
13
- :connection_timeout => Promiscuous::Config.socket_timeout)
14
- end
15
-
16
- def raw_confirm_select(channel, &callback)
17
- channel.add_confirm_listener(&callback)
18
- channel.confirm_select
19
- end
20
-
21
- def raw_publish(options={})
22
- options[:exchange].publish(options[:payload], :routing_key => options[:key], :persistent => true)
23
- end
24
-
25
- def disconnect
26
- @connection_lock.synchronize do
27
- return unless connected?
28
- @channel.close rescue nil
29
- @connection.close rescue nil
30
- @connection = @channel = nil
31
- end
32
- end
33
-
34
- def connected?
35
- !!@connection.try(:is_open)
36
- end
37
-
38
- module Subscriber
39
- include Promiscuous::AMQP::Bunny::Subscriber
40
-
41
- def subscribe_queue(queue, &block)
42
- queue.subscribe(:ack => true, :blocking => false) do |metadata, payload|
43
- block.call(MetaData.new(self, metadata), payload)
44
- end
45
- end
46
-
47
- class MetaData < Promiscuous::AMQP::Bunny::Subscriber::MetaData
48
- def initialize(subscriber, metadata)
49
- @subscriber = subscriber
50
- @metadata = metadata
51
- end
52
-
53
- def ack
54
- @metadata.ack
55
- end
56
- end
57
-
58
- def disconnect
59
- @lock.synchronize do
60
- @channel = nil
61
- @subscription.shutdown! rescue nil
62
- @connection.close rescue nil
63
- end
64
- end
65
- end
66
- end