promiscuous 0.53.1 → 0.90.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. data/lib/promiscuous.rb +25 -28
  2. data/lib/promiscuous/amqp.rb +27 -8
  3. data/lib/promiscuous/amqp/bunny.rb +131 -16
  4. data/lib/promiscuous/amqp/fake.rb +52 -0
  5. data/lib/promiscuous/amqp/hot_bunnies.rb +56 -0
  6. data/lib/promiscuous/amqp/null.rb +6 -6
  7. data/lib/promiscuous/cli.rb +108 -24
  8. data/lib/promiscuous/config.rb +73 -12
  9. data/lib/promiscuous/convenience.rb +18 -0
  10. data/lib/promiscuous/dependency.rb +59 -0
  11. data/lib/promiscuous/dsl.rb +36 -0
  12. data/lib/promiscuous/error.rb +3 -1
  13. data/lib/promiscuous/error/already_processed.rb +5 -0
  14. data/lib/promiscuous/error/base.rb +1 -0
  15. data/lib/promiscuous/error/connection.rb +7 -5
  16. data/lib/promiscuous/error/dependency.rb +111 -0
  17. data/lib/promiscuous/error/lock_unavailable.rb +12 -0
  18. data/lib/promiscuous/error/lost_lock.rb +12 -0
  19. data/lib/promiscuous/error/missing_context.rb +29 -0
  20. data/lib/promiscuous/error/publisher.rb +5 -15
  21. data/lib/promiscuous/error/recovery.rb +7 -0
  22. data/lib/promiscuous/error/subscriber.rb +2 -4
  23. data/lib/promiscuous/key.rb +36 -0
  24. data/lib/promiscuous/loader.rb +12 -16
  25. data/lib/promiscuous/middleware.rb +112 -0
  26. data/lib/promiscuous/publisher.rb +7 -4
  27. data/lib/promiscuous/publisher/context.rb +92 -0
  28. data/lib/promiscuous/publisher/mock_generator.rb +72 -0
  29. data/lib/promiscuous/publisher/model.rb +3 -86
  30. data/lib/promiscuous/publisher/model/active_record.rb +8 -15
  31. data/lib/promiscuous/publisher/model/base.rb +136 -0
  32. data/lib/promiscuous/publisher/model/ephemeral.rb +69 -0
  33. data/lib/promiscuous/publisher/model/mock.rb +61 -0
  34. data/lib/promiscuous/publisher/model/mongoid.rb +57 -100
  35. data/lib/promiscuous/{common/lint.rb → publisher/operation.rb} +1 -1
  36. data/lib/promiscuous/publisher/operation/base.rb +707 -0
  37. data/lib/promiscuous/publisher/operation/mongoid.rb +370 -0
  38. data/lib/promiscuous/publisher/worker.rb +22 -0
  39. data/lib/promiscuous/railtie.rb +21 -3
  40. data/lib/promiscuous/redis.rb +132 -40
  41. data/lib/promiscuous/resque.rb +12 -0
  42. data/lib/promiscuous/sidekiq.rb +15 -0
  43. data/lib/promiscuous/subscriber.rb +9 -20
  44. data/lib/promiscuous/subscriber/model.rb +4 -104
  45. data/lib/promiscuous/subscriber/model/active_record.rb +10 -0
  46. data/lib/promiscuous/subscriber/model/base.rb +96 -0
  47. data/lib/promiscuous/subscriber/model/mongoid.rb +86 -0
  48. data/lib/promiscuous/subscriber/model/observer.rb +37 -0
  49. data/lib/promiscuous/subscriber/operation.rb +167 -0
  50. data/lib/promiscuous/subscriber/payload.rb +34 -0
  51. data/lib/promiscuous/subscriber/worker.rb +22 -18
  52. data/lib/promiscuous/subscriber/worker/message.rb +48 -25
  53. data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +273 -181
  54. data/lib/promiscuous/subscriber/worker/pump.rb +17 -43
  55. data/lib/promiscuous/subscriber/worker/recorder.rb +24 -0
  56. data/lib/promiscuous/subscriber/worker/runner.rb +24 -3
  57. data/lib/promiscuous/subscriber/worker/stats.rb +62 -0
  58. data/lib/promiscuous/timer.rb +38 -0
  59. data/lib/promiscuous/version.rb +1 -1
  60. metadata +98 -143
  61. data/README.md +0 -33
  62. data/lib/promiscuous/amqp/ruby_amqp.rb +0 -140
  63. data/lib/promiscuous/common.rb +0 -4
  64. data/lib/promiscuous/common/class_helpers.rb +0 -12
  65. data/lib/promiscuous/common/lint/base.rb +0 -24
  66. data/lib/promiscuous/common/options.rb +0 -51
  67. data/lib/promiscuous/ephemeral.rb +0 -14
  68. data/lib/promiscuous/error/recover.rb +0 -1
  69. data/lib/promiscuous/observer.rb +0 -5
  70. data/lib/promiscuous/publisher/active_record.rb +0 -7
  71. data/lib/promiscuous/publisher/amqp.rb +0 -18
  72. data/lib/promiscuous/publisher/attributes.rb +0 -32
  73. data/lib/promiscuous/publisher/base.rb +0 -23
  74. data/lib/promiscuous/publisher/class.rb +0 -36
  75. data/lib/promiscuous/publisher/envelope.rb +0 -7
  76. data/lib/promiscuous/publisher/ephemeral.rb +0 -9
  77. data/lib/promiscuous/publisher/lint.rb +0 -35
  78. data/lib/promiscuous/publisher/lint/amqp.rb +0 -14
  79. data/lib/promiscuous/publisher/lint/attributes.rb +0 -12
  80. data/lib/promiscuous/publisher/lint/base.rb +0 -5
  81. data/lib/promiscuous/publisher/lint/class.rb +0 -15
  82. data/lib/promiscuous/publisher/lint/polymorphic.rb +0 -22
  83. data/lib/promiscuous/publisher/mock.rb +0 -79
  84. data/lib/promiscuous/publisher/mongoid.rb +0 -33
  85. data/lib/promiscuous/publisher/mongoid/embedded.rb +0 -27
  86. data/lib/promiscuous/publisher/mongoid/embedded_many.rb +0 -12
  87. data/lib/promiscuous/publisher/polymorphic.rb +0 -8
  88. data/lib/promiscuous/subscriber/active_record.rb +0 -11
  89. data/lib/promiscuous/subscriber/amqp.rb +0 -25
  90. data/lib/promiscuous/subscriber/attributes.rb +0 -35
  91. data/lib/promiscuous/subscriber/base.rb +0 -29
  92. data/lib/promiscuous/subscriber/class.rb +0 -29
  93. data/lib/promiscuous/subscriber/dummy.rb +0 -19
  94. data/lib/promiscuous/subscriber/envelope.rb +0 -18
  95. data/lib/promiscuous/subscriber/lint.rb +0 -30
  96. data/lib/promiscuous/subscriber/lint/amqp.rb +0 -21
  97. data/lib/promiscuous/subscriber/lint/attributes.rb +0 -21
  98. data/lib/promiscuous/subscriber/lint/base.rb +0 -14
  99. data/lib/promiscuous/subscriber/lint/class.rb +0 -13
  100. data/lib/promiscuous/subscriber/lint/polymorphic.rb +0 -39
  101. data/lib/promiscuous/subscriber/mongoid.rb +0 -27
  102. data/lib/promiscuous/subscriber/mongoid/embedded.rb +0 -17
  103. data/lib/promiscuous/subscriber/mongoid/embedded_many.rb +0 -44
  104. data/lib/promiscuous/subscriber/observer.rb +0 -26
  105. data/lib/promiscuous/subscriber/polymorphic.rb +0 -36
  106. 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
@@ -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::Loader.load_descriptors(:publishers)
8
+ Promiscuous::Config.configure unless Promiscuous::Config.configured?
9
+ Promiscuous::Loader.prepare
10
+
5
11
  ActionDispatch::Reloader.to_prepare do
6
- Promiscuous::Loader.load_descriptors
12
+ Promiscuous::Loader.prepare
7
13
  end
8
14
  ActionDispatch::Reloader.to_cleanup do
9
- Promiscuous::Loader.unload_descriptors
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
@@ -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.client.disconnect if 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
- return Null.new if Promiscuous::Config.backend == :null
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
- redis_url = Promiscuous::Config.redis_url || 'redis://localhost/'
20
- url = URI.parse(redis_url)
21
- raise "Please use redis://:password@host:port/db" if url.scheme != 'redis'
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.new_celluloid_connection
34
- return Null.new if Promiscuous::Config.backend == :null
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.client.connection.instance_eval do
38
- @sock = Celluloid::IO::TCPSocket.from_ruby_socket(@sock)
39
- @sock.instance_eval do
40
- extend ::Redis::Connection::SocketMixin
41
- @timeout = nil
42
- @buffer = ""
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
- def self.method_missing(name, *args, &block)
63
- self.master.__send__(name, *args, &block)
64
- end
66
+ class Script
67
+ def initialize(script)
68
+ @script = script
69
+ @sha = Digest::SHA1.hexdigest(@script)
70
+ end
65
71
 
66
- def self.pub_key(str)
67
- "publishers:#{Promiscuous::Config.app}:#{str}"
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
- def self.sub_key(str)
71
- "subscribers:#{Promiscuous::Config.app}:#{str}"
72
- end
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
- class Null
75
- def client
76
- return self.class.new
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 method_missing(name, *args, &block)
80
- 0
171
+ def still_locked?
172
+ @node.get(@key) == "#{@expires_at}:#{@token}"
81
173
  end
82
174
  end
83
175
  end