promiscuous 1.0.0.beta5 → 1.0.0.beta6

Sign up to get free protection for your applications and to get access to all the features.
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