promiscuous 0.53.1 → 0.90.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.
- 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
|