promiscuous 0.90.0 → 0.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/lib/promiscuous/amqp/bunny.rb +63 -36
  3. data/lib/promiscuous/amqp/fake.rb +3 -1
  4. data/lib/promiscuous/amqp/hot_bunnies.rb +26 -16
  5. data/lib/promiscuous/amqp/null.rb +1 -0
  6. data/lib/promiscuous/amqp.rb +12 -12
  7. data/lib/promiscuous/cli.rb +70 -29
  8. data/lib/promiscuous/config.rb +54 -29
  9. data/lib/promiscuous/convenience.rb +1 -1
  10. data/lib/promiscuous/dependency.rb +25 -6
  11. data/lib/promiscuous/error/connection.rb +11 -9
  12. data/lib/promiscuous/error/dependency.rb +8 -1
  13. data/lib/promiscuous/loader.rb +4 -2
  14. data/lib/promiscuous/publisher/bootstrap/connection.rb +25 -0
  15. data/lib/promiscuous/publisher/bootstrap/data.rb +127 -0
  16. data/lib/promiscuous/publisher/bootstrap/mode.rb +19 -0
  17. data/lib/promiscuous/publisher/bootstrap/status.rb +40 -0
  18. data/lib/promiscuous/publisher/bootstrap/version.rb +46 -0
  19. data/lib/promiscuous/publisher/bootstrap.rb +27 -0
  20. data/lib/promiscuous/publisher/context/base.rb +67 -0
  21. data/lib/promiscuous/{middleware.rb → publisher/context/middleware.rb} +16 -13
  22. data/lib/promiscuous/publisher/context/transaction.rb +36 -0
  23. data/lib/promiscuous/publisher/context.rb +4 -88
  24. data/lib/promiscuous/publisher/mock_generator.rb +9 -9
  25. data/lib/promiscuous/publisher/model/active_record.rb +7 -7
  26. data/lib/promiscuous/publisher/model/base.rb +29 -29
  27. data/lib/promiscuous/publisher/model/ephemeral.rb +5 -3
  28. data/lib/promiscuous/publisher/model/mock.rb +9 -5
  29. data/lib/promiscuous/publisher/model/mongoid.rb +5 -22
  30. data/lib/promiscuous/publisher/operation/active_record.rb +360 -0
  31. data/lib/promiscuous/publisher/operation/atomic.rb +167 -0
  32. data/lib/promiscuous/publisher/operation/base.rb +279 -474
  33. data/lib/promiscuous/publisher/operation/mongoid.rb +153 -145
  34. data/lib/promiscuous/publisher/operation/non_persistent.rb +28 -0
  35. data/lib/promiscuous/publisher/operation/proxy_for_query.rb +42 -0
  36. data/lib/promiscuous/publisher/operation/transaction.rb +85 -0
  37. data/lib/promiscuous/publisher/operation.rb +1 -1
  38. data/lib/promiscuous/publisher/worker.rb +7 -7
  39. data/lib/promiscuous/publisher.rb +1 -1
  40. data/lib/promiscuous/railtie.rb +20 -5
  41. data/lib/promiscuous/redis.rb +104 -56
  42. data/lib/promiscuous/subscriber/message_processor/base.rb +38 -0
  43. data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +17 -0
  44. data/lib/promiscuous/subscriber/message_processor/regular.rb +192 -0
  45. data/lib/promiscuous/subscriber/message_processor.rb +4 -0
  46. data/lib/promiscuous/subscriber/model/base.rb +20 -15
  47. data/lib/promiscuous/subscriber/model/mongoid.rb +4 -4
  48. data/lib/promiscuous/subscriber/model/observer.rb +16 -2
  49. data/lib/promiscuous/subscriber/operation/base.rb +68 -0
  50. data/lib/promiscuous/subscriber/operation/bootstrap.rb +54 -0
  51. data/lib/promiscuous/subscriber/operation/regular.rb +13 -0
  52. data/lib/promiscuous/subscriber/operation.rb +3 -166
  53. data/lib/promiscuous/subscriber/worker/message.rb +61 -35
  54. data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +90 -59
  55. data/lib/promiscuous/subscriber/worker/pump.rb +17 -5
  56. data/lib/promiscuous/subscriber/worker/recorder.rb +4 -1
  57. data/lib/promiscuous/subscriber/worker/runner.rb +49 -9
  58. data/lib/promiscuous/subscriber/worker/stats.rb +2 -2
  59. data/lib/promiscuous/subscriber/worker.rb +6 -0
  60. data/lib/promiscuous/subscriber.rb +1 -1
  61. data/lib/promiscuous/timer.rb +31 -18
  62. data/lib/promiscuous/version.rb +1 -1
  63. data/lib/promiscuous.rb +23 -3
  64. metadata +104 -89
  65. data/lib/promiscuous/subscriber/payload.rb +0 -34
@@ -2,17 +2,17 @@ module Promiscuous::Publisher::Model::ActiveRecord
2
2
  extend ActiveSupport::Concern
3
3
  include Promiscuous::Publisher::Model::Base
4
4
 
5
- # TODO FIXME This needs some serious work. We need to hook deeper.
6
-
7
- included do
8
- around_create { |&block| promiscuous.sync(:operation => :create, &block) }
9
- around_update { |&block| promiscuous.sync(:operation => :update, &block) }
10
- around_destroy { |&block| promiscuous.sync(:operation => :destroy, &block) }
11
- end
5
+ require 'promiscuous/publisher/operation/active_record'
12
6
 
13
7
  module ClassMethods
14
8
  def __promiscuous_missing_record_exception
15
9
  ActiveRecord::RecordNotFound
16
10
  end
11
+
12
+ def belongs_to(*args, &block)
13
+ super.tap do |association|
14
+ publish(association.foreign_key) if self.in_publish_block?
15
+ end
16
+ end
17
17
  end
18
18
  end
@@ -2,7 +2,7 @@ module Promiscuous::Publisher::Model::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- class_attribute :publish_to, :published_attrs, :tracked_attrs
5
+ class_attribute :published_attrs, :tracked_attrs
6
6
  cattr_accessor :published_db_fields # There is one on each root class, none on the subclasses
7
7
  self.published_attrs = []
8
8
  self.tracked_attrs = []
@@ -16,23 +16,14 @@ module Promiscuous::Publisher::Model::Base
16
16
  @instance = instance
17
17
  end
18
18
 
19
- def sync(options={}, &block)
20
- options = {:instance => @instance, :operation => :update}.merge(options)
21
- Promiscuous::Publisher::Operation::Base.new(options).execute(&block)
22
- end
23
-
24
19
  def payload(options={})
25
- # It's nice to see the entire payload in one piece, not merged 36 times
26
- # TODO migrate this format to something that makes more sense once the store is out
27
20
  msg = {}
28
- msg[:__amqp__] = @instance.class.publish_to
29
- msg[:type] = @instance.class.publish_as # for backward compatibility
30
- msg[:ancestors] = @instance.class.ancestors.select { |a| a < Promiscuous::Publisher::Model::Base }.map(&:publish_as)
31
- msg[:id] = @instance.id.to_s
32
- if @instance.respond_to?(:promiscuous_payload)
33
- msg[:payload] = @instance.promiscuous_payload
34
- else
35
- msg[:payload] = self.attributes unless options[:with_attributes] == false
21
+ msg[:types] = @instance.class.ancestors.select { |a| a < Promiscuous::Publisher::Model::Base }.map(&:publish_as)
22
+ msg[:id] = @instance.id.to_s
23
+ unless options[:with_attributes] == false
24
+ # promiscuous_payload is useful to implement relays
25
+ msg[:attributes] = @instance.respond_to?(:promiscuous_payload) ? @instance.promiscuous_payload :
26
+ self.attributes
36
27
  end
37
28
  msg
38
29
  end
@@ -57,10 +48,16 @@ module Promiscuous::Publisher::Model::Base
57
48
  # FIXME This is not sufficient, we need to consider the previous and next
58
49
  # values in case of an update.
59
50
  # Note that the caller expect the id dependency to come first
