redis-objects 0.2.1 → 0.2.4

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 ADDED
@@ -0,0 +1,23 @@
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
+
10
+ *0.2.2 [Final] (14 December 2009)*
11
+
12
+ * Added @set.diff(@set2) with "^" and "-" synonyms (oversight). [Nate Wiger]
13
+
14
+ * Implemented Redis core commands in all data types, such as rename. [Nate Wiger]
15
+
16
+ * Renamed Redis::Serialize to Redis::Helpers::Serialize to keep Redis:: cleaner. [Nate Wiger]
17
+
18
+ * More spec coverage. [Nate Wiger]
19
+
20
+ *0.2.1 [Final] (27 November 2009)*
21
+
22
+ * First worthwhile public release, with good spec coverage and functionality. [Nate Wiger]
23
+
data/README.rdoc CHANGED
@@ -1,48 +1,142 @@
1
1
  = Redis::Objects - Map Redis types directly to Ruby objects
2
2
 
3
- This is *not* an ORM. People that are wrapping ORM's around Redis are missing
4
- the point.
3
+ This is *not* an ORM. People that are wrapping ORMs around Redis are missing the point.
5
4
 
6
- The killer feature of Redis that it allows you to perform atomic operations
7
- on _individual_ data structures, like counters, lists, and sets. You can then use
8
- these *with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
9
- nothing to do with an ORM or even a database. This gem maps Ezra's +redis+ library
10
- to Ruby objects to make use seamless.
5
+ The killer feature of Redis is that it allows you to perform _atomic_ operations
6
+ on _individual_ data structures, like counters, lists, and sets. The *atomic* part is HUGE.
7
+ Using an ORM wrapper that retrieves a "record", updates values, then sends those values back,
8
+ _removes_ the atomicity, cutting the nuts off the major advantage of Redis. Just use MySQL, k?
9
+
10
+ This gem provides a Rubyish interface to Redis, by mapping {Redis types}[http://code.google.com/p/redis/wiki/CommandReference]
11
+ to Ruby objects, via a thin layer over Ezra's +redis+ gem. It offers several advantages
12
+ over the lower-level redis-rb API:
13
+
14
+ 1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc. Add counters to your model!
15
+ 2. Complex data structures are automatically Marshaled
16
+ 3. Integers are returned as integers, rather than '17'
17
+ 4. Higher-level types are provided, such as Locks, that wrap multiple calls
11
18
 
12
19
  This gem originally arose out of a need for high-concurrency atomic operations;
13
- for a fun rant on the topic, see
14
- {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc],
20
+ for a fun rant on the topic, see {An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant],
15
21
  or scroll down to "Atomicity" in this README.
16
22
 
17
- There are two ways to use Redis::Objects, either as an +include+ in a model class,
18
- or by using +new+ with the type of data structure you want to create.
23
+ There are two ways to use Redis::Objects, either as an include in a model class (to
24
+ integrate with ORMs or other classes), or by using new with the type of data structure
25
+ you want to create.
19
26
 
20
27
  == Installation
21
28
 
22
- gem install gemcutter
23
- gem tumble
24
29
  gem install redis-objects
25
30
 
26
- == Example 1: Individual Usage
31
+ == Example 1: Model Class Usage
32
+
33
+ Using Redis::Objects this way makes it trivial to integrate Redis types with an
34
+ existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
35
+ will work with _any_ class that provides an +id+ method that returns a unique
36
+ value. Redis::Objects will automatically create keys that are unique to
37
+ each object, in the format:
27
38
 
28
- There is a Ruby object that maps to each Redis type.
39
+ model_name:id:field_name
29
40
 
30
41
  === Initialization
31
42
 
32
- You can either set the $redis global variable to your Redis handle:
43
+ Redis::Objects needs a handle created by Redis.new. (If you're on Rails,
44
+ config/initializers/redis.rb is a good place for this.)
45
+
46
+ require 'redis'
47
+ require 'redis/objects'
48
+ Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
49
+
50
+ Remember you can use Redis::Objects in any Ruby code. There are *no* dependencies
51
+ on Rails. Standalone, Sinatra, Resque - no problem.
52
+
53
+ === Model Class
54
+
55
+ You can include Redis::Objects in any type of class:
56
+
57
+ class Team < ActiveRecord::Base
58
+ include Redis::Objects
59
+
60
+ lock :trade_players, :expiration => 15 # sec
61
+ counter :hits
62
+ counter :runs
63
+ counter :outs
64
+ counter :inning, :start => 1
65
+ list :on_base
66
+ set :outfielders
67
+ value :at_bat
68
+ end
69
+
70
+ Familiar Ruby array operations Just Work (TM):
71
+
72
+ @team = Team.find_by_name('New York Yankees')
73
+ @team.on_base << 'player1'
74
+ @team.on_base << 'player2'
75
+ @team.on_base << 'player3'
76
+ @team.on_base # ['player1', 'player2', 'player3']
77
+ @team.on_base.pop
78
+ @team.on_base.shift
79
+ @team.on_base.length # 1
80
+ @team.on_base.delete('player2')
81
+
82
+ Sets work too:
83
+
84
+ @team.outfielders << 'outfielder1'
85
+ @team.outfielders << 'outfielder2'
86
+ @team.outfielders << 'outfielder1' # dup ignored
87
+ @team.outfielders # ['outfielder1', 'outfielder2']
88
+ @team.outfielders.each do |player|
89
+ puts player
90
+ end
91
+ player = @team.outfielders.detect{|of| of == 'outfielder2'}
92
+
93
+ And you can do intersections between objects (kinda cool):
94
+
95
+ @team1.outfielders | @team2.outfielders # outfielders on both teams
96
+ @team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
97
+
98
+ Counters can be atomically incremented/decremented (but not assigned):
99
+
100
+ @team.hits.increment # or incr
101
+ @team.hits.decrement # or decr
102
+ @team.hits.incr(3) # add 3
103
+ @team.runs = 4 # exception
104
+
105
+ Finally, for free, you get a +redis+ method that points directly to a Redis connection:
106
+
107
+ Team.redis.get('somekey')
108
+ @team = Team.new
109
+ @team.redis.get('somekey')
110
+ @team.redis.smembers('someset')
111
+
112
+ You can use the +redis+ handle to directly call any {Redis API command}[http://code.google.com/p/redis/wiki/CommandReference].
113
+
114
+ == Example 2: Standalone Usage
115
+
116
+ There is a Ruby class that maps to each Redis type, with methods for each
117
+ {Redis API command}[http://code.google.com/p/redis/wiki/CommandReference].
118
+ Note that calling +new+ does not imply it's actually a "new" value - it just
119
+ creates a mapping between that object and the corresponding Redis data structure,
120
+ which may already exist on the redis-server.
121
+
122
+ === Initialization
123
+
124
+ Redis::Objects needs a handle to the +redis+ server. For standalone use, you
125
+ can either set the $redis global variable:
33
126
 
34
127
  $redis = Redis.new(:host => 'localhost', :port => 6379)
35
- @value = Redis::Value.new('myvalue')
128
+ @list = Redis::List.new('mylist')
36
129
 
37
- Or you can pass the Redis handle into the new method:
130
+ Or you can pass the Redis handle into the new method for each type:
38
131
 
39
132
  redis = Redis.new(:host => 'localhost', :port => 6379)
40
- @value = Redis::Value.new('myvalue', redis)
133
+ @list = Redis::List.new('mylist', redis)
41
134
 
42
135
  === Counters
43
136
 
44
137
  Create a new counter. The +counter_name+ is the key stored in Redis.
45
138
 
139
+ require 'redis/counter'
46
140
  @counter = Redis::Counter.new('counter_name')
47
141
  @counter.increment
48
142
  @counter.decrement
@@ -56,10 +150,39 @@ This gem provides a clean way to do atomic blocks as well:
56
150
 
57
151
  See the section on "Atomicity" for cool uses of atomic counter blocks.
58
152
 
153
+ === Locks
154
+
155
+ A convenience class that wraps the pattern of {using +setnx+ to perform locking}[http://code.google.com/p/redis/wiki/SetnxCommand].
156
+
157
+ require 'redis/lock'
158
+ @lock = Redis::Lock.new('image_resizing', :expiration => 15, :timeout => 0.1)
159
+ @lock.lock do
160
+ # do work
161
+ end
162
+
163
+ This can be especially useful if you're running batch jobs spread across multiple hosts.
164
+
165
+ === Values
166
+
167
+ Simple values are easy as well:
168
+
169
+ require 'redis/value'
170
+ @value = Redis::Value.new('value_name')
171
+ @value.value = 'a'
172
+ @value.delete
173
+
174
+ Of course complex data is no problem:
175
+
176
+ @account = Account.create!(params[:account])
177
+ @newest = Redis::Value.new('newest_account')
178
+ @newest.value = @account.attributes
179
+ puts @newest.value['username']
180
+
59
181
  === Lists
60
182
 
61
183
  Lists work just like Ruby arrays:
62
184
 
185
+ require 'redis/list'
63
186
  @list = Redis::List.new('list_name')
64
187
  @list << 'a'
65
188
  @list << 'b'
@@ -82,11 +205,14 @@ Complex data types are no problem:
82
205
  @list.each do |el|
83
206
  puts "#{el[:name]} lives in #{el[:city]}"
84
207
  end
208
+
209
+ This gem will automatically marshal complex data, similar to how session stores work.
85
210
 
86
211
  === Sets
87
212
 
88
213
  Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
89
214
 
215
+ require 'redis/set'
90
216
  @set = Redis::Set.new('set_name')
91
217
  @set << 'a'
92
218
  @set << 'b'
@@ -99,7 +225,7 @@ Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
99
225
  @set.clear
100
226
  # etc
101
227
 
102
- You can perform Redis intersections/unions easily:
228
+ You can perform Redis intersections/unions/diffs easily:
103
229
 
104
230
  @set1 = Redis::Set.new('set1')
105
231
  @set2 = Redis::Set.new('set2')
@@ -107,8 +233,11 @@ You can perform Redis intersections/unions easily:
107
233
  members = @set1 & @set2 # intersection
108
234
  members = @set1 | @set2 # union
109
235
  members = @set1 + @set2 # union
236
+ members = @set1 ^ @set2 # difference
237
+ members = @set1 - @set2 # difference
110
238
  members = @set1.intersection(@set2, @set3) # multiple
111
239
  members = @set1.union(@set2, @set3) # multiple
240
+ members = @set1.difference(@set2, @set3) # multiple
112
241
 
113
242
  Or store them in Redis:
114
243
 
@@ -116,6 +245,8 @@ Or store them in Redis:
116
245
  members = @set1.redis.get('intername')
117
246
  @set1.unionstore('unionname', @set2, @set3)
118
247
  members = @set1.redis.get('unionname')
248
+ @set1.diffstore('diffname', @set2, @set3)
249
+ members = @set1.redis.get('diffname')
119
250
 
120
251
  And use complex data types too:
121
252
 
@@ -125,95 +256,43 @@ And use complex data types too:
125
256
  @set2 << {:name => "Jeff", :city => "Del Mar"}
126
257
 
127
258
  @set1 & @set2 # Nate
259
+ @set1 - @set2 # Peter
128
260
  @set1 | @set2 # all 3 people
129
261
 
130
- === Values
131
-
132
- Simple values are easy as well:
133
-
134
- @value = Redis::Value.new('value_name')
135
- @value.value = 'a'
136
- @value.delete
137
-
138
- Of course complex data is no problem:
139
-
140
- @account = Account.create!(params[:account])
141
- @newest = Redis::Value.new('newest_account')
142
- @newest.value = @account
143
-
144
- == Example 2: Class Usage
145
-
146
- Using Redis::Objects this way makes it trivial to integrate Redis types with an
147
- existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
148
- will work with any class that provides an +id+ method that returns a unique
149
- value. Redis::Objects will automatically create keys that are unique to
150
- each object.
151
-
152
- === Initialization
262
+ === Sorted Sets
153
263
 
154
- If on Rails, config/initializers/redis.rb is a good place for this:
264
+ Due to their unique properties, Sorted Sets work like a hybrid between
265
+ a Hash and an Array. You assign like a Hash, but retrieve like an Array:
155
266
 
156
- require 'redis'
157
- require 'redis/objects'
158
- Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
159
-
160
- === Model Class
161
-
162
- Include Redis::Objects in any type of class:
163
-
164
- class Team < ActiveRecord::Base
165
- include Redis::Objects
166
-
167
- counter :hits
168
- counter :runs
169
- counter :outs
170
- counter :inning, :start => 1
171
- list :on_base
172
- set :outfielders
173
- value :at_bat
174
- end
175
-
176
- Familiar Ruby array operations Just Work (TM):
177
-
178
- @team = Team.find_by_name('New York Yankees')
179
- @team.on_base << 'player1'
180
- @team.on_base << 'player2'
181
- @team.on_base << 'player3'
182
- @team.on_base # ['player1', 'player2']
183
- @team.on_base.pop
184
- @team.on_base.shift
185
- @team.on_base.length # 1
186
- @team.on_base.delete('player3')
187
-
188
- Sets operations work too:
267
+ require 'redis/sorted_set'
268
+ @sorted_set = Redis::SortedSet.new('number_of_posts')
269
+ @sorted_set['Nate'] = 15
270
+ @sorted_set['Peter'] = 75
271
+ @sorted_set['Jeff'] = 24
189
272
 
190
- @team.outfielders << 'outfielder1' << 'outfielder1'
191
- @team.outfielders << 'outfielder2'
192
- @team.outfielders # ['outfielder1', 'outfielder2']
193
- @team.outfielders.each do |player|
194
- puts player
195
- end
196
- player = @team.outfielders.detect{|of| of == 'outfielder2'}
197
-
198
- Counters can be atomically incremented/decremented (but not assigned):
273
+ # atomic increment
274
+ @sorted_set.increment('Nate')
275
+ @sorted_set.incr('Peter') # shorthand
276
+ @sorted_set.incr('Jeff', 4)
199
277
 
200
- @team.hits.increment # or incr
201
- @team.hits.decrement # or decr
202
- @team.hits.incr(3) # add 3
203
- @team.runs = 4 # exception
278
+ @sorted_set[0,2] # => ["Nate", "Jeff", "Peter"]
279
+ @sorted_set.first # => "Nate"
280
+ @sorted_set.last # => "Nate"
281
+ @sorted_set.revrange(0,2) # => ["Peter", "Jeff", "Nate"]
204
282
 
205
- For free, you get a +redis+ handle usable in your class:
283
+ @sorted_set['Newbie'] = 1
284
+ @sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
206
285
 
207
- @team.redis.get('somekey')
208
- @team.redis.smembers('someset')
286
+ @sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
287
+ @sorted_set.members(:withscores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
209
288
 
210
- You can call any operation supported by {Redis}[http://code.google.com/p/redis/wiki/CommandReference]
289
+ The other Redis Sorted Set commands are supported as well; see {SortedSets API}[http://code.google.com/p/redis/wiki/SortedSets].
211
290
 
212
- == Atomicity
291
+ == Atomic Counters and Locks
213
292
 
214
293
  You are probably not handling atomicity correctly in your app. For a fun rant
215
294
  on the topic, see
216
- {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc].
295
+ {An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant].
217
296
 
218
297
  Atomic counters are a good way to handle concurrency:
219
298
 
@@ -260,6 +339,10 @@ Class-level atomic block (may save a DB fetch depending on your app):
260
339
 
261
340
  Locks with Redis. On completion or exception the lock is released:
262
341
 
342
+ class Team < ActiveRecord::Base
343
+ lock :reorder # declare a lock
344
+ end
345
+
263
346
  @team.reorder_lock.lock do
264
347
  @team.reorder_all_players
265
348
  end
@@ -270,9 +353,19 @@ Class-level lock (same concept)
270
353
  Team.reorder_all_players(team_id)
271
354
  end
272
355
 
356
+ Lock expiration. Sometimes you want to make sure your locks are cleaned up should
357
+ the unthinkable happen (server failure). You can set lock expirations to handle
358
+ this. Expired locks are released by the next process to attempt lock. Just
359
+ make sure you expiration value is sufficiently large compared to your expected
360
+ lock time.
361
+
362
+ class Team < ActiveRecord::Base
363
+ lock :reorder, :expiration => 15.minutes
364
+ end
365
+
273
366
 
274
367
  == Author
275
368
 
276
- Copyright (c) 2009 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
369
+ Copyright (c) 2009-2010 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
277
370
  Released under the {Artistic License}[http://www.opensource.org/licenses/artistic-license-2.0.php].
278
371
 
data/lib/redis/counter.rb CHANGED
@@ -7,11 +7,14 @@ class Redis
7
7
  # class to define a counter.
8
8
  #
9
9
  class Counter
10
+ require 'redis/helpers/core_commands'
11
+ include Redis::Helpers::CoreCommands
12
+
10
13
  attr_reader :key, :options, :redis
11
- def initialize(key, redis=$redis, options={})
14
+ def initialize(key, *args)
12
15
  @key = key
13
- @redis = redis
14
- @options = options
16
+ @options = args.last.is_a?(Hash) ? args.pop : {}
17
+ @redis = args.first || $redis
15
18
  @options[:start] ||= 0
16
19
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
17
20
  end
@@ -21,7 +24,8 @@ class Redis
21
24
  # with a parent and starting over (for example, restarting a game and
22
25
  # disconnecting all players).
23
26
  def reset(to=options[:start])
24
- redis.set(key, to.to_i)
27
+ redis.set key, to.to_i
28
+ true # hack for redis-rb regression
25
29
  end
26
30
 
27
31
  # Returns the current value of the counter. Normally just calling the
@@ -33,19 +37,13 @@ class Redis
33
37
  end
34
38
  alias_method :get, :value
35
39
 
36
- # Delete a counter. Usage discouraged. Consider +reset+ instead.
37
- def delete
38
- redis.del(key)
39
- end
40
- alias_method :del, :delete
41
-
42
40
  # Increment the counter atomically and return the new value. If passed
43
41
  # a block, that block will be evaluated with the new value of the counter
44
42
  # as an argument. If the block returns nil or throws an exception, the
45
43
  # counter will automatically be decremented to its previous value. This
46
44
  # method is aliased as incr() for brevity.
47
45
  def increment(by=1, &block)
48
- val = redis.incr(key, by).to_i
46
+ val = redis.incrby(key, by).to_i
49
47
  block_given? ? rewindable_block(:decrement, val, &block) : val
50
48
  end
51
49
  alias_method :incr, :increment
@@ -56,7 +54,7 @@ class Redis
56
54
  # counter will automatically be incremented to its previous value. This
57
55
  # method is aliased as incr() for brevity.
58
56
  def decrement(by=1, &block)
59
- val = redis.decr(key, by).to_i
57
+ val = redis.decrby(key, by).to_i
60
58
  block_given? ? rewindable_block(:increment, val, &block) : val
61
59
  end
62
60
  alias_method :decr, :decrement
@@ -0,0 +1,54 @@
1
+ class Redis
2
+ module Helpers
3
+ # These are core commands that all types share (rename, etc)
4
+ module CoreCommands
5
+ def exists?
6
+ redis.exists key
7
+ end
8
+
9
+ def delete
10
+ redis.del key
11
+ end
12
+ alias_method :del, :delete
13
+ alias_method :clear, :delete
14
+
15
+ def type
16
+ redis.type key
17
+ end
18
+
19
+ def rename(name, setkey=true)
20
+ dest = name.is_a?(self.class) ? name.key : name
21
+ ret = redis.rename key, dest
22
+ @key = dest if ret && setkey
23
+ ret
24
+ end
25
+
26
+ def renamenx(name, setkey=true)
27
+ dest = name.is_a?(self.class) ? name.key : name
28
+ ret = redis.renamenx key, dest
29
+ @key = dest if ret && setkey
30
+ ret
31
+ end
32
+
33
+ def expire(seconds)
34
+ redis.expire key, seconds
35
+ end
36
+
37
+ def expireat(unixtime)
38
+ redis.expire key, unixtime
39
+ end
40
+
41
+ def move(dbindex)
42
+ redis.move key, dbindex
43
+ end
44
+
45
+ # See the documentation for SORT: http://code.google.com/p/redis/wiki/SortCommand
46
+ # TODO
47
+ # def sort(options)
48
+ # args = []
49
+ # args += ['sort']
50
+ # from_redis redis.sort key
51
+ # end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ class Redis
2
+ module Helpers
3
+ module Serialize
4
+ include Marshal
5
+
6
+ def to_redis(value)
7
+ case value
8
+ when String, Fixnum, Bignum, Float
9
+ value
10
+ else
11
+ dump(value)
12
+ end
13
+ end
14
+
15
+ def from_redis(value)
16
+ case value
17
+ when Array
18
+ value.collect{|v| from_redis(v)}
19
+ else
20
+ restore(value) rescue value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/redis/list.rb CHANGED
@@ -6,14 +6,16 @@ class Redis
6
6
  class List
7
7
  require 'enumerator'
8
8
  include Enumerable
9
- require 'redis/serialize'
10
- include Redis::Serialize
9
+ require 'redis/helpers/core_commands'
10
+ include Redis::Helpers::CoreCommands
11
+ require 'redis/helpers/serialize'
12
+ include Redis::Helpers::Serialize
11
13
 
12
14
  attr_reader :key, :options, :redis
13
- def initialize(key, redis=$redis, options={})
15
+ def initialize(key, *args)
14
16
  @key = key
15
- @redis = redis
16
- @options = options
17
+ @options = args.last.is_a?(Hash) ? args.pop : {}
18
+ @redis = args.first || $redis
17
19
  end
18
20
 
19
21
  # Works like push. Can chain together: list << 'a' << 'b'
@@ -63,6 +65,7 @@ class Redis
63
65
 
64
66
  # Delete the element(s) from the list that match name. If count is specified,
65
67
  # only the first-N (if positive) or last-N (if negative) will be removed.
68
+ # Use .del to completely delete the entire key.
66
69
  # Redis: LREM
67
70
  def delete(name, count=0)
68
71
  redis.lrem(key, count, name) # weird api
@@ -96,11 +99,6 @@ class Redis
96
99
  at(-1)
97
100
  end
98
101
 
99
- # Clear the list entirely. Redis: DEL
100
- def clear
101
- redis.del(key)
102
- end
103
-
104
102
  # Return the length of the list. Aliased as size. Redis: LLEN
105
103
  def length
106
104
  redis.llen(key)
data/lib/redis/lock.rb CHANGED
@@ -10,11 +10,12 @@ class Redis
10
10
  class LockTimeout < StandardError; end #:nodoc:
11
11
 
12
12
  attr_reader :key, :options, :redis
13
- def initialize(key, redis=$redis, options={})
13
+ def initialize(key, *args)
14
14
  @key = key
15
- @redis = redis
16
- @options = options
15
+ @options = args.last.is_a?(Hash) ? args.pop : {}
16
+ @redis = args.first || $redis
17
17
  @options[:timeout] ||= 5
18
+ @options[:init] = false if @options[:init].nil? # default :init to false
18
19
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
19
20
  end
20
21
 
@@ -31,17 +32,53 @@ class Redis
31
32
  def lock(&block)
32
33
  start = Time.now
33
34
  gotit = false
35
+ expiration = nil
34
36
  while Time.now - start < @options[:timeout]
35
- gotit = redis.setnx(key, 1)
37
+ expiration = generate_expiration
38
+ # Use the expiration as the value of the lock.
39
+ gotit = redis.setnx(key, expiration)
36
40
  break if gotit
41
+
42
+ # Lock is being held. Now check to see if it's expired (if we're using
43
+ # lock expiration).
44
+ # See "Handling Deadlocks" section on http://code.google.com/p/redis/wiki/SetnxCommand
45
+ if !@options[:expiration].nil?
46
+ old_expiration = redis.get(key).to_f
47
+
48
+ if old_expiration < Time.now.to_f
49
+ # If it's expired, use GETSET to update it.
50
+ expiration = generate_expiration
51
+ old_expiration = redis.getset(key, expiration).to_f
52
+
53
+ # Since GETSET returns the old value of the lock, if the old expiration
54
+ # is still in the past, we know no one else has expired the locked
55
+ # and we now have it.
56
+ if old_expiration < Time.now.to_f
57
+ gotit = true
58
+ break
59
+ end
60
+ end
61
+ end
62
+
37
63
  sleep 0.1
38
64
  end
39
65
  raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec" unless gotit
40
66
  begin
41
67
  yield
42
68
  ensure
43
- redis.del(key)
69
+ # We need to be careful when cleaning up the lock key. If we took a really long
70
+ # time for some reason, and the lock expired, someone else may have it, and
71
+ # it's not safe for us to remove it. Check how much time has passed since we
72
+ # wrote the lock key and only delete it if it hasn't expired (or we're not using
73
+ # lock expiration)
74
+ if @options[:expiration].nil? || expiration > Time.now.to_f
75
+ redis.del(key)
76
+ end
44
77
  end
45
78
  end
79
+
80
+ def generate_expiration
81
+ @options[:expiration].nil? ? 1 : (Time.now + @options[:expiration].to_f + 1).to_f
82
+ end
46
83
  end
47
84
  end