promiscuous 0.90.0 → 0.91.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -4,7 +4,7 @@ raise "moped > 1.3.2 please" unless Gem.loaded_specs['moped'].version >= Ge
|
|
4
4
|
require 'yaml'
|
5
5
|
|
6
6
|
class Moped::PromiscuousCollectionWrapper < Moped::Collection
|
7
|
-
class PromiscuousCollectionOperation < Promiscuous::Publisher::Operation::
|
7
|
+
class PromiscuousCollectionOperation < Promiscuous::Publisher::Operation::Atomic
|
8
8
|
def initialize(options={})
|
9
9
|
super
|
10
10
|
@operation = :create
|
@@ -21,13 +21,13 @@ class Moped::PromiscuousCollectionWrapper < Moped::Collection
|
|
21
21
|
rescue NameError
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
#
|
26
|
-
|
27
|
-
@document.to_yaml
|
24
|
+
def recovery_payload
|
25
|
+
# We use yaml because we need the BSON types.
|
26
|
+
[@instance.class.promiscuous_collection_name, @instance.id, @document.to_yaml]
|
28
27
|
end
|
29
28
|
|
30
|
-
def self.recover_operation(
|
29
|
+
def self.recover_operation(collection, instance_id, document)
|
30
|
+
model = Promiscuous::Publisher::Model::Mongoid.collection_mapping[collection]
|
31
31
|
document = YAML.load(document)
|
32
32
|
instance = Mongoid::Factory.from_db(model, document)
|
33
33
|
new(:collection => model.collection, :document => document, :instance => instance)
|
@@ -40,18 +40,17 @@ class Moped::PromiscuousCollectionWrapper < Moped::Collection
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
def
|
44
|
-
@document[VERSION_FIELD] = @instance_version
|
45
|
-
end
|
46
|
-
|
47
|
-
def execute_persistent(&db_operation)
|
43
|
+
def execute_instrumented(query)
|
48
44
|
@instance = Mongoid::Factory.from_db(model, @document)
|
49
45
|
super
|
50
46
|
end
|
51
47
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
48
|
+
def should_instrument_query?
|
49
|
+
super && model
|
50
|
+
end
|
51
|
+
|
52
|
+
def recoverable_failure?(exception)
|
53
|
+
exception.is_a?(Moped::Errors::ConnectionFailure)
|
55
54
|
end
|
56
55
|
end
|
57
56
|
|
@@ -74,8 +73,39 @@ class Moped::PromiscuousCollectionWrapper < Moped::Collection
|
|
74
73
|
end
|
75
74
|
|
76
75
|
class Moped::PromiscuousQueryWrapper < Moped::Query
|
77
|
-
|
78
|
-
|
76
|
+
module PromiscuousHelpers
|
77
|
+
def collection_name
|
78
|
+
@collection_name ||= @query.collection.is_a?(String) ? @query.collection : @query.collection.name
|
79
|
+
end
|
80
|
+
|
81
|
+
def model
|
82
|
+
@model ||= Promiscuous::Publisher::Model::Mongoid.collection_mapping[collection_name]
|
83
|
+
end
|
84
|
+
|
85
|
+
def get_selector_instance
|
86
|
+
selector = @query.operation.selector["$query"] || @query.operation.selector
|
87
|
+
|
88
|
+
# TODO use the original instance for an update/delete, that would be
|
89
|
+
# an even better hint.
|
90
|
+
|
91
|
+
# We only support == selectors, no $in, or $gt.
|
92
|
+
@selector = selector.select { |k,v| k.to_s =~ /^[^$]/ && !v.is_a?(Hash) }
|
93
|
+
|
94
|
+
# @instance is not really a proper instance of a model, it's just a
|
95
|
+
# convenient representation of a selector as explain in base.rb,
|
96
|
+
# which explain why we don't want any constructor to be called.
|
97
|
+
# Note that this optimistic mechanism also works with writes because
|
98
|
+
# the instance gets reloaded once the lock is taken. If the
|
99
|
+
# dependencies were incorrect, the locks will be released and
|
100
|
+
# reacquired appropriately.
|
101
|
+
model.allocate.tap { |doc| doc.instance_variable_set(:@attributes, @selector) }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class PromiscuousWriteOperation < Promiscuous::Publisher::Operation::Atomic
|
106
|
+
include Moped::PromiscuousQueryWrapper::PromiscuousHelpers
|
107
|
+
|
108
|
+
attr_accessor :change
|
79
109
|
|
80
110
|
def initialize(options={})
|
81
111
|
super
|
@@ -83,87 +113,60 @@ class Moped::PromiscuousQueryWrapper < Moped::Query
|
|
83
113
|
@change = options[:change]
|
84
114
|
end
|
85
115
|
|
86
|
-
def
|
87
|
-
@
|
116
|
+
def recovery_payload
|
117
|
+
[@instance.class.promiscuous_collection_name, @instance.id]
|
88
118
|
end
|
89
119
|
|
90
|
-
def
|
91
|
-
|
120
|
+
def self.recover_operation(collection, instance_id)
|
121
|
+
# TODO We need to use the primary database. We cannot read from a secondary.
|
122
|
+
model = Promiscuous::Publisher::Model::Mongoid.collection_mapping[collection]
|
123
|
+
query = model.unscoped.where(:id => instance_id).query
|
124
|
+
|
125
|
+
# We no-op the update operation instead of making it idempotent.
|
126
|
+
# To do so, we do a dummy update on the document.
|
127
|
+
# The original caller will fail because the lock was unlocked, so we'll
|
128
|
+
# won't send a different message.
|
129
|
+
new(:query => query, :change => {}).tap { |op| op.instance_eval { reload_instance } }
|
92
130
|
end
|
93
131
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
# TODO refactor this not so pretty instance_eval
|
100
|
-
op.instance_eval do
|
101
|
-
reload_instance
|
102
|
-
@instance ||= get_selector_instance
|
132
|
+
def recover_db_operation
|
133
|
+
if operation == :update
|
134
|
+
without_promiscuous { @query.update(@change) }
|
135
|
+
else
|
136
|
+
without_promiscuous { @query.remove }
|
103
137
|
end
|
104
|
-
op
|
105
138
|
end
|
106
139
|
|
107
|
-
def
|
108
|
-
|
109
|
-
# The original caller will fail because the lock was unlocked.
|
110
|
-
without_promiscuous { @query.update(@change) }
|
111
|
-
@operation = :dummy
|
140
|
+
def recoverable_failure?(exception)
|
141
|
+
exception.is_a?(Moped::Errors::ConnectionFailure)
|
112
142
|
end
|
113
143
|
|
114
144
|
def fetch_instance
|
115
|
-
|
116
|
-
Mongoid::Factory.from_db(model,
|
145
|
+
raw_instance = without_promiscuous { @query.first }
|
146
|
+
Mongoid::Factory.from_db(model, raw_instance) if raw_instance
|
117
147
|
end
|
118
148
|
|
119
149
|
def use_id_selector(options={})
|
120
|
-
selector = {'_id' => @instance.id}
|
150
|
+
selector = {'_id' => @instance.id}.merge(@query.selector.select { |k,v| k.to_s.include?("_id") })
|
121
151
|
|
122
152
|
if options[:use_atomic_version_selector]
|
123
|
-
version = @instance[
|
124
|
-
selector.merge!(
|
153
|
+
version = @instance[Promiscuous::Config.version_field]
|
154
|
+
selector.merge!(Promiscuous::Config.version_field => version)
|
125
155
|
end
|
126
156
|
|
127
157
|
@query.selector = selector
|
128
158
|
end
|
129
159
|
|
130
|
-
def
|
160
|
+
def stash_version_in_document(version)
|
131
161
|
@change['$set'] ||= {}
|
132
|
-
@change['$set'][
|
162
|
+
@change['$set'][Promiscuous::Config.version_field] = version
|
133
163
|
end
|
134
164
|
|
135
|
-
def
|
136
|
-
selector = @query.operation.selector["$query"] || @query.operation.selector
|
137
|
-
|
138
|
-
# TODO use the original instance for an update/delete, that would be
|
139
|
-
# an even better hint.
|
140
|
-
|
141
|
-
# We only support == selectors, no $in, or $gt.
|
142
|
-
@selector = selector.select { |k,v| k.to_s =~ /^[^$]/ && !v.is_a?(Hash) }
|
143
|
-
|
144
|
-
# @instance is not really a proper instance of a model, it's just a
|
145
|
-
# convenient representation of a selector as explain in base.rb,
|
146
|
-
# which explain why we don't want any constructor to be called.
|
147
|
-
# Note that this optimistic mechanism also works with writes because
|
148
|
-
# the instance gets reloaded once the lock is taken. If the
|
149
|
-
# dependencies were incorrect, the locks will be released and
|
150
|
-
# reacquired appropriately.
|
151
|
-
model.allocate.tap { |doc| doc.instance_variable_set(:@attributes, @selector) }
|
152
|
-
end
|
153
|
-
|
154
|
-
def execute_persistent(&db_operation)
|
165
|
+
def execute_instrumented(query)
|
155
166
|
# We are trying to be optimistic for the locking. We are trying to figure
|
156
167
|
# out our dependencies with the selector upfront to avoid an extra read
|
157
168
|
# from reload_instance.
|
158
|
-
@instance
|
159
|
-
super
|
160
|
-
end
|
161
|
-
|
162
|
-
def execute_non_persistent(&db_operation)
|
163
|
-
if multi?
|
164
|
-
@instance = get_selector_instance
|
165
|
-
@selector_keys = @selector.keys
|
166
|
-
end
|
169
|
+
@instance ||= get_selector_instance
|
167
170
|
super
|
168
171
|
end
|
169
172
|
|
@@ -185,48 +188,55 @@ class Moped::PromiscuousQueryWrapper < Moped::Query
|
|
185
188
|
# TODO maybe we should cache these things
|
186
189
|
# TODO discover field dependencies automatically (hard)
|
187
190
|
aliases = Hash[model.aliased_fields.map { |k,v| [v,k] }]
|
188
|
-
attributes = fields_in_query(@change).map { |f|
|
191
|
+
attributes = fields_in_query(@change).map { |f| [aliases[f.to_s], f] }.flatten.compact.map(&:to_sym)
|
189
192
|
(attributes & model.published_db_fields).present?
|
190
193
|
end
|
191
194
|
|
192
|
-
def
|
193
|
-
|
194
|
-
|
195
|
-
|
195
|
+
def should_instrument_query?
|
196
|
+
super && model && any_published_field_changed?
|
197
|
+
end
|
198
|
+
end
|
196
199
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
200
|
+
class PromiscuousReadOperation < Promiscuous::Publisher::Operation::NonPersistent
|
201
|
+
include Moped::PromiscuousQueryWrapper::PromiscuousHelpers
|
202
|
+
|
203
|
+
def initialize(options={})
|
201
204
|
super
|
205
|
+
@operation = :read
|
206
|
+
@query = options[:query]
|
207
|
+
end
|
208
|
+
|
209
|
+
def query_dependencies
|
210
|
+
deps = dependencies_for(get_selector_instance)
|
211
|
+
deps.empty? ? super : deps
|
202
212
|
end
|
203
|
-
end
|
204
213
|
|
205
|
-
|
206
|
-
|
214
|
+
def should_instrument_query?
|
215
|
+
super && model
|
216
|
+
end
|
207
217
|
end
|
208
218
|
|
209
|
-
def
|
210
|
-
|
211
|
-
@operation.selector = value
|
219
|
+
def promiscuous_read_operation(options={})
|
220
|
+
PromiscuousReadOperation.new(options.merge(:query => self))
|
212
221
|
end
|
213
222
|
|
214
|
-
def
|
215
|
-
|
223
|
+
def promiscuous_write_operation(operation, options={})
|
224
|
+
PromiscuousWriteOperation.new(options.merge(:query => self, :operation => operation))
|
216
225
|
end
|
217
226
|
|
218
|
-
def
|
219
|
-
|
227
|
+
def selector=(value)
|
228
|
+
@selector = value
|
229
|
+
@operation.selector = value
|
220
230
|
end
|
221
231
|
|
222
232
|
# Moped::Query
|
223
233
|
|
224
234
|
def count(*args)
|
225
|
-
|
235
|
+
promiscuous_read_operation(:operation_ext => :count).execute { super }.to_i
|
226
236
|
end
|
227
237
|
|
228
238
|
def distinct(key)
|
229
|
-
|
239
|
+
promiscuous_read_operation(:operation_ext => :distinct).execute { super }
|
230
240
|
end
|
231
241
|
|
232
242
|
def each
|
@@ -240,68 +250,82 @@ class Moped::PromiscuousQueryWrapper < Moped::Query
|
|
240
250
|
alias :cursor :each
|
241
251
|
|
242
252
|
def first
|
243
|
-
#
|
244
|
-
# sure that we add the id, otherwise we
|
245
|
-
|
246
|
-
|
247
|
-
|
253
|
+
# FIXME If the the user is using something like .only(), we need to make
|
254
|
+
# sure that we add the id, otherwise we are screwed.
|
255
|
+
op = promiscuous_read_operation
|
256
|
+
|
257
|
+
op.execute do |query|
|
258
|
+
query.non_instrumented { super }
|
259
|
+
query.instrumented do
|
260
|
+
super.tap do |doc|
|
261
|
+
op.instances = doc ? [Mongoid::Factory.from_db(op.model, doc)] : []
|
262
|
+
end
|
263
|
+
end
|
248
264
|
end
|
249
265
|
end
|
250
266
|
alias :one :first
|
251
267
|
|
252
268
|
def update(change, flags=nil)
|
253
|
-
|
254
|
-
|
269
|
+
update_op = promiscuous_write_operation(:update, :change => change)
|
270
|
+
|
271
|
+
if flags && update_op.should_instrument_query?
|
272
|
+
raise "You cannot do a multi update. Instead, update each document separately." if flags.include?(:multi)
|
273
|
+
raise "No upsert support yet" if flags.include?(:upsert)
|
274
|
+
end
|
255
275
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
276
|
+
update_op.execute do |query|
|
277
|
+
query.non_instrumented { super }
|
278
|
+
query.instrumented do |op|
|
279
|
+
raw_instance = without_promiscuous { modify(change, :new => true) }
|
280
|
+
op.instance = Mongoid::Factory.from_db(op.model, raw_instance)
|
260
281
|
{'updatedExisting' => true, 'n' => 1, 'err' => nil, 'ok' => 1.0}
|
261
|
-
else
|
262
|
-
super
|
263
282
|
end
|
264
283
|
end
|
265
284
|
end
|
266
285
|
|
267
286
|
def modify(change, options={})
|
268
|
-
|
269
|
-
|
287
|
+
promiscuous_write_operation(:update, :change => change).execute do |query|
|
288
|
+
query.non_instrumented { super }
|
289
|
+
query.instrumented do |op|
|
290
|
+
raise "You can only use find_and_modify() with :new => true" if !options[:new]
|
291
|
+
super.tap { |raw_instance| op.instance = Mongoid::Factory.from_db(op.model, raw_instance) }
|
292
|
+
end
|
293
|
+
end
|
270
294
|
end
|
271
295
|
|
272
296
|
def remove
|
273
|
-
|
274
|
-
# FIXME raise when recovery raced
|
297
|
+
promiscuous_write_operation(:destroy).execute { super }
|
275
298
|
end
|
276
299
|
|
277
300
|
def remove_all
|
278
|
-
|
301
|
+
raise "Instead of doing a multi delete, delete each document separatly.\n" +
|
302
|
+
"Declare your has_many relationships with :dependent => :destroy instead of :delete"
|
279
303
|
end
|
280
304
|
end
|
281
305
|
|
282
306
|
class Moped::PromiscuousCursorWrapper < Moped::Cursor
|
283
|
-
def promiscuous_operation(op, options={})
|
284
|
-
Moped::PromiscuousQueryWrapper::PromiscuousQueryOperation.new(
|
285
|
-
options.merge(:query => @query, :operation => op))
|
286
|
-
end
|
287
|
-
|
288
307
|
# Moped::Cursor
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
308
|
+
def promiscuous_read_each(&block)
|
309
|
+
op = Moped::PromiscuousQueryWrapper::PromiscuousReadOperation.new(
|
310
|
+
:query => @query, :operation_ext => :each)
|
311
|
+
|
312
|
+
op.execute do |query|
|
313
|
+
query.non_instrumented { block.call.to_a }
|
314
|
+
query.instrumented do
|
315
|
+
block.call.to_a.tap do |docs|
|
316
|
+
op.instances = docs.map { |doc| Mongoid::Factory.from_db(op.model, doc) }
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
293
320
|
end
|
294
321
|
|
295
322
|
def load_docs
|
296
|
-
|
297
|
-
promiscuous_operation(:read, :multi => !should_fake_single_read).execute do |operation|
|
298
|
-
operation && should_fake_single_read ? fake_single_read(operation) : super
|
299
|
-
end.to_a
|
323
|
+
promiscuous_read_each { super }
|
300
324
|
end
|
301
325
|
|
302
326
|
def get_more
|
303
327
|
# TODO support batch_size
|
304
|
-
|
328
|
+
promiscuous_read_each { super }
|
305
329
|
end
|
306
330
|
|
307
331
|
def initialize(session, query_operation)
|
@@ -313,9 +337,8 @@ end
|
|
313
337
|
class Moped::PromiscuousDatabase < Moped::Database
|
314
338
|
# TODO it might be safer to use the alias attribute method because promiscuous
|
315
339
|
# may come late in the loading.
|
316
|
-
def
|
317
|
-
Moped::PromiscuousQueryWrapper::
|
318
|
-
options.merge(:operation => op))
|
340
|
+
def promiscuous_read_operation(options={})
|
341
|
+
Moped::PromiscuousQueryWrapper::PromiscuousReadOperation.new(options)
|
319
342
|
end
|
320
343
|
|
321
344
|
# Moped::Database
|
@@ -323,29 +346,13 @@ class Moped::PromiscuousDatabase < Moped::Database
|
|
323
346
|
def command(command)
|
324
347
|
if command[:mapreduce]
|
325
348
|
query = Moped::Query.new(self[command[:mapreduce]], command[:query])
|
326
|
-
|
327
|
-
:operation_ext => :mapreduce, :multi => true).execute { super }
|
349
|
+
promiscuous_read_operation(:query => query, :operation_ext => :mapreduce).execute { super }
|
328
350
|
else
|
329
351
|
super
|
330
352
|
end
|
331
353
|
end
|
332
354
|
end
|
333
355
|
|
334
|
-
class Mongoid::Contextual::Mongo
|
335
|
-
alias_method :each_hijacked, :each
|
336
|
-
|
337
|
-
def each(&block)
|
338
|
-
query.without_promiscuous! if criteria.options[:without_promiscuous]
|
339
|
-
each_hijacked(&block)
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
module Origin::Optional
|
344
|
-
def without_promiscuous
|
345
|
-
clone.tap { |criteria| criteria.options.store(:without_promiscuous, true) }
|
346
|
-
end
|
347
|
-
end
|
348
|
-
|
349
356
|
class Mongoid::Validations::UniquenessValidator
|
350
357
|
alias_method :validate_root_without_promisucous, :validate_root
|
351
358
|
def validate_root(*args)
|
@@ -354,7 +361,8 @@ class Mongoid::Validations::UniquenessValidator
|
|
354
361
|
end
|
355
362
|
|
356
363
|
class Moped::BSON::ObjectId
|
357
|
-
# No {"$oid": "123"}, it's horrible
|
364
|
+
# No {"$oid": "123"}, it's horrible.
|
365
|
+
# TODO Document this shit.
|
358
366
|
def to_json(*args)
|
359
367
|
"\"#{to_s}\""
|
360
368
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Promiscuous::Publisher::Operation::NonPersistent < Promiscuous::Publisher::Operation::Base
|
2
|
+
# XXX As opposed to atomic operations, NonPersistent operations deals with an
|
3
|
+
# array of instances
|
4
|
+
attr_accessor :instances
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
super
|
8
|
+
@instances = options[:instances].to_a
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute_instrumented(db_operation)
|
12
|
+
db_operation.call_and_remember_result(:instrumented)
|
13
|
+
|
14
|
+
unless db_operation.failed?
|
15
|
+
current_context.read_operations << self if read?
|
16
|
+
trace_operation
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def operation_payloads
|
21
|
+
return [] if self.failed?
|
22
|
+
@instances.map { |instance| payloads_for(instance) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def query_dependencies
|
26
|
+
@instances.map { |instance| dependencies_for(instance) }
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Promiscuous::Publisher::Operation::ProxyForQuery
|
2
|
+
attr_accessor :exception, :result
|
3
|
+
|
4
|
+
def initialize(operation, &block)
|
5
|
+
@operation = operation
|
6
|
+
@queries = {}
|
7
|
+
|
8
|
+
if block.arity == 1
|
9
|
+
block.call(self)
|
10
|
+
else
|
11
|
+
self.non_instrumented { block.call }
|
12
|
+
self.instrumented { block.call }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare(&block)
|
17
|
+
@queries[:prepare] = block
|
18
|
+
end
|
19
|
+
|
20
|
+
def non_instrumented(&block)
|
21
|
+
@queries[:non_instrumented] = block
|
22
|
+
end
|
23
|
+
|
24
|
+
def instrumented(&block)
|
25
|
+
@queries[:instrumented] = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def call_and_remember_result(which)
|
29
|
+
raise "Fatal: #{which} query unspecified" unless @queries[which]
|
30
|
+
@result = @queries[which].call(@operation)
|
31
|
+
rescue Exception => e
|
32
|
+
@exception = e
|
33
|
+
end
|
34
|
+
|
35
|
+
def failed?
|
36
|
+
!!@exception
|
37
|
+
end
|
38
|
+
|
39
|
+
def result
|
40
|
+
failed? ? (raise @exception) : @result
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Promiscuous::Publisher::Operation::Transaction < Promiscuous::Publisher::Operation::Base
|
2
|
+
attr_accessor :transaction_id, :transaction_operations, :operation_payloads
|
3
|
+
|
4
|
+
def initialize(options={})
|
5
|
+
super
|
6
|
+
@operation = :commit
|
7
|
+
@transaction_operations = options[:transaction_operations].to_a
|
8
|
+
@transaction_id = options[:transaction_id]
|
9
|
+
@operation_payloads = options[:operation_payloads]
|
10
|
+
end
|
11
|
+
|
12
|
+
def dependency_for_op_lock
|
13
|
+
# We don't take locks on rows as the database already have the locks on them
|
14
|
+
# until the transaction is committed.
|
15
|
+
# A lock on the transaction ID is taken so we know when we conflict with
|
16
|
+
# the recovery mechanism.
|
17
|
+
Promiscuous::Dependency.new("__transactions__", self.transaction_id, :dont_hash => true)
|
18
|
+
end
|
19
|
+
|
20
|
+
def pending_writes
|
21
|
+
# TODO (performance) Return a list of writes that:
|
22
|
+
# - Never touch the same id (the latest write is sufficient)
|
23
|
+
# - create/update and then delete should be invisible
|
24
|
+
@transaction_operations
|
25
|
+
end
|
26
|
+
|
27
|
+
def query_dependencies
|
28
|
+
@query_dependencies ||= pending_writes.map(&:query_dependencies).flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
def operation_payloads
|
32
|
+
@operation_payloads ||= pending_writes.map(&:operation_payloads).flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
alias cache_operation_payloads operation_payloads
|
36
|
+
|
37
|
+
def should_instrument_query?
|
38
|
+
super && !pending_writes.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute_instrumented(query)
|
42
|
+
unless self.recovering?
|
43
|
+
generate_read_dependencies
|
44
|
+
acquire_op_lock
|
45
|
+
|
46
|
+
# As opposed to atomic operations, we know the values of the instances
|
47
|
+
# before the database operation, and not after, so only one stage
|
48
|
+
# of recovery is used.
|
49
|
+
cache_operation_payloads
|
50
|
+
|
51
|
+
query.call_and_remember_result(:prepare)
|
52
|
+
end
|
53
|
+
|
54
|
+
self.increment_read_and_write_dependencies
|
55
|
+
|
56
|
+
query.call_and_remember_result(:instrumented)
|
57
|
+
|
58
|
+
# We can't do anything if the prepared commit doesn't go through.
|
59
|
+
# Either it's a network failure, or the database is having some real
|
60
|
+
# difficulties. The recovery mechanism will have to retry the transaction.
|
61
|
+
return if query.failed?
|
62
|
+
|
63
|
+
# We take a timestamp right after the write is performed because latency
|
64
|
+
# measurements are performed on the subscriber.
|
65
|
+
record_timestamp
|
66
|
+
|
67
|
+
ensure_op_still_locked
|
68
|
+
|
69
|
+
generate_payload
|
70
|
+
clear_previous_dependencies
|
71
|
+
|
72
|
+
publish_payload_in_redis
|
73
|
+
release_op_lock
|
74
|
+
publish_payload_in_rabbitmq_async
|
75
|
+
end
|
76
|
+
|
77
|
+
def recovery_payload
|
78
|
+
# TODO just save the table/ids, or publish the real payload directly.
|
79
|
+
[@transaction_id, @operation_payloads]
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.recover_operation(transaction_id, operation_payloads)
|
83
|
+
new(:transaction_id => transaction_id, :operation_payloads => operation_payloads)
|
84
|
+
end
|
85
|
+
end
|
@@ -1,11 +1,10 @@
|
|
1
1
|
class Promiscuous::Publisher::Worker
|
2
2
|
def initialize
|
3
|
-
@recovery_timer = Promiscuous::Timer.new
|
4
|
-
@timeout = Promiscuous::Config.recovery_timeout
|
3
|
+
@recovery_timer = Promiscuous::Timer.new("recovery", Promiscuous::Config.recovery_timeout) { try_recover }
|
5
4
|
end
|
6
5
|
|
7
6
|
def start
|
8
|
-
@recovery_timer.
|
7
|
+
@recovery_timer.start(:run_immediately => true)
|
9
8
|
end
|
10
9
|
|
11
10
|
def stop
|
@@ -13,10 +12,11 @@ class Promiscuous::Publisher::Worker
|
|
13
12
|
end
|
14
13
|
|
15
14
|
def try_recover
|
16
|
-
Promiscuous::Publisher::Operation::Base.
|
17
|
-
Promiscuous::Publisher::Operation::Base.recover_payloads_for_rabbitmq
|
15
|
+
Promiscuous::Publisher::Operation::Base.run_recovery_mechanisms
|
18
16
|
rescue Exception => e
|
19
|
-
Promiscuous.warn "[recovery] #{e}
|
20
|
-
Promiscuous::Config.error_notifier.
|
17
|
+
Promiscuous.warn "[recovery] #{e}\n#{e.backtrace.join("\n")}"
|
18
|
+
Promiscuous::Config.error_notifier.call(e)
|
19
|
+
ensure
|
20
|
+
ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
|
21
21
|
end
|
22
22
|
end
|