60
- @instance.class.tracked_attrs
61
- .map { |attr| [attr, @instance.__send__(attr)] }
62
- .map { |attr, value| get_dependency(attr, value) }
63
- .compact
51
+ @instance.class.tracked_attrs.map do |attr|
52
+ begin
53
+ [attr, @instance.__send__(attr)]
54
+ rescue Exception => e
55
+ # Don't care about missing attributes for read dependencies.
56
+ raise e unless options[:allow_missing_attributes] && e.is_a?(ActiveModel::MissingAttributeError)
57
+ end
58
+ end
59
+ .map { |attr, value| get_dependency(attr, value) }
60
+ .compact
64
61
  end
65
62
  end
66
63
 
@@ -88,10 +85,6 @@ module Promiscuous::Publisher::Model::Base
88
85
 
89
86
  # TODO reject invalid options
90
87
 
91
- if attributes.present? && self.publish_to && options[:to] && self.publish_to != options[:to]
92
- raise 'versionned publishing is not supported yet'
93
- end
94
- self.publish_to ||= options[:to] || "#{Promiscuous::Config.app}/#{self.name.underscore}"
95
88
  @publish_as = options[:as].to_s if options[:as]
96
89
 
97
90
  ([self] + descendants).each do |klass|
@@ -103,6 +96,18 @@ module Promiscuous::Publisher::Model::Base
103
96
  klass.published_db_fields |= attributes # aliased fields are resolved later
104
97
  klass.published_attrs |= attributes
105
98
  end
99
+
100
+
101
+ begin
102
+ @in_publish_block = @in_publish_block.to_i + 1
103
+ block.call if block
104
+ ensure
105
+ @in_publish_block -= 1
106
+ end
107
+ end
108
+
109
+ def in_publish_block?
110
+ @in_publish_block.to_i > 0
106
111
  end
107
112
 
108
113
  def track_dependencies_of(*attributes)
@@ -127,10 +132,5 @@ module Promiscuous::Publisher::Model::Base
127
132
  subclass.tracked_attrs = self.tracked_attrs.dup
128
133
  # no copy for published_db_fields
129
134
  end
130
-
131
- class None; end
132
- def promiscuous_missing_record_exception
133
- None
134
- end
135
135
  end
136
136
  end
@@ -9,8 +9,8 @@ module Promiscuous::Publisher::Model::Ephemeral
9
9
  value = super
10
10
  if value.is_a?(Array) &&
11
11
  value.first.is_a?(Promiscuous::Publisher::Model::Ephemeral)
12
- value = {:__amqp__ => '__promiscuous__/embedded_many',
13
- :payload => value.map(&:promiscuous).map(&:payload)}
12
+ value = {:types => ['Promiscuous::EmbeddedDocs'],
13
+ :attributes => value.map(&:promiscuous).map(&:payload)}
14
14
  end
15
15
  value
16
16
  end
@@ -32,7 +32,9 @@ module Promiscuous::Publisher::Model::Ephemeral
32
32
  operation = :create
33
33
  operation = :update unless self.new_record
34
34
  operation = :destroy if self.destroyed
35
- promiscuous.sync(:operation => operation)
35
+
36
+ Promiscuous::Publisher::Operation::Atomic.new(:instance => self, :operation => operation).execute {}
37
+
36
38
  self.new_record = false
37
39
  true
38
40
  end
@@ -2,7 +2,10 @@ module Promiscuous::Publisher::Model::Mock
2
2
  extend ActiveSupport::Concern
3
3
  include Promiscuous::Publisher::Model::Ephemeral
4
4
 
5
- included { class_attribute :mock_options }
5
+ included do
6
+ class_attribute :mock_options
7
+ self.mock_options = {}
8
+ end
6
9
 
7
10
  def initialize(attrs={})
8
11
  self.id = __get_new_id
@@ -10,7 +13,7 @@ module Promiscuous::Publisher::Model::Mock
10
13
  end
11
14
 
12
15
  def __get_new_id
13
- if self.class.mock_options.try(:[], :id) == :bson
16
+ if self.class.mock_options[:id] == :bson
14
17
  BSON::ObjectId.new
15
18
  else
16
19
  # XXX Not thread safe
@@ -24,8 +27,8 @@ module Promiscuous::Publisher::Model::Mock
24
27
  include Promiscuous::Publisher::Model::Ephemeral::PromiscuousMethodsEphemeral
25
28
 
26
29
  def sync(options={}, &block)
