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 +23 -0
- data/README.rdoc +190 -97
- data/lib/redis/counter.rb +10 -12
- data/lib/redis/helpers/core_commands.rb +54 -0
- data/lib/redis/helpers/serialize.rb +25 -0
- data/lib/redis/list.rb +8 -10
- data/lib/redis/lock.rb +42 -5
- data/lib/redis/objects/counters.rb +41 -23
- data/lib/redis/objects/lists.rb +19 -9
- data/lib/redis/objects/locks.rb +21 -11
- data/lib/redis/objects/sets.rb +20 -9
- data/lib/redis/objects/sorted_sets.rb +45 -0
- data/lib/redis/objects/values.rb +29 -12
- data/lib/redis/objects.rb +10 -8
- data/lib/redis/set.rb +33 -11
- data/lib/redis/sorted_set.rb +275 -0
- data/lib/redis/value.rb +10 -12
- data/spec/redis_objects_instance_spec.rb +339 -32
- data/spec/redis_objects_model_spec.rb +203 -2
- data/spec/spec_helper.rb +1 -0
- metadata +28 -13
- data/lib/redis/objects/core.rb +0 -0
- data/lib/redis/serialize.rb +0 -23
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.
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
===
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
191
|
-
@
|
|
192
|
-
@
|
|
193
|
-
@
|
|
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
|
-
@
|
|
201
|
-
@
|
|
202
|
-
@
|
|
203
|
-
@
|
|
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
|
-
|
|
283
|
+
@sorted_set['Newbie'] = 1
|
|
284
|
+
@sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
|
|
206
285
|
|
|
207
|
-
@
|
|
208
|
-
@
|
|
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
|
-
|
|
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
|
-
==
|
|
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
|
-
{
|
|
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,
|
|
14
|
+
def initialize(key, *args)
|
|
12
15
|
@key = key
|
|
13
|
-
@
|
|
14
|
-
@
|
|
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
|
|
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.
|
|
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.
|
|
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/
|
|
10
|
-
include Redis::
|
|
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,
|
|
15
|
+
def initialize(key, *args)
|
|
14
16
|
@key = key
|
|
15
|
-
@
|
|
16
|
-
@
|
|
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,
|
|
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
|