redis-objects 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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