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,370 @@
|
|
1
|
+
raise "mongoid > 3.0.19 please" unless Gem.loaded_specs['mongoid'].version >= Gem::Version.new('3.0.19')
|
2
|
+
raise "moped > 1.3.2 please" unless Gem.loaded_specs['moped'].version >= Gem::Version.new('1.3.2')
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
class Moped::PromiscuousCollectionWrapper < Moped::Collection
|
7
|
+
class PromiscuousCollectionOperation < Promiscuous::Publisher::Operation::Base
|
8
|
+
def initialize(options={})
|
9
|
+
super
|
10
|
+
@operation = :create
|
11
|
+
@collection = options[:collection]
|
12
|
+
@document = options[:document]
|
13
|
+
end
|
14
|
+
|
15
|
+
def model
|
16
|
+
@model ||= @document.try(:[], '_type').try(:constantize) ||
|
17
|
+
Promiscuous::Publisher::Model::Mongoid.collection_mapping[@collection.name]
|
18
|
+
# Double check because of the _type lookup
|
19
|
+
@model = nil unless @model < Promiscuous::Publisher::Model::Mongoid
|
20
|
+
@model
|
21
|
+
rescue NameError
|
22
|
+
end
|
23
|
+
|
24
|
+
def serialize_document_for_create_recovery
|
25
|
+
# TODO the serialization/deserialization is not very nice, but we need
|
26
|
+
# the bson types.
|
27
|
+
@document.to_yaml
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.recover_operation(model, instance_id, document)
|
31
|
+
document = YAML.load(document)
|
32
|
+
instance = Mongoid::Factory.from_db(model, document)
|
33
|
+
new(:collection => model.collection, :document => document, :instance => instance)
|
34
|
+
end
|
35
|
+
|
36
|
+
def recover_db_operation
|
37
|
+
without_promiscuous do
|
38
|
+
return if model.unscoped.where(:id => @instance.id).first # already done?
|
39
|
+
@collection.insert(@document)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def stash_version_in_write_query
|
44
|
+
@document[VERSION_FIELD] = @instance_version
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute_persistent(&db_operation)
|
48
|
+
@instance = Mongoid::Factory.from_db(model, @document)
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute(&db_operation)
|
53
|
+
return db_operation.call unless model
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def promiscuous_create_operation(options)
|
59
|
+
PromiscuousCollectionOperation.new(options.merge(:collection => self, :operation => :create))
|
60
|
+
end
|
61
|
+
|
62
|
+
# Moped::Collection
|
63
|
+
|
64
|
+
# Create has its own Operation class, as it's the only scenario where there
|
65
|
+
# is no matching document in the database
|
66
|
+
def insert(documents, flags=nil)
|
67
|
+
documents = [documents] unless documents.is_a?(Array)
|
68
|
+
documents.each do |doc|
|
69
|
+
promiscuous_create_operation(:document => doc).execute { super(doc, flags) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# TODO aggregate
|
74
|
+
end
|
75
|
+
|
76
|
+
class Moped::PromiscuousQueryWrapper < Moped::Query
|
77
|
+
class PromiscuousQueryOperation < Promiscuous::Publisher::Operation::Base
|
78
|
+
attr_accessor :raw_instance, :new_raw_instance, :change
|
79
|
+
|
80
|
+
def initialize(options={})
|
81
|
+
super
|
82
|
+
@query = options[:query]
|
83
|
+
@change = options[:change]
|
84
|
+
end
|
85
|
+
|
86
|
+
def collection_name
|
87
|
+
@collection_name ||= @query.collection.is_a?(String) ? @query.collection : @query.collection.name
|
88
|
+
end
|
89
|
+
|
90
|
+
def model
|
91
|
+
@model ||= Promiscuous::Publisher::Model::Mongoid.collection_mapping[collection_name]
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.recover_operation(model, instance_id, document)
|
95
|
+
# TODO We need to use the primary database. We cannot read from a
|
96
|
+
# secondary.
|
97
|
+
query = model.unscoped.where(:id => instance_id).query
|
98
|
+
op = new(:query => query, :change => {})
|
99
|
+
# TODO refactor this not so pretty instance_eval
|
100
|
+
op.instance_eval do
|
101
|
+
reload_instance
|
102
|
+
@instance ||= get_selector_instance
|
103
|
+
end
|
104
|
+
op
|
105
|
+
end
|
106
|
+
|
107
|
+
def recover_db_operation
|
108
|
+
# We no-op the update/destroy operation instead of making it idempotent.
|
109
|
+
# The original caller will fail because the lock was unlocked.
|
110
|
+
without_promiscuous { @query.update(@change) }
|
111
|
+
@operation = :dummy
|
112
|
+
end
|
113
|
+
|
114
|
+
def fetch_instance
|
115
|
+
@raw_instance = @new_raw_instance || without_promiscuous { @query.first }
|
116
|
+
Mongoid::Factory.from_db(model, @raw_instance) if @raw_instance
|
117
|
+
end
|
118
|
+
|
119
|
+
def use_id_selector(options={})
|
120
|
+
selector = {'_id' => @instance.id}
|
121
|
+
|
122
|
+
if options[:use_atomic_version_selector]
|
123
|
+
version = @instance[VERSION_FIELD]
|
124
|
+
selector.merge!(VERSION_FIELD => version) if version
|
125
|
+
end
|
126
|
+
|
127
|
+
@query.selector = selector
|
128
|
+
end
|
129
|
+
|
130
|
+
def stash_version_in_write_query
|
131
|
+
@change['$set'] ||= {}
|
132
|
+
@change['$set'][VERSION_FIELD] = @instance_version
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_selector_instance
|
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)
|
155
|
+
# We are trying to be optimistic for the locking. We are trying to figure
|
156
|
+
# out our dependencies with the selector upfront to avoid an extra read
|
157
|
+
# from reload_instance.
|
158
|
+
@instance = get_selector_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
|
167
|
+
super
|
168
|
+
end
|
169
|
+
|
170
|
+
def fields_in_query(change)
|
171
|
+
# We are going to extract all the keys in any nested hashes, this will be the
|
172
|
+
# list of fields that can potentially change during the update.
|
173
|
+
if change.is_a?(Hash)
|
174
|
+
fields = change.keys + change.values.map(&method(:fields_in_query)).flatten
|
175
|
+
# The split on . is for embedded documents, we don't look further down.
|
176
|
+
fields.map { |f| f.to_s.split('.').first}.select { |k| k.to_s =~ /^[^$]/ }.uniq
|
177
|
+
else
|
178
|
+
[]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def any_published_field_changed?
|
183
|
+
return true unless @change
|
184
|
+
|
185
|
+
# TODO maybe we should cache these things
|
186
|
+
# TODO discover field dependencies automatically (hard)
|
187
|
+
aliases = Hash[model.aliased_fields.map { |k,v| [v,k] }]
|
188
|
+
attributes = fields_in_query(@change).map { |f| (aliases[f.to_s] || f).to_sym }
|
189
|
+
(attributes & model.published_db_fields).present?
|
190
|
+
end
|
191
|
+
|
192
|
+
def execute(&db_operation)
|
193
|
+
return db_operation.call if @query.without_promiscuous?
|
194
|
+
return db_operation.call unless model
|
195
|
+
return db_operation.call unless any_published_field_changed?
|
196
|
+
|
197
|
+
# We cannot do multi update/destroy
|
198
|
+
if (operation == :update || operation == :destroy) && multi?
|
199
|
+
raise Promiscuous::Error::Dependency.new(:operation => self)
|
200
|
+
end
|
201
|
+
super
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def promiscuous_operation(operation, options={})
|
206
|
+
PromiscuousQueryOperation.new(options.merge(:query => self, :operation => operation))
|
207
|
+
end
|
208
|
+
|
209
|
+
def selector=(value)
|
210
|
+
@selector = value
|
211
|
+
@operation.selector = value
|
212
|
+
end
|
213
|
+
|
214
|
+
def without_promiscuous!
|
215
|
+
@without_promiscuous = true
|
216
|
+
end
|
217
|
+
|
218
|
+
def without_promiscuous?
|
219
|
+
!!@without_promiscuous
|
220
|
+
end
|
221
|
+
|
222
|
+
# Moped::Query
|
223
|
+
|
224
|
+
def count(*args)
|
225
|
+
promiscuous_operation(:read, :multi => true, :operation_ext => :count).execute { super }.to_i
|
226
|
+
end
|
227
|
+
|
228
|
+
def distinct(key)
|
229
|
+
promiscuous_operation(:read, :multi => true).execute { super }
|
230
|
+
end
|
231
|
+
|
232
|
+
def each
|
233
|
+
# The TLS is used to pass arguments to the Cursor so we don't hijack more than
|
234
|
+
# necessary.
|
235
|
+
old_moped_query, Thread.current[:moped_query] = Thread.current[:moped_query], self
|
236
|
+
super
|
237
|
+
ensure
|
238
|
+
Thread.current[:moped_query] = old_moped_query
|
239
|
+
end
|
240
|
+
alias :cursor :each
|
241
|
+
|
242
|
+
def first
|
243
|
+
# TODO If the the user is using something like .only(), we need to make
|
244
|
+
# sure that we add the id, otherwise we may not be able to perform the
|
245
|
+
# dependency optimization by resolving the selector to an id.
|
246
|
+
promiscuous_operation(:read).execute do |operation|
|
247
|
+
operation ? operation.raw_instance : super
|
248
|
+
end
|
249
|
+
end
|
250
|
+
alias :one :first
|
251
|
+
|
252
|
+
def update(change, flags=nil)
|
253
|
+
multi = flags && flags.include?(:multi)
|
254
|
+
raise "No upsert support yet" if flags && flags.include?(:upsert)
|
255
|
+
|
256
|
+
promiscuous_operation(:update, :change => change, :multi => multi).execute do |operation|
|
257
|
+
if operation
|
258
|
+
operation.new_raw_instance = without_promiscuous { modify(change, :new => true) }
|
259
|
+
# FIXME raise when recovery raced
|
260
|
+
{'updatedExisting' => true, 'n' => 1, 'err' => nil, 'ok' => 1.0}
|
261
|
+
else
|
262
|
+
super
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def modify(change, options={})
|
268
|
+
promiscuous_operation(:update, :change => change).execute { super }
|
269
|
+
# FIXME raise when recovery raced
|
270
|
+
end
|
271
|
+
|
272
|
+
def remove
|
273
|
+
promiscuous_operation(:destroy).execute { super }
|
274
|
+
# FIXME raise when recovery raced
|
275
|
+
end
|
276
|
+
|
277
|
+
def remove_all
|
278
|
+
promiscuous_operation(:destroy, :multi => true).execute { super }
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
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
|
+
# Moped::Cursor
|
289
|
+
|
290
|
+
def fake_single_read(operation)
|
291
|
+
@cursor_id = 0
|
292
|
+
[operation.raw_instance].compact
|
293
|
+
end
|
294
|
+
|
295
|
+
def load_docs
|
296
|
+
should_fake_single_read = @limit == 1
|
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
|
300
|
+
end
|
301
|
+
|
302
|
+
def get_more
|
303
|
+
# TODO support batch_size
|
304
|
+
promiscuous_operation(:read, :multi => true).execute { super }
|
305
|
+
end
|
306
|
+
|
307
|
+
def initialize(session, query_operation)
|
308
|
+
super
|
309
|
+
@query = Thread.current[:moped_query]
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class Moped::PromiscuousDatabase < Moped::Database
|
314
|
+
# TODO it might be safer to use the alias attribute method because promiscuous
|
315
|
+
# may come late in the loading.
|
316
|
+
def promiscuous_operation(op, options={})
|
317
|
+
Moped::PromiscuousQueryWrapper::PromiscuousQueryOperation.new(
|
318
|
+
options.merge(:operation => op))
|
319
|
+
end
|
320
|
+
|
321
|
+
# Moped::Database
|
322
|
+
|
323
|
+
def command(command)
|
324
|
+
if command[:mapreduce]
|
325
|
+
query = Moped::Query.new(self[command[:mapreduce]], command[:query])
|
326
|
+
promiscuous_operation(:read, :query => query,
|
327
|
+
:operation_ext => :mapreduce, :multi => true).execute { super }
|
328
|
+
else
|
329
|
+
super
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
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
|
+
class Mongoid::Validations::UniquenessValidator
|
350
|
+
alias_method :validate_root_without_promisucous, :validate_root
|
351
|
+
def validate_root(*args)
|
352
|
+
without_promiscuous { validate_root_without_promisucous(*args) }
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
class Moped::BSON::ObjectId
|
357
|
+
# No {"$oid": "123"}, it's horrible
|
358
|
+
def to_json(*args)
|
359
|
+
"\"#{to_s}\""
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
Moped.__send__(:remove_const, :Collection)
|
364
|
+
Moped.__send__(:const_set, :Collection, Moped::PromiscuousCollectionWrapper)
|
365
|
+
Moped.__send__(:remove_const, :Query)
|
366
|
+
Moped.__send__(:const_set, :Query, Moped::PromiscuousQueryWrapper)
|
367
|
+
Moped.__send__(:remove_const, :Cursor)
|
368
|
+
Moped.__send__(:const_set, :Cursor, Moped::PromiscuousCursorWrapper)
|
369
|
+
Moped.__send__(:remove_const, :Database)
|
370
|
+
Moped.__send__(:const_set, :Database, Moped::PromiscuousDatabase)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Promiscuous::Publisher::Worker
|
2
|
+
def initialize
|
3
|
+
@recovery_timer = Promiscuous::Timer.new
|
4
|
+
@timeout = Promiscuous::Config.recovery_timeout
|
5
|
+
end
|
6
|
+
|
7
|
+
def start
|
8
|
+
@recovery_timer.run_every(@timeout, :run_immediately => true) { try_recover }
|
9
|
+
end
|
10
|
+
|
11
|
+
def stop
|
12
|
+
@recovery_timer.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
def try_recover
|
16
|
+
Promiscuous::Publisher::Operation::Base.recover_locks
|
17
|
+
Promiscuous::Publisher::Operation::Base.recover_payloads_for_rabbitmq
|
18
|
+
rescue Exception => e
|
19
|
+
Promiscuous.warn "[recovery] #{e} #{e.backtrace.join("\n")}"
|
20
|
+
Promiscuous::Config.error_notifier.try(:call, e)
|
21
|
+
end
|
22
|
+
end
|
data/lib/promiscuous/railtie.rb
CHANGED
@@ -1,12 +1,30 @@
|
|
1
1
|
class Promiscuous::Railtie < Rails::Railtie
|
2
2
|
initializer 'load promiscuous' do
|
3
|
+
config.before_initialize do
|
4
|
+
ActionController::Base.__send__(:include, Promiscuous::Middleware::Controller)
|
5
|
+
end
|
6
|
+
|
3
7
|
config.after_initialize do
|
4
|
-
Promiscuous::
|
8
|
+
Promiscuous::Config.configure unless Promiscuous::Config.configured?
|
9
|
+
Promiscuous::Loader.prepare
|
10
|
+
|
5
11
|
ActionDispatch::Reloader.to_prepare do
|
6
|
-
Promiscuous::Loader.
|
12
|
+
Promiscuous::Loader.prepare
|
7
13
|
end
|
8
14
|
ActionDispatch::Reloader.to_cleanup do
|
9
|
-
Promiscuous::Loader.
|
15
|
+
Promiscuous::Loader.cleanup
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
console do
|
21
|
+
class << IRB
|
22
|
+
alias_method :start_without_promiscuous, :start
|
23
|
+
|
24
|
+
def start
|
25
|
+
::Promiscuous::Middleware.with_context 'rails/console' do
|
26
|
+
start_without_promiscuous
|
27
|
+
end
|
10
28
|
end
|
11
29
|
end
|
12
30
|
end
|
data/lib/promiscuous/redis.rb
CHANGED
@@ -1,48 +1,52 @@
|
|
1
1
|
require 'redis'
|
2
|
+
require 'redis/distributed'
|
3
|
+
require 'digest/sha1'
|
2
4
|
|
3
5
|
module Promiscuous::Redis
|
4
|
-
mattr_accessor :master
|
6
|
+
mattr_accessor :master, :slave
|
5
7
|
|
6
8
|
def self.connect
|
7
9
|
disconnect
|
8
10
|
self.master = new_connection
|
9
11
|
end
|
10
12
|
|
13
|
+
def self.ensure_slave
|
14
|
+
# ensure_slave is called on the first publisher declaration.
|
15
|
+
if Promiscuous::Config.redis_slave_url
|
16
|
+
self.slave = new_connection(Promiscuous::Config.redis_slave_url)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
11
20
|
def self.disconnect
|
12
|
-
self.master.
|
21
|
+
self.master.quit if self.master
|
22
|
+
self.slave.quit if self.slave
|
13
23
|
self.master = nil
|
24
|
+
self.slave = nil
|
14
25
|
end
|
15
26
|
|
16
|
-
def self.new_connection
|
17
|
-
|
27
|
+
def self.new_connection(url=nil)
|
28
|
+
url ||= Promiscuous::Config.redis_urls
|
29
|
+
redis = ::Redis::Distributed.new(url, :tcp_keepalive => 60)
|
18
30
|
|
19
|
-
|
20
|
-
|
21
|
-
|
31
|
+
redis.info.each do |info|
|
32
|
+
version = info['redis_version']
|
33
|
+
unless Gem::Version.new(version) >= Gem::Version.new('2.6.0')
|
34
|
+
raise "You are using Redis #{version}. Please use Redis 2.6.0 or later."
|
35
|
+
end
|
36
|
+
end
|
22
37
|
|
23
|
-
redis_options = { :host => url.host,
|
24
|
-
:port => url.port,
|
25
|
-
:password => url.password,
|
26
|
-
:db => url.path.empty? ? nil : url.path,
|
27
|
-
:tcp_keepalive => 60}
|
28
|
-
redis = ::Redis.new(redis_options)
|
29
|
-
redis.client.connect
|
30
38
|
redis
|
31
39
|
end
|
32
40
|
|
33
|
-
def self.
|
34
|
-
|
35
|
-
|
41
|
+
def self.new_blocking_connection
|
42
|
+
# Remove the read/select loop in redis, it's weird and unecessary
|
36
43
|
new_connection.tap do |redis|
|
37
|
-
redis.
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def _read_from_socket(nbytes)
|
45
|
-
readpartial(nbytes)
|
44
|
+
redis.nodes.each do |node|
|
45
|
+
node.client.connection.instance_eval do
|
46
|
+
@sock.instance_eval do
|
47
|
+
def _read_from_socket(nbytes)
|
48
|
+
readpartial(nbytes)
|
49
|
+
end
|
46
50
|
end
|
47
51
|
end
|
48
52
|
end
|
@@ -55,29 +59,117 @@ module Promiscuous::Redis
|
|
55
59
|
|
56
60
|
def self.ensure_connected
|
57
61
|
Promiscuous::Redis.master.ping
|
58
|
-
rescue
|
62
|
+
rescue Exception
|
59
63
|
raise lost_connection_exception
|
60
64
|
end
|
61
65
|
|
62
|
-
|
63
|
-
|
64
|
-
|
66
|
+
class Script
|
67
|
+
def initialize(script)
|
68
|
+
@script = script
|
69
|
+
@sha = Digest::SHA1.hexdigest(@script)
|
70
|
+
end
|
65
71
|
|
66
|
-
|
67
|
-
|
72
|
+
def eval(redis, options={})
|
73
|
+
redis.evalsha(@sha, options)
|
74
|
+
rescue ::Redis::CommandError => e
|
75
|
+
if e.message =~ /^NOSCRIPT/
|
76
|
+
redis.script(:load, @script)
|
77
|
+
retry
|
78
|
+
end
|
79
|
+
raise e
|
80
|
+
end
|
68
81
|
end
|
69
82
|
|
70
|
-
|
71
|
-
|
72
|
-
|
83
|
+
class Mutex
|
84
|
+
def initialize(key, options={})
|
85
|
+
# TODO remove old code with orig_key
|
86
|
+
@orig_key = key.to_s
|
87
|
+
@key = "#{key}:lock"
|
88
|
+
@timeout = options[:timeout]
|
89
|
+
@sleep = options[:sleep]
|
90
|
+
@expire = options[:expire]
|
91
|
+
@lock_set = options[:lock_set]
|
92
|
+
@node = options[:node]
|
93
|
+
raise "Which node?" unless @node
|
94
|
+
end
|
73
95
|
|
74
|
-
|
75
|
-
|
76
|
-
|
96
|
+
def key
|
97
|
+
@orig_key
|
98
|
+
end
|
99
|
+
|
100
|
+
def node
|
101
|
+
@node
|
102
|
+
end
|
103
|
+
|
104
|
+
def lock
|
105
|
+
if @timeout > 0
|
106
|
+
# Blocking mode
|
107
|
+
result = false
|
108
|
+
start_at = Time.now
|
109
|
+
while Time.now - start_at < @timeout
|
110
|
+
break if result = try_lock
|
111
|
+
sleep @sleep
|
112
|
+
end
|
113
|
+
result
|
114
|
+
else
|
115
|
+
# Non-blocking mode
|
116
|
+
try_lock
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def try_lock
|
121
|
+
now = Time.now.to_i
|
122
|
+
@expires_at = now + @expire + 1
|
123
|
+
@token = Random.rand(1000000000)
|
124
|
+
|
125
|
+
# This script loading is not thread safe (touching a class variable), but
|
126
|
+
# that's okay, because the race is harmless.
|
127
|
+
@@lock_script ||= Promiscuous::Redis::Script.new <<-SCRIPT
|
128
|
+
local key = KEYS[1]
|
129
|
+
local lock_set = KEYS[2]
|
130
|
+
local now = tonumber(ARGV[1])
|
131
|
+
local orig_key = ARGV[2]
|
132
|
+
local expires_at = tonumber(ARGV[3])
|
133
|
+
local token = ARGV[4]
|
134
|
+
local lock_value = expires_at .. ':' .. token
|
135
|
+
local old_value = redis.call('get', key)
|
136
|
+
|
137
|
+
if old_value and tonumber(old_value:match("([^:]*):"):rep(1)) > now then return false end
|
138
|
+
redis.call('set', key, lock_value)
|
139
|
+
if lock_set then redis.call('zadd', lock_set, now, orig_key) end
|
140
|
+
|
141
|
+
if old_value then return 'recovered' else return true end
|
142
|
+
SCRIPT
|
143
|
+
result = @@lock_script.eval(@node, :keys => [@key, @lock_set], :argv => [now, @orig_key, @expires_at, @token])
|
144
|
+
return :recovered if result == 'recovered'
|
145
|
+
!!result
|
146
|
+
end
|
147
|
+
|
148
|
+
def unlock
|
149
|
+
# Since it's possible that the operations in the critical section took a long time,
|
150
|
+
# we can't just simply release the lock. The unlock method checks if @expires_at
|
151
|
+
# remains the same, and do not release when the lock timestamp was overwritten.
|
152
|
+
@@unlock_script ||= Promiscuous::Redis::Script.new <<-SCRIPT
|
153
|
+
local key = KEYS[1]
|
154
|
+
local lock_set = KEYS[2]
|
155
|
+
local orig_key = ARGV[1]
|
156
|
+
local expires_at = ARGV[2]
|
157
|
+
local token = ARGV[3]
|
158
|
+
local lock_value = expires_at .. ':' .. token
|
159
|
+
|
160
|
+
if redis.call('get', key) == lock_value then
|
161
|
+
redis.call('del', key)
|
162
|
+
if lock_set then redis.call('zrem', lock_set, orig_key) end
|
163
|
+
return true
|
164
|
+
else
|
165
|
+
return false
|
166
|
+
end
|
167
|
+
SCRIPT
|
168
|
+
@@unlock_script.eval(@node, :keys => [@key, @lock_set], :argv => [@orig_key, @expires_at, @token])
|
77
169
|
end
|
78
170
|
|
79
|
-
def
|
80
|
-
|
171
|
+
def still_locked?
|
172
|
+
@node.get(@key) == "#{@expires_at}:#{@token}"
|
81
173
|
end
|
82
174
|
end
|
83
175
|
end
|