redis-objects 0.2.2 → 0.3.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.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,37 @@
1
+ = Changelog for Redis::Objects
2
+
3
+ == 0.3.0 [Final] (14 April 2010)
4
+
5
+ * Due to Ruby 1.9 bugs and performance considerations, marshaling of
6
+ data types is now OFF by default. You must say :marshal => true for
7
+ any objects that you want serialization enabled on. [Nate Wiger]
8
+
9
+ * Sorted Set class changed slightly due to feedback. You can now get
10
+ an individual element back via @set['item'] since it acts like a Hash.
11
+
12
+ == 0.2.4 [Final] (9 April 2010)*
13
+
14
+ * Added sorted set support via Redis::SortedSet [Nate Wiger]
15
+
16
+ == 0.2.3 [Final] (18 February 2010)*
17
+
18
+ * Added lock expiration to Redis::Lock [Ben VandenBos]
19
+
20
+ * Fixed some bugs [Ben VandenBos]
21
+
22
+ * Added lock tests and test helpers [Ben VandenBos]
23
+
24
+ == 0.2.2 [Final] (14 December 2009)*
25
+
26
+ * Added @set.diff(@set2) with "^" and "-" synonyms (oversight). [Nate Wiger]
27
+
28
+ * Implemented Redis core commands in all data types, such as rename. [Nate Wiger]
29
+
30
+ * Renamed Redis::Serialize to Redis::Helpers::Serialize to keep Redis:: cleaner. [Nate Wiger]
31
+
32
+ * More spec coverage. [Nate Wiger]
33
+
34
+ == 0.2.1 [Final] (27 November 2009)*
35
+
36
+ * First worthwhile public release, with good spec coverage and functionality. [Nate Wiger]
37
+
data/README.rdoc CHANGED
@@ -1,44 +1,136 @@
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 {Redis types}[http://code.google.com/p/redis/wiki/CommandReference]
10
- to Ruby objects, via a thin layer over Ezra's +redis+ gem.
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 (if you set :marshal => true)
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: Standalone 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:
38
+
39
+ model_name:id:field_name
40
+
41
+ === Initialization
42
+
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
27
115
 
28
- There is a Ruby object that maps to each Redis type.
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.
29
121
 
30
122
  === Initialization
31
123
 
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:
124
+ Redis::Objects needs a handle to the +redis+ server. For standalone use, you
125
+ can either set the $redis global variable:
34
126
 
35
127
  $redis = Redis.new(:host => 'localhost', :port => 6379)
36
- @value = Redis::Value.new('myvalue')
128
+ @list = Redis::List.new('mylist')
37
129
 
38
- 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:
39
131
 
40
132
  redis = Redis.new(:host => 'localhost', :port => 6379)
41
- @value = Redis::Value.new('myvalue', redis)
133
+ @list = Redis::List.new('mylist', redis)
42
134
 
43
135
  === Counters
44
136
 
@@ -58,6 +150,34 @@ This gem provides a clean way to do atomic blocks as well:
58
150
 
59
151
  See the section on "Atomicity" for cool uses of atomic counter blocks.
60
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
+ Complex data is no problem with :marshal => true:
175
+
176
+ @account = Account.create!(params[:account])
177
+ @newest = Redis::Value.new('newest_account', :marshal => true)
178
+ @newest.value = @account.attributes
179
+ puts @newest.value['username']
180
+
61
181
  === Lists
62
182
 
63
183
  Lists work just like Ruby arrays:
@@ -78,14 +198,15 @@ Lists work just like Ruby arrays:
78
198
  @list.clear
79
199
  # etc
80
200
 
81
- Complex data types are no problem:
201
+ Complex data types are no problem with :marshal => true:
82
202
 
203
+ @list = Redis::List.new('list_name', :marshal => true)
83
204
  @list << {:name => "Nate", :city => "San Diego"}
84
205
  @list << {:name => "Peter", :city => "Oceanside"}
85
206
  @list.each do |el|
86
207
  puts "#{el[:name]} lives in #{el[:city]}"
87
208
  end
88
-
209
+
89
210
  === Sets
90
211
 
91
212
  Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
@@ -111,8 +232,8 @@ You can perform Redis intersections/unions/diffs easily:
111
232
  members = @set1 & @set2 # intersection
112
233
  members = @set1 | @set2 # union
113
234
  members = @set1 + @set2 # union
114
- members = @set1 ^ @set2 # union
115
- members = @set1 - @set2 # union
235
+ members = @set1 ^ @set2 # difference
236
+ members = @set1 - @set2 # difference
116
237
  members = @set1.intersection(@set2, @set3) # multiple
117
238
  members = @set1.union(@set2, @set3) # multiple
118
239
  members = @set1.difference(@set2, @set3) # multiple
@@ -126,8 +247,10 @@ Or store them in Redis:
126
247
  @set1.diffstore('diffname', @set2, @set3)
127
248
  members = @set1.redis.get('diffname')
128
249
 
129
- And use complex data types too:
250
+ And use complex data types too, with :marshal => true:
130
251
 
252
+ @set1 = Redis::Set.new('set1', :marshal => true)
253
+ @set2 = Redis::Set.new('set2', :marshal => true)
131
254
  @set1 << {:name => "Nate", :city => "San Diego"}
132
255
  @set1 << {:name => "Peter", :city => "Oceanside"}
133
256
  @set2 << {:name => "Nate", :city => "San Diego"}
@@ -137,100 +260,50 @@ And use complex data types too:
137
260
  @set1 - @set2 # Peter
138
261
  @set1 | @set2 # all 3 people
139
262
 
140
- === Values
141
-
142
- Simple values are easy as well:
143
-
144
- require 'redis/value'
145
- @value = Redis::Value.new('value_name')
146
- @value.value = 'a'
147
- @value.delete
148
-
149
- Of course complex data is no problem:
150
-
151
- @account = Account.create!(params[:account])
152
- @newest = Redis::Value.new('newest_account')
153
- @newest.value = @account
154
-
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
263
+ === Sorted Sets
186
264
 
187
- Familiar Ruby array operations Just Work (TM):
265
+ Due to their unique properties, Sorted Sets work like a hybrid between
266
+ a Hash and an Array. You assign like a Hash, but retrieve like an Array:
188
267
 
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')
268
+ require 'redis/sorted_set'
269
+ @sorted_set = Redis::SortedSet.new('number_of_posts')
270
+ @sorted_set['Nate'] = 15
271
+ @sorted_set['Peter'] = 75
272
+ @sorted_set['Jeff'] = 24
198
273
 
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'}
274
+ # Array access to get sorted order
275
+ @sorted_set[0..2] # => ["Nate", "Jeff", "Peter"]
276
+ @sorted_set[0,2] # => ["Nate", "Jeff"]
208
277
 
209
- And you can do intersections between ORM objects (kinda cool):
278
+ @sorted_set['Peter'] # => 75
279
+ @sorted_set['Jeff'] # => 24
280
+ @sorted_set.score('Jeff') # same thing (24)
210
281
 
211
- @team1.outfielders | @team2.outfielders # all outfielders
212
- @team1.outfielders & @team2.outfielders # should be empty
282
+ @sorted_set.rank('Peter') # => 2
283
+ @sorted_set.rank('Jeff') # => 1
213
284
 
214
- Counters can be atomically incremented/decremented (but not assigned):
285
+ @sorted_set.first # => "Nate"
286
+ @sorted_set.last # => "Peter"
287
+ @sorted_set.revrange(0,2) # => ["Peter", "Jeff", "Nate"]
215
288
 
216
- @team.hits.increment # or incr
217
- @team.hits.decrement # or decr
218
- @team.hits.incr(3) # add 3
219
- @team.runs = 4 # exception
289
+ @sorted_set['Newbie'] = 1
290
+ @sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
220
291
 
221
- Finally, for free, you get a +redis+ handle usable in your class that
222
- points directly to a Redis API object:
292
+ @sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
293
+ @sorted_set.members(:withscores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
223
294
 
224
- @team.redis.get('somekey')
225
- @team.redis.smembers('someset')
295
+ # atomic increment
296
+ @sorted_set.increment('Nate')
297
+ @sorted_set.incr('Peter') # shorthand
298
+ @sorted_set.incr('Jeff', 4)
226
299
 
227
- You can use the +redis+ handle to directly call any {Redis command}[http://code.google.com/p/redis/wiki/CommandReference]
300
+ The other Redis Sorted Set commands are supported as well; see {Sorted Sets API}[http://code.google.com/p/redis/wiki/SortedSets].
228
301
 
229
302
  == Atomic Counters and Locks
230
303
 
231
304
  You are probably not handling atomicity correctly in your app. For a fun rant
232
305
  on the topic, see
233
- {ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc].
306
+ {An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant].
234
307
 
235
308
  Atomic counters are a good way to handle concurrency:
236
309
 
@@ -277,6 +350,10 @@ Class-level atomic block (may save a DB fetch depending on your app):
277
350
 
278
351
  Locks with Redis. On completion or exception the lock is released:
279
352
 
353
+ class Team < ActiveRecord::Base
354
+ lock :reorder # declare a lock
355
+ end
356
+
280
357
  @team.reorder_lock.lock do
281
358
  @team.reorder_all_players
282
359
  end
@@ -287,9 +364,19 @@ Class-level lock (same concept)
287
364
  Team.reorder_all_players(team_id)
288
365
  end
289
366
 
367
+ Lock expiration. Sometimes you want to make sure your locks are cleaned up should
368
+ the unthinkable happen (server failure). You can set lock expirations to handle
369
+ this. Expired locks are released by the next process to attempt lock. Just
370
+ make sure you expiration value is sufficiently large compared to your expected
371
+ lock time.
372
+
373
+ class Team < ActiveRecord::Base
374
+ lock :reorder, :expiration => 15.minutes
375
+ end
376
+
290
377
 
291
378
  == Author
292
379
 
293
- Copyright (c) 2009 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
380
+ Copyright (c) 2009-2010 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
294
381
  Released under the {Artistic License}[http://www.opensource.org/licenses/artistic-license-2.0.php].
295
382
 
data/lib/redis/counter.rb CHANGED
@@ -11,10 +11,10 @@ class Redis
11
11
  include Redis::Helpers::CoreCommands
12
12
 
13
13
  attr_reader :key, :options, :redis
14
- def initialize(key, redis=$redis, options={})
14
+ def initialize(key, *args)
15
15
  @key = key
16
- @redis = redis
17
- @options = options
16
+ @options = args.last.is_a?(Hash) ? args.pop : {}
17
+ @redis = args.first || $redis
18
18
  @options[:start] ||= 0
19
19
  @redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
20
20
  end
@@ -25,6 +25,7 @@ class Redis
25
25
  # disconnecting all players).
26
26
  def reset(to=options[:start])
27
27
  redis.set key, to.to_i
28
+ true # hack for redis-rb regression
28
29
  end
29
30
 
30
31
  # Returns the current value of the counter. Normally just calling the
@@ -42,7 +43,7 @@ class Redis
42
43
  # counter will automatically be decremented to its previous value. This
43
44
  # method is aliased as incr() for brevity.
44
45
  def increment(by=1, &block)
45
- val = redis.incr(key, by).to_i
46
+ val = redis.incrby(key, by).to_i
46
47
  block_given? ? rewindable_block(:decrement, val, &block) : val
47
48
  end
48
49
  alias_method :incr, :increment
@@ -53,7 +54,7 @@ class Redis
53
54
  # counter will automatically be incremented to its previous value. This
54
55
  # method is aliased as incr() for brevity.
55
56
  def decrement(by=1, &block)
56
- val = redis.decr(key, by).to_i
57
+ val = redis.decrby(key, by).to_i
57
58
  block_given? ? rewindable_block(:increment, val, &block) : val
58
59
  end
59
60
  alias_method :decr, :decrement
@@ -15,7 +15,7 @@ class Redis
15
15
  def type
16
16
  redis.type key
17
17
  end
18
-
18
+
19
19
  def rename(name, setkey=true)
20
20
  dest = name.is_a?(self.class) ? name.key : name
21
21
  ret = redis.rename key, dest
@@ -41,6 +41,14 @@ class Redis
41
41
  def move(dbindex)
42
42
  redis.move key, dbindex
43
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
44
52
  end
45
53
  end
46
54
  end
@@ -4,6 +4,7 @@ class Redis
4
4
  include Marshal
5
5
 
6
6
  def to_redis(value)
7
+ return value unless options[:marshal]
7
8
  case value
8
9
  when String, Fixnum, Bignum, Float
9
10
  value
@@ -13,6 +14,7 @@ class Redis
13
14
  end
14
15
 
15
16
  def from_redis(value)
17
+ return value unless options[:marshal]
16
18
  case value
17
19
  when Array
18
20
  value.collect{|v| from_redis(v)}
data/lib/redis/list.rb CHANGED
@@ -12,10 +12,10 @@ class Redis
12
12
  include Redis::Helpers::Serialize
13
13
 
14
14
  attr_reader :key, :options, :redis
15
- def initialize(key, redis=$redis, options={})
15
+ def initialize(key, *args)
16
16
  @key = key
17
- @redis = redis
18
- @options = options
17
+ @options = args.last.is_a?(Hash) ? args.pop : {}
18
+ @redis = args.first || $redis
19
19
  end
20
20
 
21
21
  # Works like push. Can chain together: list << 'a' << 'b'
@@ -62,7 +62,7 @@ class Redis
62
62
  at(index)
63
63
  end
64
64
  end
65
-
65
+
66
66
  # Delete the element(s) from the list that match name. If count is specified,
67
67
  # only the first-N (if positive) or last-N (if negative) will be removed.
68
68
  # Use .del to completely delete the entire 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
@@ -56,7 +56,7 @@ class Redis
56
56
  def increment_counter(name, id=nil, by=1, &block)
57
57
  verify_counter_defined!(name, id)
58
58
  initialize_counter!(name, id)
59
- value = redis.incr(field_key(name, id), by).to_i
59
+ value = redis.incrby(field_key(name, id), by).to_i
60
60
  block_given? ? rewindable_block(:decrement_counter, name, id, value, &block) : value
61
61
  end
62
62
 
@@ -65,7 +65,7 @@ class Redis
65
65
  def decrement_counter(name, id=nil, by=1, &block)
66
66
  verify_counter_defined!(name, id)
67
67
  initialize_counter!(name, id)
68
- value = redis.decr(field_key(name, id), by).to_i
68
+ value = redis.decrby(field_key(name, id), by).to_i
69
69
  block_given? ? rewindable_block(:increment_counter, name, id, value, &block) : value
70
70
  end
71
71
 
@@ -73,7 +73,8 @@ class Redis
73
73
  def reset_counter(name, id=nil, to=nil)
74
74
  verify_counter_defined!(name, id)
75
75
  to = @redis_objects[name][:start] if to.nil?
76
- redis.set(field_key(name, id), to)
76
+ redis.set(field_key(name, id), to.to_i)
77
+ true
77
78
  end
78
79
 
79
80
  private
@@ -35,9 +35,6 @@ class Redis
35
35
  end
36
36
  EndMethods
37
37
  end
38
-
39
-
40
-
41
38
  end
42
39
 
43
40
  # Obtain a lock, and execute the block synchronously. Any other code
@@ -47,7 +44,7 @@ class Redis
47
44
  verify_lock_defined!(name)
48
45
  raise ArgumentError, "Missing block to #{self.name}.obtain_lock" unless block_given?
49
46
  lock_name = field_key("#{name}_lock", id)
50
- Redis::Lock.new(redis, lock_name, self.class.redis_objects[name]).lock(&block)
47
+ Redis::Lock.new(lock_name, redis, self.redis_objects[name]).lock(&block)
51
48
  end
52
49
 
53
50
  # Clear the lock. Use with care - usually only in an Admin page to clear