redis-objects 0.2.2 → 0.2.3

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/ChangeLog CHANGED
@@ -1,4 +1,12 @@
1
1
 
2
+ *0.2.3 [Final] (18 February 2010)*
3
+
4
+ * Added lock expiration to Redis::Lock [Ben VandenBos]
5
+
6
+ * Fixed some bugs [Ben VandenBos]
7
+
8
+ * Added lock tests and test helpers [Ben VandenBos]
9
+
2
10
  *0.2.2 [Final] (14 December 2009)*
3
11
 
4
12
  * Added @set.diff(@set2) with "^" and "-" synonyms (oversight). [Nate Wiger]
data/README.rdoc CHANGED
@@ -23,23 +23,105 @@ or by using +new+ with the type of data structure you want to create.
23
23
  gem tumble
24
24
  gem install redis-objects
25
25
 
26
- == Example 1: Standalone Usage
26
+ == Example 1: Model Class Usage
27
+
28
+ Using Redis::Objects this way makes it trivial to integrate Redis types with an
29
+ existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
30
+ will work with _any_ class that provides an +id+ method that returns a unique
31
+ value. Redis::Objects will automatically create keys that are unique to
32
+ each object.
33
+
34
+ === Initialization
35
+
36
+ Redis::Objects needs a handle created by Redis.new. If you're on Rails,
37
+ config/initializers/redis.rb is a good place for this:
38
+
39
+ require 'redis'
40
+ require 'redis/objects'
41
+ Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
42
+
43
+ You can use Redis::Objects with any framework. There are *no* dependencies on Rails.
44
+ I use it from Sinatra and rake tasks all the time.
45
+
46
+ === Model Class
47
+
48
+ Include Redis::Objects in any type of class:
49
+
50
+ class Team < ActiveRecord::Base
51
+ include Redis::Objects
52
+
53
+ counter :hits
54
+ counter :runs
55
+ counter :outs
56
+ counter :inning, :start => 1
57
+ list :on_base
58
+ set :outfielders
59
+ value :at_bat
60
+ end
61
+
62
+ Familiar Ruby array operations Just Work (TM):
63
+
64
+ @team = Team.find_by_name('New York Yankees')
65
+ @team.on_base << 'player1'
66
+ @team.on_base << 'player2'
67
+ @team.on_base << 'player3'
68
+ @team.on_base # ['player1', 'player2', 'player3']
69
+ @team.on_base.pop
70
+ @team.on_base.shift
71
+ @team.on_base.length # 1
72
+ @team.on_base.delete('player2')
73
+
74
+ Sets work too:
75
+
76
+ @team.outfielders << 'outfielder1'
77
+ @team.outfielders << 'outfielder2'
78
+ @team.outfielders << 'outfielder1' # dup ignored
79
+ @team.outfielders # ['outfielder1', 'outfielder2']
80
+ @team.outfielders.each do |player|
81
+ puts player
82
+ end
83
+ player = @team.outfielders.detect{|of| of == 'outfielder2'}
84
+
85
+ And you can do intersections between ORM objects (kinda cool):
86
+
87
+ @team1.outfielders | @team2.outfielders # outfielders on both teams
88
+ @team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
89
+
90
+ Counters can be atomically incremented/decremented (but not assigned):
91
+
92
+ @team.hits.increment # or incr
93
+ @team.hits.decrement # or decr
94
+ @team.hits.incr(3) # add 3
95
+ @team.runs = 4 # exception
96
+
97
+ Finally, for free, you get a +redis+ method that points directly to a Redis connection:
98
+
99
+ Team.redis.get('somekey')
100
+ @team = Team.new
101
+ @team.redis.get('somekey')
102
+ @team.redis.smembers('someset')
103
+
104
+ You can use the +redis+ handle to directly call any {Redis command}[http://code.google.com/p/redis/wiki/CommandReference]
105
+
106
+ == Example 2: Standalone Usage
27
107
 
28
108
  There is a Ruby object that maps to each Redis type.
29
109
 
30
110
  === Initialization
31
111
 
32
- This gem needs a handle to the +redis+ server. For standalone use, you can
33
- either set the $redis global variable to your Redis.new handle:
112
+ Again, Redis::Objects needs a handle to the +redis+ server. For standalone use, you
113
+ can either set the $redis global variable:
34
114
 
35
115
  $redis = Redis.new(:host => 'localhost', :port => 6379)
36
116
  @value = Redis::Value.new('myvalue')
37
117
 
38
- Or you can pass the Redis handle into the new method:
118
+ Or you can pass the Redis handle into the new method for each type:
39
119
 
40
120
  redis = Redis.new(:host => 'localhost', :port => 6379)
41
121
  @value = Redis::Value.new('myvalue', redis)
42
122
 
123
+ Your choice.
124
+
43
125
  === Counters
44
126
 
45
127
  Create a new counter. The +counter_name+ is the key stored in Redis.
@@ -111,8 +193,8 @@ You can perform Redis intersections/unions/diffs easily:
111
193
  members = @set1 & @set2 # intersection
112
194
  members = @set1 | @set2 # union
113
195
  members = @set1 + @set2 # union
114
- members = @set1 ^ @set2 # union
115
- members = @set1 - @set2 # union
196
+ members = @set1 ^ @set2 # difference
197
+ members = @set1 - @set2 # difference
116
198
  members = @set1.intersection(@set2, @set3) # multiple
117
199
  members = @set1.union(@set2, @set3) # multiple
118
200
  members = @set1.difference(@set2, @set3) # multiple
@@ -152,80 +234,6 @@ Of course complex data is no problem:
152
234
  @newest = Redis::Value.new('newest_account')
153
235
  @newest.value = @account
154
236
 
155
- == Example 2: Model Class Usage
156
-
157
- Using Redis::Objects this way makes it trivial to integrate Redis types with an
158
- existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
159
- will work with _any_ class that provides an +id+ method that returns a unique
160
- value. Redis::Objects will automatically create keys that are unique to
161
- each object.
162
-
163
- === Initialization
164
-
165
- If on Rails, config/initializers/redis.rb is a good place for this:
166
-
167
- require 'redis'
168
- require 'redis/objects'
169
- Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
170
-
171
- === Model Class
172
-
173
- Include Redis::Objects in any type of class:
174
-
175
- class Team < ActiveRecord::Base
176
- include Redis::Objects
177
-
178
- counter :hits
179
- counter :runs
180
- counter :outs
181
- counter :inning, :start => 1
182
- list :on_base
183
- set :outfielders
184
- value :at_bat
185
- end
186
-
187
- Familiar Ruby array operations Just Work (TM):
188
-
189
- @team = Team.find_by_name('New York Yankees')
190
- @team.on_base << 'player1'
191
- @team.on_base << 'player2'
192
- @team.on_base << 'player3'
193
- @team.on_base # ['player1', 'player2']
194
- @team.on_base.pop
195
- @team.on_base.shift
196
- @team.on_base.length # 1
197
- @team.on_base.delete('player3')
198
-
199
- Sets work too:
200
-
201
- @team.outfielders << 'outfielder1' << 'outfielder1'
202
- @team.outfielders << 'outfielder2'
203
- @team.outfielders # ['outfielder1', 'outfielder2']
204
- @team.outfielders.each do |player|
205
- puts player
206
- end
207
- player = @team.outfielders.detect{|of| of == 'outfielder2'}
208
-
209
- And you can do intersections between ORM objects (kinda cool):
210
-
211
- @team1.outfielders | @team2.outfielders # all outfielders
212
- @team1.outfielders & @team2.outfielders # should be empty
213
-
214
- Counters can be atomically incremented/decremented (but not assigned):
215
-
216
- @team.hits.increment # or incr
217
- @team.hits.decrement # or decr
218
- @team.hits.incr(3) # add 3
219
- @team.runs = 4 # exception
220
-
221
- Finally, for free, you get a +redis+ handle usable in your class that
222
- points directly to a Redis API object:
223
-
224
- @team.redis.get('somekey')
225
- @team.redis.smembers('someset')
226
-
227
- You can use the +redis+ handle to directly call any {Redis command}[http://code.google.com/p/redis/wiki/CommandReference]
228
-
229
237
  == Atomic Counters and Locks
230
238
 
231
239
  You are probably not handling atomicity correctly in your app. For a fun rant
@@ -277,6 +285,10 @@ Class-level atomic block (may save a DB fetch depending on your app):
277
285
 
278
286
  Locks with Redis. On completion or exception the lock is released:
279
287
 
288
+ class Team < ActiveRecord::Base
289
+ lock :reorder # declare a lock
290
+ end
291
+
280
292
  @team.reorder_lock.lock do
281
293
  @team.reorder_all_players
282
294
  end
@@ -287,9 +299,19 @@ Class-level lock (same concept)
287
299
  Team.reorder_all_players(team_id)
288
300
  end
289
301
 
302
+ Lock expiration. Sometimes you want to make sure your locks are cleaned up should
303
+ the unthinkable happen (server failure). You can set lock expirations to handle
304
+ this. Expired locks are released by the next process to attempt lock. Just
305
+ make sure you expiration value is sufficiently large compared to your expected
306
+ lock time.
307
+
308
+ class Team < ActiveRecord::Base
309
+ lock :reorder, :expiration => 15.minutes
310
+ end
311
+
290
312
 
291
313
  == Author
292
314
 
293
- Copyright (c) 2009 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
315
+ Copyright (c) 2009-2010 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
294
316
  Released under the {Artistic License}[http://www.opensource.org/licenses/artistic-license-2.0.php].
295
317
 
data/lib/redis/lock.rb CHANGED
@@ -31,17 +31,53 @@ class Redis
31
31
  def lock(&block)
32
32
  start = Time.now
33
33
  gotit = false
34
+ expiration = nil
34
35
  while Time.now - start < @options[:timeout]
35
- gotit = redis.setnx(key, 1)
36
+ expiration = generate_expiration
37
+ # Use the expiration as the value of the lock.
38
+ gotit = redis.setnx(key, expiration)
36
39
  break if gotit
40
+
41
+ # Lock is being held. Now check to see if it's expired (if we're using
42
+ # lock expiration).
43
+ # See "Handling Deadlocks" section on http://code.google.com/p/redis/wiki/SetnxCommand
44
+ if !@options[:expiration].nil?
45
+ old_expiration = redis.get(key).to_f
46
+
47
+ if old_expiration < Time.now.to_f
48
+ # If it's expired, use GETSET to update it.
49
+ expiration = generate_expiration
50
+ old_expiration = redis.getset(key, expiration).to_f
51
+
52
+ # Since GETSET returns the old value of the lock, if the old expiration
53
+ # is still in the past, we know no one else has expired the locked
54
+ # and we now have it.
55
+ if old_expiration < Time.now.to_f
56
+ gotit = true
57
+ break
58
+ end
59
+ end
60
+ end
61
+
37
62
  sleep 0.1
38
63
  end
39
64
  raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec" unless gotit
40
65
  begin
41
66
  yield
42
67
  ensure
43
- redis.del(key)
68
+ # We need to be careful when cleaning up the lock key. If we took a really long
69
+ # time for some reason, and the lock expired, someone else may have it, and
70
+ # it's not safe for us to remove it. Check how much time has passed since we
71
+ # wrote the lock key and only delete it if it hasn't expired (or we're not using
72
+ # lock expiration)
73
+ if @options[:expiration].nil? || expiration > Time.now.to_f
74
+ redis.del(key)
75
+ end
44
76
  end
45
77
  end
78
+
79
+ def generate_expiration
80
+ @options[:expiration].nil? ? 1 : (Time.now + @options[:expiration].to_f + 1).to_f
81
+ end
46
82
  end
47
83
  end
@@ -16,6 +16,7 @@ class Redis
16
16
  # so it can be used alongside ActiveRecord/DataMapper, etc.
17
17
  def lock(name, options={})
18
18
  options[:timeout] ||= 5 # seconds
19
+ options[:init] = false if options[:init].nil? # default :init to false
19
20
  @redis_objects[name] = options.merge(:type => :lock)
20
21
  if options[:global]
21
22
  instance_eval <<-EndMethods
@@ -47,7 +48,7 @@ class Redis
47
48
  verify_lock_defined!(name)
48
49
  raise ArgumentError, "Missing block to #{self.name}.obtain_lock" unless block_given?
49
50
  lock_name = field_key("#{name}_lock", id)
50
- Redis::Lock.new(redis, lock_name, self.class.redis_objects[name]).lock(&block)
51
+ Redis::Lock.new(lock_name, redis, self.redis_objects[name]).lock(&block)
51
52
  end
52
53
 
53
54
  # Clear the lock. Use with care - usually only in an Admin page to clear
@@ -5,6 +5,7 @@ require 'redis/counter'
5
5
  require 'redis/list'
6
6
  require 'redis/set'
7
7
  require 'redis/value'
8
+ require 'redis/lock'
8
9
 
9
10
  describe Redis::Value do
10
11
  before :all do
@@ -329,4 +330,91 @@ describe Redis::Counter do
329
330
  after :all do
330
331
  @counter.delete
331
332
  end
333
+ end
334
+
335
+ describe Redis::Lock do
336
+
337
+ before :each do
338
+ $redis.flushall
339
+ end
340
+
341
+ it "should set the value to the expiration" do
342
+ start = Time.now
343
+ expiry = 15
344
+ lock = Redis::Lock.new(:test_lock, $redis, :expiration => expiry, :init => false)
345
+ lock.lock do
346
+ expiration = $redis.get("test_lock").to_f
347
+
348
+ # The expiration stored in redis should be 15 seconds from when we started
349
+ # or a little more
350
+ expiration.should be_close((start + expiry).to_f, 2.0)
351
+ end
352
+
353
+ # key should have been cleaned up
354
+ $redis.get("test_lock").should be_nil
355
+ end
356
+
357
+ it "should set value to 1 when no expiration is set" do
358
+ lock = Redis::Lock.new(:test_lock, $redis, :init => false)
359
+ lock.lock do
360
+ $redis.get('test_lock').should == '1'
361
+ end
362
+
363
+ # key should have been cleaned up
364
+ $redis.get("test_lock").should be_nil
365
+ end
366
+
367
+ it "should let lock be gettable when lock is expired" do
368
+ expiry = 15
369
+ lock = Redis::Lock.new(:test_lock, $redis, :expiration => expiry, :timeout => 0.1, :init => false)
370
+
371
+ # create a fake lock in the past
372
+ $redis.set("test_lock", Time.now-(expiry + 60))
373
+
374
+ gotit = false
375
+ lock.lock do
376
+ gotit = true
377
+ end
378
+
379
+ # should get the lock because it has expired
380
+ gotit.should be_true
381
+ $redis.get("test_lock").should be_nil
382
+ end
383
+
384
+ it "should not let non-expired locks be gettable" do
385
+ expiry = 15
386
+ lock = Redis::Lock.new(:test_lock, $redis, :expiration => expiry, :timeout => 0.1, :init => false)
387
+
388
+ # create a fake lock
389
+ $redis.set("test_lock", (Time.now + expiry).to_f)
390
+
391
+ gotit = false
392
+ error = nil
393
+ begin
394
+ lock.lock do
395
+ gotit = true
396
+ end
397
+ rescue => error
398
+ end
399
+
400
+ error.should be_kind_of(Redis::Lock::LockTimeout)
401
+
402
+ # should not have the lock
403
+ gotit.should_not be_true
404
+
405
+ # lock value should still be set
406
+ $redis.get("test_lock").should_not be_nil
407
+ end
408
+
409
+ it "should not remove the key if lock is held past expiration" do
410
+ lock = Redis::Lock.new(:test_lock, $redis, :expiration => 0.0, :init => false)
411
+
412
+ lock.lock do
413
+ sleep 1.1
414
+ end
415
+
416
+ # lock value should still be set since the lock was held for more than the expiry
417
+ $redis.get("test_lock").should_not be_nil
418
+ end
419
+
332
420
  end
@@ -282,6 +282,19 @@ describe Redis::Objects do
282
282
  end
283
283
  error.should be_kind_of(NoMethodError)
284
284
  end
285
+
286
+ it "should support obtain_lock as a class method" do
287
+ error = nil
288
+ begin
289
+ Roster.obtain_lock(:resort, 2) do
290
+ Roster.redis.get("roster:2:resort_lock").should_not be_nil
291
+ end
292
+ rescue => error
293
+ end
294
+
295
+ error.should be_nil
296
+ Roster.redis.get("roster:2:resort_lock").should be_nil
297
+ end
285
298
 
286
299
  it "should handle simple values" do
287
300
  @roster.starting_pitcher.should == nil
@@ -635,4 +648,5 @@ describe Redis::Objects do
635
648
  error.should_not be_nil
636
649
  error.should be_kind_of(Redis::Lock::LockTimeout)
637
650
  end
651
+
638
652
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Wiger
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-14 00:00:00 -08:00
12
+ date: 2010-02-18 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency