promiscuous 0.90.0 → 0.91.0

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