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 +8 -0
- data/README.rdoc +103 -81
- data/lib/redis/lock.rb +38 -2
- data/lib/redis/objects/locks.rb +2 -1
- data/spec/redis_objects_instance_spec.rb +88 -0
- data/spec/redis_objects_model_spec.rb +14 -0
- metadata +2 -2
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:
|
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
|
-
|
33
|
-
either set the $redis global variable
|
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 #
|
115
|
-
members = @set1 - @set2 #
|
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
|
-
|
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
|
-
|
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
|
data/lib/redis/objects/locks.rb
CHANGED
@@ -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(
|
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.
|
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:
|
12
|
+
date: 2010-02-18 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|