27
- payload = self.payload
28
- payload[:operation] = options[:operation] || :update
30
+ payload[:operations] = [self.payload.merge(:operation => options[:operation] || :update)]
31
+ payload[:app] = self.class.mock_options[:from]
29
32
  Promiscuous::Subscriber::Worker::Message.new(MultiJson.dump(payload)).process
30
33
  end
31
34
  end
@@ -37,7 +40,8 @@ module Promiscuous::Publisher::Model::Mock
37
40
  end
38
41
 
39
42
  def mock(options={})
40
- self.mock_options = options
43
+ # careful, all subclasses will be touched
44
+ self.mock_options.merge!(options)
41
45
  end
42
46
 
43
47
  def publish(*args, &block)
@@ -11,9 +11,8 @@ module Promiscuous::Publisher::Model::Mongoid
11
11
  # Important for the query hooks (see ../operation/mongoid.rb)
12
12
  # We want the root class when we do a collection name lookup
13
13
 
14
- root_class = self.collection.name.singularize.camelize.constantize
15
- unless self == root_class
16
- raise "Please include Promiscuous::Publisher in #{root_class} first (the root class)"
14
+ if self.superclass.include?(Mongoid::Document)
15
+ raise "Please include Promiscuous::Publisher in the root class of #{self}"
17
16
  end
18
17
 
19
18
  Promiscuous::Publisher::Model::Mongoid.collection_mapping[self.collection.name] = self
@@ -32,8 +31,8 @@ module Promiscuous::Publisher::Model::Mongoid
32
31
  if value.is_a?(Array) &&
33
32
  value.respond_to?(:ancestors) &&
34
33
  value.ancestors.any? { |a| a == Promiscuous::Publisher::Model::Mongoid }
35
- value = {:__amqp__ => '__promiscuous__/embedded_many',
36
- :payload => value.map(&:promiscuous).map(&:payload)}
34
+ value = {:types => ['Promiscuous::EmbeddedDocs'],
35
+ :attributes => value.map(&:promiscuous).map(&:payload)}
37
36
  end
38
37
  value
39
38
  end
@@ -41,22 +40,10 @@ module Promiscuous::Publisher::Model::Mongoid
41
40
 
42
41
  module ClassMethods
43
42
  # TODO DRY this up with the publisher side
44
- def publish(*args, &block)
45
- super
46
- return unless block
47
-
48
- begin
49
- @in_publish_block = true
50
- block.call
51
- ensure
52
- @in_publish_block = false
53
- end
54
- end
55
-
56
43
  def self.publish_on(method, options={})
57
44
  define_method(method) do |name, *args, &block|
58
45
  super(name, *args, &block)
59
- if @in_publish_block
46
+ if self.in_publish_block?
60
47
  name = args.last[:as] if args.last.is_a?(Hash) && args.last[:as]
61
48
  publish(name)
62
49
  end
@@ -78,9 +65,5 @@ module Promiscuous::Publisher::Model::Mongoid
78
65
  Moped::PromiscuousQueryWrapper::PromiscuousQueryOperation
79
66
  end
80
67
  end
81
-
82
- def promiscuous_missing_record_exception
83
- Mongoid::Errors::DocumentNotFound
84
- end
85
68
  end
86
69
  end
@@ -0,0 +1,360 @@
1
+ class ActiveRecord::Base
2
+ module PostgresSQL2PCExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ def prepare_db_transaction
6
+ execute("PREPARE TRANSACTION '#{quote_string(@current_transaction_id)}'")
7
+ end
8
+
9
+ def commit_prepared_db_transaction(xid)
10
+ # We might always be racing with another instance, these sort of errors
11
+ # are spurious.
12
+ execute("COMMIT PREPARED '#{quote_string(xid)}'")
13
+ rescue Exception => e
14
+ raise unless e.message =~ /^PG::UndefinedObject/
15
+ end
16
+
17
+ def rollback_prepared_db_transaction(xid, options={})
18
+ execute("ROLLBACK PREPARED '#{quote_string(xid)}'")
19
+ rescue Exception => e
20
+ raise unless e.message =~ /^PG::UndefinedObject/
21
+ end
22
+
23
+ included do
24
+ # We want to make sure that we never block the database by having
25
+ # uncommitted transactions.
26
+ Promiscuous::Publisher::Operation::Base.register_recovery_mechanism do
27
+ connection = ActiveRecord::Base.connection
28
+ db_name = connection.current_database
29
+
30
+ # We wait twice the time of expiration, to allow a better recovery scenario.
31
+ expire_duration = 2 * Promiscuous::Publisher::Operation::Base.lock_options[:expire]
32
+
33
+ q = "SELECT gid FROM pg_prepared_xacts " +
34
+ "WHERE database = '#{db_name}' " +
35
+ "AND prepared < current_timestamp + #{expire_duration} * interval '1 second'"
36
+
37
+ connection.exec_query(q, "Promiscuous Recovery").each do |tx|
38
+ ActiveRecord::Base::PromiscuousTransaction.recover_transaction(connection, tx['gid'])
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ class << self
45
+ alias_method :connection_without_promiscuous, :connection
46
+
47
+ def connection
48
+ connection_without_promiscuous.tap do |connection|
49
+ unless defined?(connection.promiscuous_hook)
50
+ connection.class.class_eval do
51
+ attr_accessor :current_transaction_id
52
+
53
+ if self.name == "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
54
+ include ActiveRecord::Base::PostgresSQL2PCExtensions
55
+ end
56
+
57
+ def promiscuous_hook; end
58
+
59
+ alias_method :begin_db_transaction_without_promiscuous, :begin_db_transaction
60
+ alias_method :create_savepoint_without_promiscuous, :create_savepoint
61
+ alias_method :rollback_db_transaction_without_promiscuous, :rollback_db_transaction
62
+ alias_method :rollback_to_savepoint_without_promiscuous, :rollback_to_savepoint
63
+ alias_method :commit_db_transaction_without_promiscuous, :commit_db_transaction
64
+ alias_method :release_savepoint_without_promiscuous, :release_savepoint
65
+
66
+ def with_promiscuous_transaction_context(&block)
67
+ ctx = Promiscuous::Publisher::Context.current
68
+ block.call(ctx.transaction_context_of(:active_record)) if ctx
69
+ end
70
+
71
+ def begin_db_transaction
72
+ @current_transaction_id = SecureRandom.uuid
73
+ begin_db_transaction_without_promiscuous
74
+ with_promiscuous_transaction_context { |tx| tx.start }
75
+ end
76
+
77
+ def create_savepoint
78
+ create_savepoint_without_promiscuous
79
+ with_promiscuous_transaction_context { |tx| tx.start }
80
+ end
81
+
82
+ def rollback_db_transaction
83
+ with_promiscuous_transaction_context { |tx| tx.rollback }
84
+ rollback_db_transaction_without_promiscuous
85
+ @current_transaction_id = nil
86
+ end
87
+
88
+ def rollback_to_savepoint
89
+ with_promiscuous_transaction_context { |tx| tx.rollback }
90
+ rollback_to_savepoint_without_promiscuous
91
+ end
92
+
93
+ def commit_db_transaction
94
+ ops = with_promiscuous_transaction_context { |tx| tx.write_operations_to_commit }
95
+ PromiscuousTransaction.new(:connection => self,
96
+ :transaction_id => self.current_transaction_id,
97
+ :transaction_operations => ops).execute do
98
+ commit_db_transaction_without_promiscuous
99
+ end
100
+ with_promiscuous_transaction_context { |tx| tx.commit }
101
+ @current_transaction_id = nil
102
+ end
103
+
104
+ def release_savepoint
105
+ release_savepoint_without_promiscuous
106
+ with_promiscuous_transaction_context { |tx| tx.commit }
107
+ end
108
+
109
+ alias_method :select_all_without_promiscuous, :select_all
110
+ alias_method :select_values_without_promiscuous, :select_values
111
+ alias_method :insert_without_promiscuous, :insert
112
+ alias_method :update_without_promiscuous, :update
113
+ alias_method :delete_without_promiscuous, :delete
114
+
115
+ def select_all(arel, name = nil, binds = [])
116
+ PromiscuousSelectOperation.new(arel, name, binds, :connection => self).execute do
117
+ select_all_without_promiscuous(arel, name, binds)
118
+ end
119
+ end
120
+
121
+ def select_values(arel, name = nil)
122
+ PromiscuousSelectOperation.new(arel, name, [], :connection => self).execute do
123
+ select_values_without_promiscuous(arel, name)
124
+ end
125
+ end
126
+
127
+ def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
128
+ PromiscuousInsertOperation.new(arel, name, pk, id_value, sequence_name, binds, :connection => self).execute do
129
+ insert_without_promiscuous(arel, name, pk, id_value, sequence_name, binds)
130
+ end
131
+ end
132
+
133
+ def update(arel, name = nil, binds = [])
134
+ PromiscuousUpdateOperation.new(arel, name, binds, :connection => self).execute do
135
+ update_without_promiscuous(arel, name, binds)
136
+ end
137
+ end
138
+
139
+ def delete(arel, name = nil, binds = [])
140
+ PromiscuousDeleteOperation.new(arel, name, binds, :connection => self).execute do
141
+ delete_without_promiscuous(arel, name, binds)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ class PromiscousOperation < Promiscuous::Publisher::Operation::NonPersistent
151
+ def initialize(arel, name, binds, options={})
152
+ super(options)
153
+ @arel = arel
154
+ @operation_name = name
155
+ @binds = binds
156
+ @connection = options[:connection]
157
+ end
158
+
159
+ def transaction_context
160
+ current_context.transaction_context_of(:active_record)
161
+ end
162
+
163
+ def ensure_transaction!
164
+ if current_context && write? && !transaction_context.in_transaction?
165
+ raise "You need to write to the database within an ActiveRecord transaction"
166
+ end
167
+ end
168
+
169
+ def model
170
+ @model ||= @arel.ast.relation.engine
171
+ @model = nil unless @model < Promiscuous::Publisher::Model::ActiveRecord
172
+ @model
173
+ end
174
+
175
+ def execute(&db_operation)
176
+ return db_operation.call unless model
177
+ ensure_transaction!
178
+
179
+ super do |query|
180
+ query.non_instrumented { db_operation.call }
181
+ query.instrumented do
182
+ db_operation_and_select.tap do
183
+ transaction_context.add_write_operation(self) if write? && !@instances.empty?
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ def db_operation_and_select
190
+ raise
191
+ end
192
+
193
+ def operation_payloads
194
+ @instances.map do |instance|
195
+ instance.promiscuous.payload(:with_attributes => self.operation.in?([:create, :update])).tap do |payload|
196
+ payload[:operation] = self.operation
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ class PromiscuousInsertOperation < PromiscousOperation
203
+ def initialize(arel, name, pk, id_value, sequence_name, binds, options={})
204
+ super(arel, name, binds, options)
205
+ @pk = pk
206
+ @id_value = id_value
207
+ @sequence_name = sequence_name
208
+ @operation = :create
209
+ raise unless @arel.is_a?(Arel::InsertManager)
210
+ end
211
+
212
+ def db_operation_and_select
213
+ # XXX This is only supported by Postgres and should be in the postgres driver
214
+
215
+ @connection.exec_insert("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
216
+ @instances = result.map { |row| model.instantiate(row) }
217
+ end
218
+ # TODO Use correct primary key
219
+ @instances.first.id
220
+ end
221
+ end
222
+
223
+ class PromiscuousUpdateOperation < PromiscousOperation
224
+ def initialize(arel, name, binds, options={})
225
+ super
226
+ @operation = :update
227
+ raise unless @arel.is_a?(Arel::UpdateManager)
228
+ end
229
+
230
+ def updated_fields_in_query
231
+ Hash[@arel.ast.values.map do |v|
232
+ case v
233
+ when Arel::Nodes::Assignment
234
+ [v.left.name.to_sym, v.right]
235
+ when Arel::Nodes::SqlLiteral
236
+ # Not parsing SQL, no thanks. It's an optimization anyway
237
+ return nil
238
+ else
239
+ return nil
240
+ end
241
+ end]
242
+ end
243
+
244
+ def any_published_field_changed?
245
+ updates = updated_fields_in_query
246
+ return true if updates.nil? # Couldn't parse query
247
+ (updated_fields_in_query.keys & model.published_db_fields).present?
248
+ end
249
+
250
+ def db_operation_and_select
251
+ # TODO this should be in the postgres driver (to also leverage the cache)
252
+ @connection.exec_query("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
253
+ @instances = result.map { |row| model.instantiate(row) }
254
+ end.rows.size
255
+ end
256
+
257
+ def execute(&db_operation)
258
+ return db_operation.call unless model
259
+ return db_operation.call unless any_published_field_changed?
260
+ super
261
+ end
262
+ end
263
+
264
+ class PromiscuousDeleteOperation < PromiscousOperation
265
+ def initialize(arel, name, binds, options={})
266
+ super
267
+ @operation = :destroy
268
+ raise unless @arel.is_a?(Arel::DeleteManager)
269
+ end
270
+
271
+ def db_operation_and_select
272
+ # TODO We only need the tracked attributes really (most likely, we just need ID)
273
+ # XXX This is only supported by Postgres.
274
+ @connection.exec_query("#{@connection.to_sql(@arel, @binds)} RETURNING *", @operation_name, @binds).tap do |result|
275
+ @instances = result.map { |row| model.instantiate(row) }
276
+ end.rows.size
277
+ end
278
+ end
279
+
280
+ class PromiscuousSelectOperation < PromiscousOperation
281
+ def initialize(arel, name, binds, options={})
282
+ super
283
+ @operation = :read
284
+ @result = []
285
+ end
286
+
287
+ def model
288
+ @model ||= begin
289
+ case @arel
290
+ when Arel::SelectManager
291
+ raise "SQL statement too complicated (joins?)" if @arel.ast.cores.size != 1
292
+ model = @arel.ast.cores.first.source.left.engine
293
+ when ActiveRecord::Relation
294
+ return nil # TODO
295
+ else
296
+ raise "What is this query?" unless @arel.is_a?(Arel::SelectManager)
297
+ end
298
+
299
+ model = nil unless model < Promiscuous::Publisher::Model::ActiveRecord
300
+ model
301
+ end
302
+ rescue
303
+ # TODO Track dependencies of complex queries properly...
304
+ nil
305
+ end
306
+
307
+ def get_selector_instance
308
+ attrs = @arel.ast.cores.first.wheres.map { |w| [w.children.first.left.name, w.children.first.right] }
309
+ model.instantiate(Hash[attrs])
310
+ end
311
+
312
+ def query_dependencies
313
+ deps = dependencies_for(get_selector_instance)
314
+ deps.empty? ? super : deps
315
+ end
316
+
317
+ def execute(&db_operation)
318
+ # We dup because ActiveRecord modifies our return value
319
+ super.tap { @result = @result.dup }
320
+ end
321
+
322
+ def db_operation_and_select
323
+ # XXX This is only supported by Postgres.
324
+ @connection.exec_query("#{@connection.to_sql(@arel, @binds)}", @operation_name, @binds).to_a.tap do |result|
325
+ @instances = result.map { |row| model.instantiate(row) }
326
+ end
327
+ end
328
+ end
329
+
330
+ class PromiscuousTransaction < Promiscuous::Publisher::Operation::Transaction
331
+ attr_accessor :connection
332
+
333
+ def initialize(options={})
334
+ super
335
+ # When we do a recovery, we use the default connection.
336
+ @connection = options[:connection] || ActiveRecord::Base.connection
337
+ end
338
+
339
+ def execute_instrumented(query)
340
+ query.prepare { @connection.prepare_db_transaction }
341
+ query.instrumented { @connection.commit_prepared_db_transaction(@transaction_id) }
342
+ super
343
+ end
344
+
345
+ def self.recover_transaction(connection, transaction_id)
346
+ op = new(:connection => connection, :transaction_id => transaction_id)
347
+ # Getting the lock will trigger the real recovery mechanism
348
+ if op.acquire_op_lock
349
+ op.release_op_lock
350
+ end
351
+
352
+ # In the event where the recovery payload wasn't found, we must roll back.
353
+ # If the operation was recoverable, but couldn't be recovered, an
354
+ # exception would be thrown, so we won't roll it back by mistake.
355
+ # If the operation was recovered, the roll back will result in an error,
356
+ # which is fine.
357
+ connection.rollback_prepared_db_transaction(transaction_id)
358
+ end
359
+ end
360
+ end