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.
- checksums.yaml +7 -0
- data/lib/promiscuous/amqp/bunny.rb +63 -36
- data/lib/promiscuous/amqp/fake.rb +3 -1
- data/lib/promiscuous/amqp/hot_bunnies.rb +26 -16
- data/lib/promiscuous/amqp/null.rb +1 -0
- data/lib/promiscuous/amqp.rb +12 -12
- data/lib/promiscuous/cli.rb +70 -29
- data/lib/promiscuous/config.rb +54 -29
- data/lib/promiscuous/convenience.rb +1 -1
- data/lib/promiscuous/dependency.rb +25 -6
- data/lib/promiscuous/error/connection.rb +11 -9
- data/lib/promiscuous/error/dependency.rb +8 -1
- data/lib/promiscuous/loader.rb +4 -2
- data/lib/promiscuous/publisher/bootstrap/connection.rb +25 -0
- data/lib/promiscuous/publisher/bootstrap/data.rb +127 -0
- data/lib/promiscuous/publisher/bootstrap/mode.rb +19 -0
- data/lib/promiscuous/publisher/bootstrap/status.rb +40 -0
- data/lib/promiscuous/publisher/bootstrap/version.rb +46 -0
- data/lib/promiscuous/publisher/bootstrap.rb +27 -0
- data/lib/promiscuous/publisher/context/base.rb +67 -0
- data/lib/promiscuous/{middleware.rb → publisher/context/middleware.rb} +16 -13
- data/lib/promiscuous/publisher/context/transaction.rb +36 -0
- data/lib/promiscuous/publisher/context.rb +4 -88
- data/lib/promiscuous/publisher/mock_generator.rb +9 -9
- data/lib/promiscuous/publisher/model/active_record.rb +7 -7
- data/lib/promiscuous/publisher/model/base.rb +29 -29
- data/lib/promiscuous/publisher/model/ephemeral.rb +5 -3
- data/lib/promiscuous/publisher/model/mock.rb +9 -5
- data/lib/promiscuous/publisher/model/mongoid.rb +5 -22
- data/lib/promiscuous/publisher/operation/active_record.rb +360 -0
- data/lib/promiscuous/publisher/operation/atomic.rb +167 -0
- data/lib/promiscuous/publisher/operation/base.rb +279 -474
- data/lib/promiscuous/publisher/operation/mongoid.rb +153 -145
- data/lib/promiscuous/publisher/operation/non_persistent.rb +28 -0
- data/lib/promiscuous/publisher/operation/proxy_for_query.rb +42 -0
- data/lib/promiscuous/publisher/operation/transaction.rb +85 -0
- data/lib/promiscuous/publisher/operation.rb +1 -1
- data/lib/promiscuous/publisher/worker.rb +7 -7
- data/lib/promiscuous/publisher.rb +1 -1
- data/lib/promiscuous/railtie.rb +20 -5
- data/lib/promiscuous/redis.rb +104 -56
- data/lib/promiscuous/subscriber/message_processor/base.rb +38 -0
- data/lib/promiscuous/subscriber/message_processor/bootstrap.rb +17 -0
- data/lib/promiscuous/subscriber/message_processor/regular.rb +192 -0
- data/lib/promiscuous/subscriber/message_processor.rb +4 -0
- data/lib/promiscuous/subscriber/model/base.rb +20 -15
- data/lib/promiscuous/subscriber/model/mongoid.rb +4 -4
- data/lib/promiscuous/subscriber/model/observer.rb +16 -2
- data/lib/promiscuous/subscriber/operation/base.rb +68 -0
- data/lib/promiscuous/subscriber/operation/bootstrap.rb +54 -0
- data/lib/promiscuous/subscriber/operation/regular.rb +13 -0
- data/lib/promiscuous/subscriber/operation.rb +3 -166
- data/lib/promiscuous/subscriber/worker/message.rb +61 -35
- data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +90 -59
- data/lib/promiscuous/subscriber/worker/pump.rb +17 -5
- data/lib/promiscuous/subscriber/worker/recorder.rb +4 -1
- data/lib/promiscuous/subscriber/worker/runner.rb +49 -9
- data/lib/promiscuous/subscriber/worker/stats.rb +2 -2
- data/lib/promiscuous/subscriber/worker.rb +6 -0
- data/lib/promiscuous/subscriber.rb +1 -1
- data/lib/promiscuous/timer.rb +31 -18
- data/lib/promiscuous/version.rb +1 -1
- data/lib/promiscuous.rb +23 -3
- metadata +104 -89
- 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
|
-
|
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 :
|
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[:
|
29
|
-
msg[:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
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
|
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
|
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[:
|
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
|
-
|
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
|
-
|
15
|
-
|
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
|
-
|
36
|
-
|
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
|
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
|