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