redis-objects 0.2.2 → 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 +8 -0
- data/README.rdoc +181 -105
- data/lib/redis/counter.rb +6 -5
- data/lib/redis/helpers/core_commands.rb +9 -1
- data/lib/redis/list.rb +3 -3
- data/lib/redis/lock.rb +42 -5
- data/lib/redis/objects/counters.rb +4 -3
- data/lib/redis/objects/locks.rb +1 -4
- data/lib/redis/objects/sorted_sets.rb +45 -0
- data/lib/redis/objects.rb +8 -6
- data/lib/redis/set.rb +3 -3
- data/lib/redis/sorted_set.rb +275 -0
- data/lib/redis/value.rb +3 -3
- data/spec/redis_objects_instance_spec.rb +276 -31
- data/spec/redis_objects_model_spec.rb +15 -1
- metadata +24 -11
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
|
@@ -1,44 +1,136 @@
|
|
|
1
1
|
= Redis::Objects - Map Redis types directly to Ruby objects
|
|
2
2
|
|
|
3
|
-
This is *not* an ORM.
|
|
4
|
-
the point.
|
|
3
|
+
This is *not* an ORM. People that are wrapping ORM’s around Redis are missing the point.
|
|
5
4
|
|
|
6
|
-
The killer feature of Redis that it allows you to perform
|
|
7
|
-
on _individual_ data structures, like counters, lists, and sets.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
18
|
-
or by using
|
|
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:
|
|
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
|
-
|
|
39
|
+
model_name:id:field_name
|
|
29
40
|
|
|
30
41
|
=== Initialization
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
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:
|
|
34
126
|
|
|
35
127
|
$redis = Redis.new(:host => 'localhost', :port => 6379)
|
|
36
|
-
@
|
|
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
|
-
@
|
|
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
|
+
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
|
+
|
|
61
181
|
=== Lists
|
|
62
182
|
|
|
63
183
|
Lists work just like Ruby arrays:
|
|
@@ -85,6 +205,8 @@ Complex data types are no problem:
|
|
|
85
205
|
@list.each do |el|
|
|
86
206
|
puts "#{el[:name]} lives in #{el[:city]}"
|
|
87
207
|
end
|
|
208
|
+
|
|
209
|
+
This gem will automatically marshal complex data, similar to how session stores work.
|
|
88
210
|
|
|
89
211
|
=== Sets
|
|
90
212
|
|
|
@@ -111,8 +233,8 @@ You can perform Redis intersections/unions/diffs easily:
|
|
|
111
233
|
members = @set1 & @set2 # intersection
|
|
112
234
|
members = @set1 | @set2 # union
|
|
113
235
|
members = @set1 + @set2 # union
|
|
114
|
-
members = @set1 ^ @set2 #
|
|
115
|
-
members = @set1 - @set2 #
|
|
236
|
+
members = @set1 ^ @set2 # difference
|
|
237
|
+
members = @set1 - @set2 # difference
|
|
116
238
|
members = @set1.intersection(@set2, @set3) # multiple
|
|
117
239
|
members = @set1.union(@set2, @set3) # multiple
|
|
118
240
|
members = @set1.difference(@set2, @set3) # multiple
|
|
@@ -137,100 +259,40 @@ And use complex data types too:
|
|
|
137
259
|
@set1 - @set2 # Peter
|
|
138
260
|
@set1 | @set2 # all 3 people
|
|
139
261
|
|
|
140
|
-
===
|
|
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
|
|
262
|
+
=== Sorted Sets
|
|
156
263
|
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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:
|
|
172
266
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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:
|
|
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
|
|
200
272
|
|
|
201
|
-
|
|
202
|
-
@
|
|
203
|
-
@
|
|
204
|
-
@
|
|
205
|
-
puts player
|
|
206
|
-
end
|
|
207
|
-
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
|
273
|
+
# atomic increment
|
|
274
|
+
@sorted_set.increment('Nate')
|
|
275
|
+
@sorted_set.incr('Peter') # shorthand
|
|
276
|
+
@sorted_set.incr('Jeff', 4)
|
|
208
277
|
|
|
209
|
-
|
|
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"]
|
|
210
282
|
|
|
211
|
-
@
|
|
212
|
-
@
|
|
283
|
+
@sorted_set['Newbie'] = 1
|
|
284
|
+
@sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
|
|
213
285
|
|
|
214
|
-
|
|
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
|
|
286
|
+
@sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
|
|
287
|
+
@sorted_set.members(:withscores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
|
|
220
288
|
|
|
221
|
-
|
|
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]
|
|
289
|
+
The other Redis Sorted Set commands are supported as well; see {SortedSets API}[http://code.google.com/p/redis/wiki/SortedSets].
|
|
228
290
|
|
|
229
291
|
== Atomic Counters and Locks
|
|
230
292
|
|
|
231
293
|
You are probably not handling atomicity correctly in your app. For a fun rant
|
|
232
294
|
on the topic, see
|
|
233
|
-
{
|
|
295
|
+
{An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant].
|
|
234
296
|
|
|
235
297
|
Atomic counters are a good way to handle concurrency:
|
|
236
298
|
|
|
@@ -277,6 +339,10 @@ Class-level atomic block (may save a DB fetch depending on your app):
|
|
|
277
339
|
|
|
278
340
|
Locks with Redis. On completion or exception the lock is released:
|
|
279
341
|
|
|
342
|
+
class Team < ActiveRecord::Base
|
|
343
|
+
lock :reorder # declare a lock
|
|
344
|
+
end
|
|
345
|
+
|
|
280
346
|
@team.reorder_lock.lock do
|
|
281
347
|
@team.reorder_all_players
|
|
282
348
|
end
|
|
@@ -287,9 +353,19 @@ Class-level lock (same concept)
|
|
|
287
353
|
Team.reorder_all_players(team_id)
|
|
288
354
|
end
|
|
289
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
|
+
|
|
290
366
|
|
|
291
367
|
== Author
|
|
292
368
|
|
|
293
|
-
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.
|
|
294
370
|
Released under the {Artistic License}[http://www.opensource.org/licenses/artistic-license-2.0.php].
|
|
295
371
|
|
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,
|
|
14
|
+
def initialize(key, *args)
|
|
15
15
|
@key = key
|
|
16
|
-
@
|
|
17
|
-
@
|
|
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.
|
|
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.
|
|
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
|
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,
|
|
15
|
+
def initialize(key, *args)
|
|
16
16
|
@key = key
|
|
17
|
-
@
|
|
18
|
-
@
|
|
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'
|
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,
|
|
13
|
+
def initialize(key, *args)
|
|
14
14
|
@key = key
|
|
15
|
-
@
|
|
16
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
data/lib/redis/objects/locks.rb
CHANGED
|
@@ -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(
|
|
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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# This is the class loader, for use as "include Redis::Objects::Sets"
|
|
2
|
+
# For the object itself, see "Redis::Set"
|
|
3
|
+
require 'redis/sorted_set'
|
|
4
|
+
class Redis
|
|
5
|
+
module Objects
|
|
6
|
+
module SortedSets
|
|
7
|
+
def self.included(klass)
|
|
8
|
+
klass.send :include, InstanceMethods
|
|
9
|
+
klass.extend ClassMethods
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
|
13
|
+
module ClassMethods
|
|
14
|
+
# Define a new list. It will function like a regular instance
|
|
15
|
+
# method, so it can be used alongside ActiveRecord, DataMapper, etc.
|
|
16
|
+
def sorted_set(name, options={})
|
|
17
|
+
@redis_objects[name] = options.merge(:type => :sorted_set)
|
|
18
|
+
if options[:global]
|
|
19
|
+
instance_eval <<-EndMethods
|
|
20
|
+
def #{name}
|
|
21
|
+
@#{name} ||= Redis::SortedSet.new(field_key(:#{name}, ''), redis, @redis_objects[:#{name}])
|
|
22
|
+
end
|
|
23
|
+
EndMethods
|
|
24
|
+
class_eval <<-EndMethods
|
|
25
|
+
def #{name}
|
|
26
|
+
self.class.#{name}
|
|
27
|
+
end
|
|
28
|
+
EndMethods
|
|
29
|
+
else
|
|
30
|
+
class_eval <<-EndMethods
|
|
31
|
+
def #{name}
|
|
32
|
+
@#{name} ||= Redis::SortedSet.new(field_key(:#{name}), redis, self.class.redis_objects[:#{name}])
|
|
33
|
+
end
|
|
34
|
+
EndMethods
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Instance methods that appear in your class when you include Redis::Objects.
|
|
41
|
+
module InstanceMethods
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/redis/objects.rb
CHANGED
|
@@ -6,7 +6,7 @@ class Redis
|
|
|
6
6
|
# Redis::Objects enables high-performance atomic operations in your app
|
|
7
7
|
# by leveraging the atomic features of the Redis server. To use Redis::Objects,
|
|
8
8
|
# first include it in any class you want. (This example uses an ActiveRecord
|
|
9
|
-
# subclass, but that is *not* required.) Then, use +counter
|
|
9
|
+
# subclass, but that is *not* required.) Then, use +counter+, +lock+, +set+, etc
|
|
10
10
|
# to define your primitives:
|
|
11
11
|
#
|
|
12
12
|
# class Game < ActiveRecord::Base
|
|
@@ -14,8 +14,8 @@ class Redis
|
|
|
14
14
|
#
|
|
15
15
|
# counter :joined_players
|
|
16
16
|
# counter :active_players
|
|
17
|
-
# set :player_ids
|
|
18
17
|
# lock :archive_game
|
|
18
|
+
# set :player_ids
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
21
|
# The, you can use these counters both for bookeeping and as atomic actions:
|
|
@@ -39,10 +39,11 @@ class Redis
|
|
|
39
39
|
dir = File.expand_path(__FILE__.sub(/\.rb$/,''))
|
|
40
40
|
|
|
41
41
|
autoload :Counters, File.join(dir, 'counters')
|
|
42
|
-
autoload :Values, File.join(dir, 'values')
|
|
43
42
|
autoload :Lists, File.join(dir, 'lists')
|
|
44
|
-
autoload :Sets, File.join(dir, 'sets')
|
|
45
43
|
autoload :Locks, File.join(dir, 'locks')
|
|
44
|
+
autoload :Sets, File.join(dir, 'sets')
|
|
45
|
+
autoload :SortedSets, File.join(dir, 'sorted_sets')
|
|
46
|
+
autoload :Values, File.join(dir, 'values')
|
|
46
47
|
|
|
47
48
|
class NotConnected < StandardError; end
|
|
48
49
|
|
|
@@ -61,10 +62,11 @@ class Redis
|
|
|
61
62
|
|
|
62
63
|
# Pull in each object type
|
|
63
64
|
klass.send :include, Redis::Objects::Counters
|
|
64
|
-
klass.send :include, Redis::Objects::Values
|
|
65
65
|
klass.send :include, Redis::Objects::Lists
|
|
66
|
-
klass.send :include, Redis::Objects::Sets
|
|
67
66
|
klass.send :include, Redis::Objects::Locks
|
|
67
|
+
klass.send :include, Redis::Objects::Sets
|
|
68
|
+
klass.send :include, Redis::Objects::SortedSets
|
|
69
|
+
klass.send :include, Redis::Objects::Values
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
|