promiscuous 0.53.1 → 0.90.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/promiscuous.rb +25 -28
- data/lib/promiscuous/amqp.rb +27 -8
- data/lib/promiscuous/amqp/bunny.rb +131 -16
- data/lib/promiscuous/amqp/fake.rb +52 -0
- data/lib/promiscuous/amqp/hot_bunnies.rb +56 -0
- data/lib/promiscuous/amqp/null.rb +6 -6
- data/lib/promiscuous/cli.rb +108 -24
- data/lib/promiscuous/config.rb +73 -12
- data/lib/promiscuous/convenience.rb +18 -0
- data/lib/promiscuous/dependency.rb +59 -0
- data/lib/promiscuous/dsl.rb +36 -0
- data/lib/promiscuous/error.rb +3 -1
- data/lib/promiscuous/error/already_processed.rb +5 -0
- data/lib/promiscuous/error/base.rb +1 -0
- data/lib/promiscuous/error/connection.rb +7 -5
- data/lib/promiscuous/error/dependency.rb +111 -0
- data/lib/promiscuous/error/lock_unavailable.rb +12 -0
- data/lib/promiscuous/error/lost_lock.rb +12 -0
- data/lib/promiscuous/error/missing_context.rb +29 -0
- data/lib/promiscuous/error/publisher.rb +5 -15
- data/lib/promiscuous/error/recovery.rb +7 -0
- data/lib/promiscuous/error/subscriber.rb +2 -4
- data/lib/promiscuous/key.rb +36 -0
- data/lib/promiscuous/loader.rb +12 -16
- data/lib/promiscuous/middleware.rb +112 -0
- data/lib/promiscuous/publisher.rb +7 -4
- data/lib/promiscuous/publisher/context.rb +92 -0
- data/lib/promiscuous/publisher/mock_generator.rb +72 -0
- data/lib/promiscuous/publisher/model.rb +3 -86
- data/lib/promiscuous/publisher/model/active_record.rb +8 -15
- data/lib/promiscuous/publisher/model/base.rb +136 -0
- data/lib/promiscuous/publisher/model/ephemeral.rb +69 -0
- data/lib/promiscuous/publisher/model/mock.rb +61 -0
- data/lib/promiscuous/publisher/model/mongoid.rb +57 -100
- data/lib/promiscuous/{common/lint.rb → publisher/operation.rb} +1 -1
- data/lib/promiscuous/publisher/operation/base.rb +707 -0
- data/lib/promiscuous/publisher/operation/mongoid.rb +370 -0
- data/lib/promiscuous/publisher/worker.rb +22 -0
- data/lib/promiscuous/railtie.rb +21 -3
- data/lib/promiscuous/redis.rb +132 -40
- data/lib/promiscuous/resque.rb +12 -0
- data/lib/promiscuous/sidekiq.rb +15 -0
- data/lib/promiscuous/subscriber.rb +9 -20
- data/lib/promiscuous/subscriber/model.rb +4 -104
- data/lib/promiscuous/subscriber/model/active_record.rb +10 -0
- data/lib/promiscuous/subscriber/model/base.rb +96 -0
- data/lib/promiscuous/subscriber/model/mongoid.rb +86 -0
- data/lib/promiscuous/subscriber/model/observer.rb +37 -0
- data/lib/promiscuous/subscriber/operation.rb +167 -0
- data/lib/promiscuous/subscriber/payload.rb +34 -0
- data/lib/promiscuous/subscriber/worker.rb +22 -18
- data/lib/promiscuous/subscriber/worker/message.rb +48 -25
- data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +273 -181
- data/lib/promiscuous/subscriber/worker/pump.rb +17 -43
- data/lib/promiscuous/subscriber/worker/recorder.rb +24 -0
- data/lib/promiscuous/subscriber/worker/runner.rb +24 -3
- data/lib/promiscuous/subscriber/worker/stats.rb +62 -0
- data/lib/promiscuous/timer.rb +38 -0
- data/lib/promiscuous/version.rb +1 -1
- metadata +98 -143
- data/README.md +0 -33
- data/lib/promiscuous/amqp/ruby_amqp.rb +0 -140
- data/lib/promiscuous/common.rb +0 -4
- data/lib/promiscuous/common/class_helpers.rb +0 -12
- data/lib/promiscuous/common/lint/base.rb +0 -24
- data/lib/promiscuous/common/options.rb +0 -51
- data/lib/promiscuous/ephemeral.rb +0 -14
- data/lib/promiscuous/error/recover.rb +0 -1
- data/lib/promiscuous/observer.rb +0 -5
- data/lib/promiscuous/publisher/active_record.rb +0 -7
- data/lib/promiscuous/publisher/amqp.rb +0 -18
- data/lib/promiscuous/publisher/attributes.rb +0 -32
- data/lib/promiscuous/publisher/base.rb +0 -23
- data/lib/promiscuous/publisher/class.rb +0 -36
- data/lib/promiscuous/publisher/envelope.rb +0 -7
- data/lib/promiscuous/publisher/ephemeral.rb +0 -9
- data/lib/promiscuous/publisher/lint.rb +0 -35
- data/lib/promiscuous/publisher/lint/amqp.rb +0 -14
- data/lib/promiscuous/publisher/lint/attributes.rb +0 -12
- data/lib/promiscuous/publisher/lint/base.rb +0 -5
- data/lib/promiscuous/publisher/lint/class.rb +0 -15
- data/lib/promiscuous/publisher/lint/polymorphic.rb +0 -22
- data/lib/promiscuous/publisher/mock.rb +0 -79
- data/lib/promiscuous/publisher/mongoid.rb +0 -33
- data/lib/promiscuous/publisher/mongoid/embedded.rb +0 -27
- data/lib/promiscuous/publisher/mongoid/embedded_many.rb +0 -12
- data/lib/promiscuous/publisher/polymorphic.rb +0 -8
- data/lib/promiscuous/subscriber/active_record.rb +0 -11
- data/lib/promiscuous/subscriber/amqp.rb +0 -25
- data/lib/promiscuous/subscriber/attributes.rb +0 -35
- data/lib/promiscuous/subscriber/base.rb +0 -29
- data/lib/promiscuous/subscriber/class.rb +0 -29
- data/lib/promiscuous/subscriber/dummy.rb +0 -19
- data/lib/promiscuous/subscriber/envelope.rb +0 -18
- data/lib/promiscuous/subscriber/lint.rb +0 -30
- data/lib/promiscuous/subscriber/lint/amqp.rb +0 -21
- data/lib/promiscuous/subscriber/lint/attributes.rb +0 -21
- data/lib/promiscuous/subscriber/lint/base.rb +0 -14
- data/lib/promiscuous/subscriber/lint/class.rb +0 -13
- data/lib/promiscuous/subscriber/lint/polymorphic.rb +0 -39
- data/lib/promiscuous/subscriber/mongoid.rb +0 -27
- data/lib/promiscuous/subscriber/mongoid/embedded.rb +0 -17
- data/lib/promiscuous/subscriber/mongoid/embedded_many.rb +0 -44
- data/lib/promiscuous/subscriber/observer.rb +0 -26
- data/lib/promiscuous/subscriber/polymorphic.rb +0 -36
- data/lib/promiscuous/subscriber/upsert.rb +0 -12
@@ -0,0 +1,707 @@
|
|
1
|
+
class Promiscuous::Publisher::Operation::Base
|
2
|
+
class TryAgain < RuntimeError; end
|
3
|
+
VERSION_FIELD = '_pv'
|
4
|
+
|
5
|
+
attr_accessor :operation, :operation_ext, :instance, :selector_keys
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
# XXX instance is not always an instance, it can be a selector
|
9
|
+
# representation.
|
10
|
+
@instance = options[:instance]
|
11
|
+
@operation = options[:operation]
|
12
|
+
@operation_ext = options[:operation_ext]
|
13
|
+
@multi = options[:multi]
|
14
|
+
end
|
15
|
+
|
16
|
+
def read?
|
17
|
+
operation == :read
|
18
|
+
end
|
19
|
+
|
20
|
+
def write?
|
21
|
+
!read?
|
22
|
+
end
|
23
|
+
|
24
|
+
def multi?
|
25
|
+
!!@multi
|
26
|
+
end
|
27
|
+
|
28
|
+
def single?
|
29
|
+
!@multi
|
30
|
+
end
|
31
|
+
|
32
|
+
def persists?
|
33
|
+
# TODO For writes in transactions, it should be false
|
34
|
+
write?
|
35
|
+
end
|
36
|
+
|
37
|
+
def failed?
|
38
|
+
!!@exception
|
39
|
+
end
|
40
|
+
|
41
|
+
def current_context
|
42
|
+
@current_context ||= Promiscuous::Publisher::Context.current
|
43
|
+
end
|
44
|
+
|
45
|
+
def record_timestamp
|
46
|
+
# Records the number of milliseconds since epoch, which we use send sending
|
47
|
+
# the payload over. It's good for latency measurements.
|
48
|
+
time = Time.now
|
49
|
+
@timestamp = time.to_i * 1000 + time.usec / 1000
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.rabbitmq_staging_set_key
|
53
|
+
Promiscuous::Key.new(:pub).join('rabbitmq_staging').to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
delegate :rabbitmq_staging_set_key, :to => self
|
57
|
+
|
58
|
+
def on_rabbitmq_confirm
|
59
|
+
# These requests could be parallelized, rabbitmq persisted the operation.
|
60
|
+
# XXX TODO
|
61
|
+
# Promiscuous::Redis.slave.del(@payload_recovery_key) if Promiscuous::Redis.slave
|
62
|
+
|
63
|
+
@payload_recovery_node.multi do
|
64
|
+
@payload_recovery_node.del(@payload_recovery_key)
|
65
|
+
@payload_recovery_node.zrem(rabbitmq_staging_set_key, @payload_recovery_key)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def publish_payload_in_rabbitmq_async
|
70
|
+
Promiscuous::AMQP.publish(:key => @amqp_key, :payload => @payload,
|
71
|
+
:on_confirm => method(:on_rabbitmq_confirm))
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.recover_payloads_for_rabbitmq
|
75
|
+
return unless Promiscuous::Redis.master
|
76
|
+
# This method is regularly called from a worker to resend payloads that
|
77
|
+
# never got their confirm. We get the oldest queued message, and test if
|
78
|
+
# it's old enough to for a republish (default 10 seconds).
|
79
|
+
# Any sort of race is okay since we would just republish, and that's okay.
|
80
|
+
|
81
|
+
Promiscuous::Redis.master.nodes.each do |node|
|
82
|
+
loop do
|
83
|
+
key, time = node.zrange(rabbitmq_staging_set_key, 0, 1, :with_scores => true).flatten
|
84
|
+
break unless key && Time.now.to_i >= time.to_i + Promiscuous::Config.recovery_timeout
|
85
|
+
|
86
|
+
# Refresh the score so we skip it next time we look for something to recover.
|
87
|
+
node.zadd(rabbitmq_staging_set_key, Time.now.to_i, key)
|
88
|
+
payload = node.get(key)
|
89
|
+
|
90
|
+
Promiscuous.info "[payload recovery] #{payload}"
|
91
|
+
new.instance_eval do
|
92
|
+
@payload_recovery_node = node
|
93
|
+
@payload_recovery_key = key
|
94
|
+
@amqp_key = MultiJson.load(payload)['__amqp__']
|
95
|
+
@payload = payload
|
96
|
+
publish_payload_in_rabbitmq_async
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def publish_payload_in_redis
|
103
|
+
# TODO Optimize and DRY this up
|
104
|
+
r = @committed_read_deps
|
105
|
+
w = @committed_write_deps
|
106
|
+
|
107
|
+
master_node = w.first.redis_node
|
108
|
+
operation_recovery_key = w.first.key(:pub).join('operation_recovery').to_s
|
109
|
+
# We identify a payload with a unique key (id:id_value:current_version) to
|
110
|
+
# avoid collisions with other updates on the same document.
|
111
|
+
@payload_recovery_node = master_node
|
112
|
+
@payload_recovery_key = w.first.key(:pub).join(w.first.version).to_s
|
113
|
+
|
114
|
+
# We need to be able to recover from a redis failure. By sending the
|
115
|
+
# payload to the slave first, we ensure that we can replay the lost
|
116
|
+
# payloads if the primary came to fail.
|
117
|
+
# We still need to recover the lost operations. This can be done by doing a
|
118
|
+
# version diff from what is stored in the database and the recovered redis slave.
|
119
|
+
# XXX TODO
|
120
|
+
# Promiscuous::Redis.slave.set(@payload_recovery_key, @payload) if Promiscuous::Redis.slave
|
121
|
+
|
122
|
+
# We don't care if we get raced by someone recovering our operation. It can
|
123
|
+
# happen if we lost the lock without knowing about it.
|
124
|
+
# The payload can be sent twice, which is okay since the subscribers
|
125
|
+
# tolerate it.
|
126
|
+
|
127
|
+
nodes = (w+r).map(&:redis_node).uniq
|
128
|
+
if nodes.size == 1
|
129
|
+
# We just have the master node. Since we are atomic, we don't need to do
|
130
|
+
# the 2pc dance.
|
131
|
+
master_node.multi do
|
132
|
+
master_node.del(operation_recovery_key)
|
133
|
+
master_node.set(@payload_recovery_key, @payload)
|
134
|
+
master_node.zadd(rabbitmq_staging_set_key, Time.now.to_i, @payload_recovery_key)
|
135
|
+
end
|
136
|
+
else
|
137
|
+
master_node.multi do
|
138
|
+
master_node.set(@payload_recovery_key, @payload)
|
139
|
+
master_node.zadd(rabbitmq_staging_set_key, Time.now.to_i, @payload_recovery_key)
|
140
|
+
end
|
141
|
+
|
142
|
+
# The payload is safe now. We can cleanup all the versions on the
|
143
|
+
# secondary. Note that we need to clear the master node at the end,
|
144
|
+
# as it acts as a lock on the other keys. This is important to avoid a
|
145
|
+
# race where we would delete data that doesn't belong to the current
|
146
|
+
# operation due to a lock loss.
|
147
|
+
nodes.reject { |node| node == master_node }
|
148
|
+
.each { |node| node.del(operation_recovery_key) }
|
149
|
+
master_node.del(operation_recovery_key)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_payload_and_clear_operations
|
154
|
+
# TODO Transactions with multi writes
|
155
|
+
raise "We don't support multi writes yet" if previous_successful_operations.select(&:write?).size > 1
|
156
|
+
raise "The instance is gone, or there is a version mismatch" unless @instance
|
157
|
+
|
158
|
+
payload = @instance.promiscuous.payload(:with_attributes => operation.in?([:create, :update]))
|
159
|
+
payload[:context] = current_context.name
|
160
|
+
payload[:timestamp] = @timestamp
|
161
|
+
|
162
|
+
# If the db operation has failed, so we publish a dummy operation on the
|
163
|
+
# failed instance. It's better than using the Dummy polisher class
|
164
|
+
# because a subscriber can choose not to receive any of these messages.
|
165
|
+
payload[:operation] = self.failed? ? :dummy : operation
|
166
|
+
|
167
|
+
# We need to consider the last write operation as an implicit read
|
168
|
+
# dependency. This is why we don't need to consider the read dependencies
|
169
|
+
# happening before a first write when publishing the second write in a
|
170
|
+
# context.
|
171
|
+
payload[:dependencies] = {}
|
172
|
+
payload[:dependencies][:read] = @committed_read_deps if @committed_read_deps.present?
|
173
|
+
payload[:dependencies][:write] = @committed_write_deps
|
174
|
+
|
175
|
+
current_context.last_write_dependency = @committed_write_deps.first
|
176
|
+
current_context.operations.clear
|
177
|
+
|
178
|
+
@amqp_key = payload[:__amqp__]
|
179
|
+
@payload = MultiJson.dump(payload)
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.recover_operation_from_lock(lock)
|
183
|
+
# We happen to have acquired a never released lock.
|
184
|
+
# The database instance is thus still prestine.
|
185
|
+
# Three cases to consider:
|
186
|
+
# 1) the key is not an id dependency or the payload queue stage was passed
|
187
|
+
# 2) The write query was never executed, we must send a dummy operation
|
188
|
+
# 3) The write query was executed, but never passed the payload queue stage
|
189
|
+
|
190
|
+
master_node = lock.node
|
191
|
+
recovery_data = master_node.hgetall("#{lock.key}:operation_recovery")
|
192
|
+
return nil unless recovery_data.present? # case 1)
|
193
|
+
|
194
|
+
Promiscuous.info "[operation recovery] #{lock.key} -> #{recovery_data}"
|
195
|
+
|
196
|
+
collection, instance_id, operation,
|
197
|
+
document, read_dependencies, write_dependencies = *MultiJson.load(recovery_data['payload'])
|
198
|
+
|
199
|
+
operation = operation.to_sym
|
200
|
+
read_dependencies.map! { |k| Promiscuous::Dependency.parse(k.to_s) }
|
201
|
+
write_dependencies.map! { |k| Promiscuous::Dependency.parse(k.to_s) }
|
202
|
+
|
203
|
+
model = Promiscuous::Publisher::Model.publishers[collection]
|
204
|
+
|
205
|
+
if model.is_a? Promiscuous::Publisher::Model::Ephemeral
|
206
|
+
operation = :dummy
|
207
|
+
else
|
208
|
+
# TODO Abstract db operations.
|
209
|
+
# We need to query on the root model
|
210
|
+
model = model.collection.name.singularize.camelize.constantize
|
211
|
+
end
|
212
|
+
|
213
|
+
op_klass = model.get_operation_class_for(operation)
|
214
|
+
op = op_klass.recover_operation(model, instance_id, document)
|
215
|
+
op.operation = operation
|
216
|
+
|
217
|
+
Promiscuous.context :operation_recovery, :detached_from_parent => true do
|
218
|
+
op.instance_eval do
|
219
|
+
@read_dependencies = read_dependencies
|
220
|
+
@write_dependencies = write_dependencies
|
221
|
+
@locks = [lock]
|
222
|
+
execute_persistent_locked { recover_db_operation }
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
lock.unlock
|
227
|
+
rescue Exception => e
|
228
|
+
message = "cannot recover #{lock.key} -> #{recovery_data}"
|
229
|
+
raise Promiscuous::Error::Recovery.new(message, e)
|
230
|
+
end
|
231
|
+
|
232
|
+
def increment_read_and_write_dependencies(read_dependencies, write_dependencies)
|
233
|
+
# We collapse all operations, ignoring the read/write interleaving.
|
234
|
+
# It doesn't matter since all write operations are serialized, so the first
|
235
|
+
# write in the transaction can have all the read dependencies.
|
236
|
+
r = read_dependencies
|
237
|
+
w = write_dependencies
|
238
|
+
|
239
|
+
# We don't need to do a read dependency if we are writing to it, so we
|
240
|
+
# prune them. The subscriber assumes the pruning (i.e. the intersection of
|
241
|
+
# r and w is empty) when it calculates the happens before relationships.
|
242
|
+
r -= w
|
243
|
+
|
244
|
+
master_node = w.first.redis_node
|
245
|
+
operation_recovery_key = w.first
|
246
|
+
|
247
|
+
# We group all the dependencies by their respective shards
|
248
|
+
# The master node will have the responsability to hold the recovery data.
|
249
|
+
# We do the master node first. The seconaries can be done in parallel.
|
250
|
+
(w+r).group_by(&:redis_node).each do |node, deps|
|
251
|
+
r_deps = deps.select { |dep| dep.in? r }
|
252
|
+
w_deps = deps.select { |dep| dep.in? w }
|
253
|
+
|
254
|
+
argv = []
|
255
|
+
argv << Promiscuous::Key.new(:pub) # key prefixes
|
256
|
+
argv << MultiJson.dump([r_deps, w_deps])
|
257
|
+
|
258
|
+
# Each shard have their own recovery payload. The master recovery node
|
259
|
+
# has the full operation recovery, and the others just have their versions.
|
260
|
+
argv << operation_recovery_key.as_json
|
261
|
+
if node == master_node
|
262
|
+
# We are on the master node, which holds the recovery payload
|
263
|
+
document = serialize_document_for_create_recovery if operation == :create
|
264
|
+
argv << MultiJson.dump([@instance.class.promiscuous_collection_name,
|
265
|
+
@instance.id, operation, document, r, w])
|
266
|
+
end
|
267
|
+
|
268
|
+
# We are going to store all the versions in redis, to be able to recover.
|
269
|
+
# We store all our increments in a transaction_id key in JSON format.
|
270
|
+
# Note that the transaction_id is the id of the current instance.
|
271
|
+
@@increment_script ||= Promiscuous::Redis::Script.new <<-SCRIPT
|
272
|
+
local prefix = ARGV[1] .. ':'
|
273
|
+
local deps = cjson.decode(ARGV[2])
|
274
|
+
local read_deps = deps[1]
|
275
|
+
local write_deps = deps[2]
|
276
|
+
local operation_recovery_key = prefix .. ARGV[3] .. ':operation_recovery'
|
277
|
+
local operation_recovery_payload = ARGV[4]
|
278
|
+
|
279
|
+
local read_versions = {}
|
280
|
+
local write_versions = {}
|
281
|
+
|
282
|
+
if redis.call('exists', operation_recovery_key) == 1 then
|
283
|
+
for i, dep in ipairs(read_deps) do
|
284
|
+
local key = prefix .. dep
|
285
|
+
read_versions[i] = redis.call('get', key .. ':w')
|
286
|
+
end
|
287
|
+
for i, dep in ipairs(write_deps) do
|
288
|
+
local key = prefix .. dep
|
289
|
+
write_versions[i] = redis.call('get', key .. ':w')
|
290
|
+
end
|
291
|
+
else
|
292
|
+
for i, dep in ipairs(read_deps) do
|
293
|
+
local key = prefix .. dep
|
294
|
+
redis.call('incr', key .. ':rw')
|
295
|
+
read_versions[i] = redis.call('get', key .. ':w')
|
296
|
+
redis.call('hset', operation_recovery_key, dep, read_versions[i])
|
297
|
+
end
|
298
|
+
|
299
|
+
for i, dep in ipairs(write_deps) do
|
300
|
+
local key = prefix .. dep
|
301
|
+
write_versions[i] = redis.call('incr', key .. ':rw')
|
302
|
+
redis.call('set', key .. ':w', write_versions[i])
|
303
|
+
redis.call('hset', operation_recovery_key, dep, write_versions[i])
|
304
|
+
end
|
305
|
+
|
306
|
+
if operation_recovery_payload then
|
307
|
+
redis.call('hset', operation_recovery_key, 'payload', operation_recovery_payload)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
return { read_versions, write_versions }
|
312
|
+
SCRIPT
|
313
|
+
read_versions, write_versions = @@increment_script.eval(node, :argv => argv)
|
314
|
+
|
315
|
+
r_deps.zip(read_versions).each { |dep, version| dep.version = version.to_i }
|
316
|
+
w_deps.zip(write_versions).each { |dep, version| dep.version = version.to_i }
|
317
|
+
end
|
318
|
+
|
319
|
+
@committed_read_deps = r
|
320
|
+
@committed_write_deps = w
|
321
|
+
@instance_version = w.first.version
|
322
|
+
end
|
323
|
+
|
324
|
+
LOCK_OPTIONS = { :timeout => 10.seconds, # after 10 seconds, we give up
|
325
|
+
:sleep => 0.01, # polling every 10ms.
|
326
|
+
:expire => 1.minute } # after one minute, we are considered dead
|
327
|
+
|
328
|
+
def self.lock_options
|
329
|
+
LOCK_OPTIONS.merge({ :lock_set => Promiscuous::Key.new(:pub).join('lock_set').to_s })
|
330
|
+
end
|
331
|
+
|
332
|
+
def self.recover_locks
|
333
|
+
return unless Promiscuous::Redis.master
|
334
|
+
# This method is regularly called from a worker to recover locks by doing a
|
335
|
+
# locking/unlocking cycle.
|
336
|
+
|
337
|
+
Promiscuous::Redis.master.nodes.each do |node|
|
338
|
+
loop do
|
339
|
+
key, time = node.zrange(lock_options[:lock_set], 0, 1, :with_scores => true).flatten
|
340
|
+
break unless key && Time.now.to_i >= time.to_i + lock_options[:expire]
|
341
|
+
|
342
|
+
mutex = Promiscuous::Redis::Mutex.new(key, lock_options.merge(:node => node))
|
343
|
+
case mutex.lock
|
344
|
+
when :recovered then recover_operation_from_lock(mutex)
|
345
|
+
when true then mutex.unlock
|
346
|
+
when false then ;
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def locks_from_write_dependencies
|
353
|
+
# XXX TODO Support multi row writes
|
354
|
+
instance_dep = write_dependencies.first
|
355
|
+
return [] unless instance_dep
|
356
|
+
options = self.class.lock_options.merge(:node => instance_dep.redis_node)
|
357
|
+
[Promiscuous::Redis::Mutex.new(instance_dep.key(:pub).to_s, options)]
|
358
|
+
end
|
359
|
+
|
360
|
+
def lock_write_dependencies
|
361
|
+
# returns true if we could get all the locks, false otherwise
|
362
|
+
|
363
|
+
start_at = Time.now
|
364
|
+
@recovered_locks = []
|
365
|
+
|
366
|
+
# We acquire all the locks in order, and unlock everything if one come
|
367
|
+
# to fail. lock/unlock return true/false when they succeed/fail
|
368
|
+
locks = locks_from_write_dependencies
|
369
|
+
locks.reduce(->{ @locks = locks; true }) do |chain, l|
|
370
|
+
lambda do
|
371
|
+
return false if Time.now - start_at > LOCK_OPTIONS[:timeout]
|
372
|
+
case l.lock
|
373
|
+
# Note that we do not unlock the recovered lock if the chain fails
|
374
|
+
when :recovered then @recovered_locks << l; chain.call
|
375
|
+
when true then chain.call or (l.unlock; false)
|
376
|
+
when false then @unavailable_lock = l; false
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end.call
|
380
|
+
end
|
381
|
+
|
382
|
+
def unlock_write_dependencies
|
383
|
+
# returns true if we could unlock all the locks, false otherwise
|
384
|
+
return true if @locks.blank?
|
385
|
+
@locks.reduce(true) { |result, l| l.unlock && result }.tap { @locks = nil }
|
386
|
+
end
|
387
|
+
|
388
|
+
def _reload_instance_dependencies
|
389
|
+
if read?
|
390
|
+
# We want to use the smallest subset that we can depend on when doing
|
391
|
+
# reads. tracked_dependencies comes sorted from the smallest subset to
|
392
|
+
# the largest. For maximum performance on the subscriber side, we thus
|
393
|
+
# pick the first one. In most cases, it should resolve to the id
|
394
|
+
# dependency.
|
395
|
+
best_dependency = @instance.promiscuous.tracked_dependencies.first
|
396
|
+
unless best_dependency
|
397
|
+
raise Promiscuous::Error::Dependency.new(:operation => self)
|
398
|
+
end
|
399
|
+
[best_dependency]
|
400
|
+
else
|
401
|
+
# Note that tracked_dependencies will not return the id dependency if it
|
402
|
+
# doesn't exist which can only happen for create operations and auto
|
403
|
+
# generated ids. Be aware that with auto generated id, create operation
|
404
|
+
# might not provide the id dependency.
|
405
|
+
@instance.promiscuous.tracked_dependencies
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def reload_instance_dependencies
|
410
|
+
# Returns true when the dependencies changed, false otherwise
|
411
|
+
@write_dependencies = nil
|
412
|
+
old = @instance_dependencies
|
413
|
+
@instance_dependencies = _reload_instance_dependencies
|
414
|
+
old != @instance_dependencies
|
415
|
+
end
|
416
|
+
|
417
|
+
def instance_dependencies
|
418
|
+
reload_instance_dependencies unless @instance_dependencies
|
419
|
+
@instance_dependencies
|
420
|
+
end
|
421
|
+
|
422
|
+
def previous_successful_operations
|
423
|
+
current_context.operations.reject(&:failed?)
|
424
|
+
end
|
425
|
+
|
426
|
+
def read_dependencies
|
427
|
+
# We memoize the read dependencies not just for performance, but also
|
428
|
+
# because we store the versions once incremented in these.
|
429
|
+
return @read_dependencies if @read_dependencies
|
430
|
+
read_dependencies = previous_successful_operations.select(&:read?)
|
431
|
+
.map(&:instance_dependencies).flatten
|
432
|
+
|
433
|
+
# We implicitly have a read dependency on the latest write.
|
434
|
+
if current_context.last_write_dependency
|
435
|
+
current_context.last_write_dependency.version = nil
|
436
|
+
read_dependencies << current_context.last_write_dependency
|
437
|
+
end
|
438
|
+
|
439
|
+
@read_dependencies = read_dependencies.uniq
|
440
|
+
end
|
441
|
+
alias verify_read_dependencies read_dependencies
|
442
|
+
|
443
|
+
def write_dependencies
|
444
|
+
# The cache is cleared when we call reload_instance_dependencies
|
445
|
+
@write_dependencies ||= previous_successful_operations.select(&:write?)
|
446
|
+
.map(&:instance_dependencies).flatten.uniq
|
447
|
+
end
|
448
|
+
|
449
|
+
def reload_instance
|
450
|
+
@instance = without_promiscuous { fetch_instance }
|
451
|
+
end
|
452
|
+
|
453
|
+
def perform_db_operation_with_no_exceptions(&db_operation)
|
454
|
+
going_to_execute_db_operation
|
455
|
+
@result = db_operation.call(self)
|
456
|
+
rescue Exception => e
|
457
|
+
@exception = e
|
458
|
+
end
|
459
|
+
|
460
|
+
def lock_instance_for_execute_persistent
|
461
|
+
current_context.add_operation(self)
|
462
|
+
|
463
|
+
# Note: At first, @instance can be a representation of a selector, to
|
464
|
+
# become a real model instance once we get to fetch it from the db with
|
465
|
+
# reload_instance to lock an instance that matches the selector.
|
466
|
+
# This is a good thing because we allow the underlying driver to hook from
|
467
|
+
# the model interface to the driver interface easily.
|
468
|
+
auto_unlock = true
|
469
|
+
|
470
|
+
begin
|
471
|
+
unless lock_write_dependencies
|
472
|
+
raise Promiscuous::Error::LockUnavailable.new(@unavailable_lock.key)
|
473
|
+
end
|
474
|
+
|
475
|
+
if @recovered_locks.present?
|
476
|
+
# When recovering locks, if we fail, we must not release the lock again
|
477
|
+
# to allow another one to do the recovery.
|
478
|
+
auto_unlock = false
|
479
|
+
@recovered_locks.each { |lock| self.class.recover_operation_from_lock(lock) }
|
480
|
+
auto_unlock = true
|
481
|
+
raise TryAgain
|
482
|
+
end
|
483
|
+
|
484
|
+
if operation != :create
|
485
|
+
# We need to lock and update all the dependencies before any other
|
486
|
+
# readers can see our write through any one of our tracked attributes.
|
487
|
+
|
488
|
+
# We want to reload the instance to make sure we have all the locked
|
489
|
+
# dependencies that we need. It's a query we cannot avoid when we have
|
490
|
+
# tracked dependencies. There is a bit of room for optimization.
|
491
|
+
# If the selector doesn't fetch any instance, the query has no effect
|
492
|
+
# so we can bypass it as if nothing happened. If reload_instance
|
493
|
+
# raises an exception, it's okay to let it bubble up since we haven't
|
494
|
+
# touch anything yet except for the locks (which will be unlocked on
|
495
|
+
# the way out)
|
496
|
+
return false unless reload_instance
|
497
|
+
|
498
|
+
# If reload_instance changed the current instance because the selector,
|
499
|
+
# we need to unlock the old instance, lock this new instance, and
|
500
|
+
# retry. XXX What should we do if we are going in a live lock?
|
501
|
+
# Sleep with some jitter?
|
502
|
+
if reload_instance_dependencies
|
503
|
+
raise TryAgain
|
504
|
+
end
|
505
|
+
end
|
506
|
+
rescue TryAgain
|
507
|
+
unlock_write_dependencies if auto_unlock
|
508
|
+
retry
|
509
|
+
end
|
510
|
+
|
511
|
+
verify_read_dependencies
|
512
|
+
if write_dependencies.blank?
|
513
|
+
# TODO We don't like auto generated ids. A good solution is to do all
|
514
|
+
# writes in a transaction, so we can know the ids at commit time.
|
515
|
+
raise "We don't support auto generated id yet"
|
516
|
+
end
|
517
|
+
|
518
|
+
# We are now in the possession of an instance that matches the original
|
519
|
+
# selector, we can proceed.
|
520
|
+
auto_unlock = false
|
521
|
+
true
|
522
|
+
ensure
|
523
|
+
# In case of an exception was raised before we updated the version in
|
524
|
+
# redis, we can unlock because we don't need recovery.
|
525
|
+
unlock_write_dependencies if auto_unlock
|
526
|
+
end
|
527
|
+
|
528
|
+
def execute_persistent_locked(&db_operation)
|
529
|
+
# We are going to commit all the pending writes in the context if we are
|
530
|
+
# doing a transaction commit. We also commit the current write operation for
|
531
|
+
# atomic writes without transactions. We enable the recovery mechanism by
|
532
|
+
# having someone expiring our lock if we die in the middle.
|
533
|
+
|
534
|
+
# All the versions are updated and a marked as pending for publish in Redis
|
535
|
+
# atomically in case we die before we could write the versions in the
|
536
|
+
# database. Once incremented, concurrent queries that are reading our
|
537
|
+
# instance will be serialized after our write, even through it may read our
|
538
|
+
# old instance. This is a race that we tolerate.
|
539
|
+
# XXX We also stash the document for create operations, so the recovery can
|
540
|
+
# redo the create to avoid races when instances are getting partitioned.
|
541
|
+
increment_read_and_write_dependencies(read_dependencies, write_dependencies)
|
542
|
+
|
543
|
+
# From this point, if we die, the one expiring our write locks must finish
|
544
|
+
# the publish, either by sending a dummy, or by sending the real instance.
|
545
|
+
# We could have die before or after the database query.
|
546
|
+
|
547
|
+
# We save the versions in the database, as it is our source of truth.
|
548
|
+
# This allow a reconstruction of redis in the face of failures.
|
549
|
+
# We would also need to send a special message to the subscribers to reset
|
550
|
+
# their read counters to the last write version since we would not be able
|
551
|
+
# to restore the read counters (and we don't want to store them because
|
552
|
+
# this would dramatically augment our footprint on the db).
|
553
|
+
#
|
554
|
+
# If we are doing a destroy operation, and redis dies right after, and
|
555
|
+
# we happen to lost contact with rabbitmq, recovery is going to be complex:
|
556
|
+
# we would need to do a diff from the dummy subscriber to see what
|
557
|
+
# documents are missing on our side to be able to resend the destroy
|
558
|
+
# message.
|
559
|
+
|
560
|
+
case operation
|
561
|
+
when :create
|
562
|
+
stash_version_in_write_query
|
563
|
+
when :update
|
564
|
+
stash_version_in_write_query
|
565
|
+
# We are now in the possession of an instance that matches the original
|
566
|
+
# selector. We need to make sure the db_operation will operate on it,
|
567
|
+
# instead of the original selector.
|
568
|
+
use_id_selector(:use_atomic_version_selector => true)
|
569
|
+
# We need to use an atomic versioned selector to make sure that
|
570
|
+
# if we lose the lock for a long period of time, we don't mess up
|
571
|
+
# with other people's updates. Also we make sure that the recovery
|
572
|
+
# mechanism is not racing with us.
|
573
|
+
when :destroy
|
574
|
+
use_id_selector(:use_atomic_version_selector => true)
|
575
|
+
end
|
576
|
+
|
577
|
+
# Perform the actual database query (single write or transaction commit).
|
578
|
+
# If successful, the result goes in @result, otherwise, @exception contains
|
579
|
+
# the thrown exception.
|
580
|
+
perform_db_operation_with_no_exceptions(&db_operation)
|
581
|
+
|
582
|
+
# We take a timestamp right after the write is performed because latency
|
583
|
+
# measurements are performed on the subscriber.
|
584
|
+
record_timestamp
|
585
|
+
|
586
|
+
if operation == :update && !failed?
|
587
|
+
# The underlying driver should implement some sort of find and modify
|
588
|
+
# operation in the previous write query to avoid this extra read query.
|
589
|
+
# If reload_instance raise an exception, we let it bubble up,
|
590
|
+
# and we'll trigger the recovery mechanism.
|
591
|
+
use_id_selector
|
592
|
+
reload_instance
|
593
|
+
end
|
594
|
+
|
595
|
+
unless @locks.first.still_locked?
|
596
|
+
# We lost the lock, let the recovery mechanism do its thing.
|
597
|
+
# This is a code optimization to avoid checking if the db operation
|
598
|
+
# succeeded or not because of the db operation race during recovery.
|
599
|
+
raise Promiscuous::Error::LostLock.new(@locks.first.key)
|
600
|
+
end
|
601
|
+
|
602
|
+
generate_payload_and_clear_operations
|
603
|
+
|
604
|
+
# As soon as we unlock the locks, the rescuer will not be able to assume
|
605
|
+
# that the database instance is still pristine, and so we need to stash the
|
606
|
+
# payload in redis. If redis dies, we don't care because it can be
|
607
|
+
# reconstructed. Subscribers can see "compressed" updates.
|
608
|
+
publish_payload_in_redis
|
609
|
+
|
610
|
+
# TODO Performance: merge these 3 redis operations to speed things up.
|
611
|
+
unlock_write_dependencies
|
612
|
+
|
613
|
+
# If we die from this point on, a recovery worker can republish our payload
|
614
|
+
# since we queued it in Redis.
|
615
|
+
|
616
|
+
# We don't care if we lost the lock and got recovered, subscribers are
|
617
|
+
# immune to duplicate messages.
|
618
|
+
publish_payload_in_rabbitmq_async
|
619
|
+
end
|
620
|
+
|
621
|
+
# --- the following methods can be overridden by the driver --- #
|
622
|
+
|
623
|
+
def execute_persistent(&db_operation)
|
624
|
+
return nil unless lock_instance_for_execute_persistent
|
625
|
+
execute_persistent_locked(&db_operation)
|
626
|
+
end
|
627
|
+
|
628
|
+
def execute_non_persistent(&db_operation)
|
629
|
+
# We are getting here in the following cases:
|
630
|
+
# * read: we fetch the instance. It's the driver's job to cache the
|
631
|
+
# raw instance and return it during db_operation.
|
632
|
+
# * multi read: nothing to do, we'll keep our current selector, sadly
|
633
|
+
# * write in a transaction: TODO
|
634
|
+
|
635
|
+
if single?
|
636
|
+
# If the query misses, we don't bother
|
637
|
+
return nil unless reload_instance
|
638
|
+
use_id_selector
|
639
|
+
end
|
640
|
+
|
641
|
+
# We don't do any reload_instance_dependencies at this point (and thus we
|
642
|
+
# won't raise an exception on a multi read that we cannot track).
|
643
|
+
# We'll wait until the commit, and hopefully with tainting, we'll be able to
|
644
|
+
# tell if we should depend the multi read operation in question.
|
645
|
+
perform_db_operation_with_no_exceptions(&db_operation)
|
646
|
+
# If the db_operation raises, we don't consider this failed operation when
|
647
|
+
# committing the next persistent write by omitting the operation in the
|
648
|
+
# context.
|
649
|
+
current_context.add_operation(self) unless failed?
|
650
|
+
end
|
651
|
+
|
652
|
+
def execute(&db_operation)
|
653
|
+
# execute returns the result of the db_operation to perform
|
654
|
+
db_operation ||= proc {}
|
655
|
+
return db_operation.call if Promiscuous.disabled
|
656
|
+
|
657
|
+
unless current_context
|
658
|
+
raise Promiscuous::Error::MissingContext if write?
|
659
|
+
return db_operation.call # Don't care for a read
|
660
|
+
end
|
661
|
+
|
662
|
+
self.persists? ? execute_persistent(&db_operation) :
|
663
|
+
execute_non_persistent(&db_operation)
|
664
|
+
|
665
|
+
@exception ? (raise @exception) : @result
|
666
|
+
end
|
667
|
+
|
668
|
+
def fetch_instance
|
669
|
+
# This method is overridden to use the original query selector.
|
670
|
+
# Should return nil if the instance is not found.
|
671
|
+
@instance
|
672
|
+
end
|
673
|
+
|
674
|
+
def serialize_document_for_create_recovery
|
675
|
+
# Overridden to be able to redo the create during recovery.
|
676
|
+
nil
|
677
|
+
end
|
678
|
+
|
679
|
+
def self.recover_operation(model, instance_id, document)
|
680
|
+
# Overriden to reconstruct the operation. If the database is read, only the
|
681
|
+
# primary must be used.
|
682
|
+
new(:instance => model.new { |instance| instance.id = instance_id })
|
683
|
+
end
|
684
|
+
|
685
|
+
def recover_db_operation
|
686
|
+
# Overriden to reexecute the db operation during recovery (or make sure that
|
687
|
+
# it will never succeed).
|
688
|
+
end
|
689
|
+
|
690
|
+
def use_id_selector(options={})
|
691
|
+
# Overridden to use the {:id => @instance.id} selector.
|
692
|
+
# if use_atomic_version_selector is passed, the driver must
|
693
|
+
# add the VERSION_FIELD selector if present in original instance.
|
694
|
+
end
|
695
|
+
|
696
|
+
def use_versioned_selector
|
697
|
+
# Overridden to use the {VERSION_FIELD => @instance[VERSION_FIELD]} selector.
|
698
|
+
end
|
699
|
+
|
700
|
+
def stash_version_in_write_query
|
701
|
+
# Overridden to update the query to set 'instance.VERSION_FIELD = @instance_version'
|
702
|
+
end
|
703
|
+
|
704
|
+
def going_to_execute_db_operation
|
705
|
+
# Test hook
|
706
|
+
end
|
707
|
+
end
|