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,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
|