redis-objects 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +92 -38
- 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 +4 -3
- data/lib/redis/objects.rb +8 -6
- data/lib/redis/objects/counters.rb +4 -3
- data/lib/redis/objects/locks.rb +0 -4
- data/lib/redis/objects/sorted_sets.rb +45 -0
- 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 +268 -111
- data/spec/redis_objects_model_spec.rb +1 -1
- metadata +24 -11
data/README.rdoc
CHANGED
@@ -1,26 +1,31 @@
|
|
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
31
|
== Example 1: Model Class Usage
|
@@ -29,27 +34,30 @@ Using Redis::Objects this way makes it trivial to integrate Redis types with an
|
|
29
34
|
existing ActiveRecord model, DataMapper resource, or other class. Redis::Objects
|
30
35
|
will work with _any_ class that provides an +id+ method that returns a unique
|
31
36
|
value. Redis::Objects will automatically create keys that are unique to
|
32
|
-
each object
|
37
|
+
each object, in the format:
|
38
|
+
|
39
|
+
model_name:id:field_name
|
33
40
|
|
34
41
|
=== Initialization
|
35
42
|
|
36
|
-
Redis::Objects needs a handle created by Redis.new. If you're on Rails,
|
37
|
-
config/initializers/redis.rb is a good place for this
|
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.)
|
38
45
|
|
39
46
|
require 'redis'
|
40
47
|
require 'redis/objects'
|
41
48
|
Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
|
42
49
|
|
43
|
-
|
44
|
-
|
50
|
+
Remember you can use Redis::Objects in any Ruby code. There are *no* dependencies
|
51
|
+
on Rails. Standalone, Sinatra, Resque - no problem.
|
45
52
|
|
46
53
|
=== Model Class
|
47
54
|
|
48
|
-
|
55
|
+
You can include Redis::Objects in any type of class:
|
49
56
|
|
50
57
|
class Team < ActiveRecord::Base
|
51
58
|
include Redis::Objects
|
52
59
|
|
60
|
+
lock :trade_players, :expiration => 15 # sec
|
53
61
|
counter :hits
|
54
62
|
counter :runs
|
55
63
|
counter :outs
|
@@ -82,7 +90,7 @@ Sets work too:
|
|
82
90
|
end
|
83
91
|
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
84
92
|
|
85
|
-
And you can do intersections between
|
93
|
+
And you can do intersections between objects (kinda cool):
|
86
94
|
|
87
95
|
@team1.outfielders | @team2.outfielders # outfielders on both teams
|
88
96
|
@team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
|
@@ -101,26 +109,28 @@ Finally, for free, you get a +redis+ method that points directly to a Redis conn
|
|
101
109
|
@team.redis.get('somekey')
|
102
110
|
@team.redis.smembers('someset')
|
103
111
|
|
104
|
-
You can use the +redis+ handle to directly call any {Redis command}[http://code.google.com/p/redis/wiki/CommandReference]
|
112
|
+
You can use the +redis+ handle to directly call any {Redis API command}[http://code.google.com/p/redis/wiki/CommandReference].
|
105
113
|
|
106
114
|
== Example 2: Standalone Usage
|
107
115
|
|
108
|
-
There is a Ruby
|
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.
|
109
121
|
|
110
122
|
=== Initialization
|
111
123
|
|
112
|
-
|
124
|
+
Redis::Objects needs a handle to the +redis+ server. For standalone use, you
|
113
125
|
can either set the $redis global variable:
|
114
126
|
|
115
127
|
$redis = Redis.new(:host => 'localhost', :port => 6379)
|
116
|
-
@
|
128
|
+
@list = Redis::List.new('mylist')
|
117
129
|
|
118
130
|
Or you can pass the Redis handle into the new method for each type:
|
119
131
|
|
120
132
|
redis = Redis.new(:host => 'localhost', :port => 6379)
|
121
|
-
@
|
122
|
-
|
123
|
-
Your choice.
|
133
|
+
@list = Redis::List.new('mylist', redis)
|
124
134
|
|
125
135
|
=== Counters
|
126
136
|
|
@@ -140,6 +150,34 @@ This gem provides a clean way to do atomic blocks as well:
|
|
140
150
|
|
141
151
|
See the section on "Atomicity" for cool uses of atomic counter blocks.
|
142
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
|
+
|
143
181
|
=== Lists
|
144
182
|
|
145
183
|
Lists work just like Ruby arrays:
|
@@ -167,6 +205,8 @@ Complex data types are no problem:
|
|
167
205
|
@list.each do |el|
|
168
206
|
puts "#{el[:name]} lives in #{el[:city]}"
|
169
207
|
end
|
208
|
+
|
209
|
+
This gem will automatically marshal complex data, similar to how session stores work.
|
170
210
|
|
171
211
|
=== Sets
|
172
212
|
|
@@ -219,26 +259,40 @@ And use complex data types too:
|
|
219
259
|
@set1 - @set2 # Peter
|
220
260
|
@set1 | @set2 # all 3 people
|
221
261
|
|
222
|
-
===
|
262
|
+
=== Sorted Sets
|
223
263
|
|
224
|
-
|
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:
|
225
266
|
|
226
|
-
require 'redis/
|
227
|
-
@
|
228
|
-
@
|
229
|
-
@
|
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
|
272
|
+
|
273
|
+
# atomic increment
|
274
|
+
@sorted_set.increment('Nate')
|
275
|
+
@sorted_set.incr('Peter') # shorthand
|
276
|
+
@sorted_set.incr('Jeff', 4)
|
230
277
|
|
231
|
-
|
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"]
|
232
282
|
|
233
|
-
@
|
234
|
-
@
|
235
|
-
|
283
|
+
@sorted_set['Newbie'] = 1
|
284
|
+
@sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
|
285
|
+
|
286
|
+
@sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
|
287
|
+
@sorted_set.members(:withscores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
|
288
|
+
|
289
|
+
The other Redis Sorted Set commands are supported as well; see {SortedSets API}[http://code.google.com/p/redis/wiki/SortedSets].
|
236
290
|
|
237
291
|
== Atomic Counters and Locks
|
238
292
|
|
239
293
|
You are probably not handling atomicity correctly in your app. For a fun rant
|
240
294
|
on the topic, see
|
241
|
-
{
|
295
|
+
{An Atomic Rant}[http://nateware.com/2010/02/18/an-atomic-rant].
|
242
296
|
|
243
297
|
Atomic counters are a good way to handle concurrency:
|
244
298
|
|
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
|
|
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
|
|
@@ -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
@@ -16,7 +16,6 @@ class Redis
|
|
16
16
|
# so it can be used alongside ActiveRecord/DataMapper, etc.
|
17
17
|
def lock(name, options={})
|
18
18
|
options[:timeout] ||= 5 # seconds
|
19
|
-
options[:init] = false if options[:init].nil? # default :init to false
|
20
19
|
@redis_objects[name] = options.merge(:type => :lock)
|
21
20
|
if options[:global]
|
22
21
|
instance_eval <<-EndMethods
|
@@ -36,9 +35,6 @@ class Redis
|
|
36
35
|
end
|
37
36
|
EndMethods
|
38
37
|
end
|
39
|
-
|
40
|
-
|
41
|
-
|
42
38
|
end
|
43
39
|
|
44
40
|
# Obtain a lock, and execute the block synchronously. Any other code
|
@@ -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/set.rb
CHANGED
@@ -13,10 +13,10 @@ class Redis
|
|
13
13
|
attr_reader :key, :options, :redis
|
14
14
|
|
15
15
|
# Create a new Set.
|
16
|
-
def initialize(key,
|
16
|
+
def initialize(key, *args)
|
17
17
|
@key = key
|
18
|
-
@
|
19
|
-
@
|
18
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
19
|
+
@redis = args.first || $redis
|
20
20
|
end
|
21
21
|
|
22
22
|
# Works like add. Can chain together: list << 'a' << 'b'
|
@@ -0,0 +1,275 @@
|
|
1
|
+
class Redis
|
2
|
+
#
|
3
|
+
# Class representing a sorted set.
|
4
|
+
#
|
5
|
+
class SortedSet
|
6
|
+
# require 'enumerator'
|
7
|
+
# include Enumerable
|
8
|
+
require 'redis/helpers/core_commands'
|
9
|
+
include Redis::Helpers::CoreCommands
|
10
|
+
require 'redis/helpers/serialize'
|
11
|
+
include Redis::Helpers::Serialize
|
12
|
+
|
13
|
+
attr_reader :key, :options, :redis
|
14
|
+
|
15
|
+
# Create a new SortedSet.
|
16
|
+
def initialize(key, *args)
|
17
|
+
@key = key
|
18
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
19
|
+
@redis = args.first || $redis
|
20
|
+
end
|
21
|
+
|
22
|
+
# How to add values using a sorted set. The key is the member, eg,
|
23
|
+
# "Peter", and the value is the score, eg, 163. So:
|
24
|
+
# num_posts['Peter'] = 163
|
25
|
+
def []=(member, score)
|
26
|
+
add(member, score)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add a member and its corresponding value to Redis. Note that the
|
30
|
+
# arguments to this are flipped; the member comes first rather than
|
31
|
+
# the score, since the member is the unique item (not the score).
|
32
|
+
def add(member, score)
|
33
|
+
redis.zadd(key, score, to_redis(member))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Same functionality as Ruby arrays. If a single number is given, return
|
37
|
+
# just the element at that index using Redis: ZRANGE. Otherwise, return
|
38
|
+
# a range of values using Redis: ZRANGE.
|
39
|
+
def [](index, length=nil)
|
40
|
+
if index.is_a? Range
|
41
|
+
range(index.first, index.last)
|
42
|
+
elsif length
|
43
|
+
range(index, length)
|
44
|
+
else
|
45
|
+
at(index)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return the score of the specified element of the sorted set at key. If the
|
50
|
+
# specified element does not exist in the sorted set, or the key does not exist
|
51
|
+
# at all, nil is returned. Redis: ZSCORE.
|
52
|
+
def score(member)
|
53
|
+
redis.zscore(key, to_redis(member)).to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the rank of the member in the sorted set, with scores ordered from
|
57
|
+
# low to high. +revrank+ returns the rank with scores ordered from high to low.
|
58
|
+
# When the given member does not exist in the sorted set, nil is returned.
|
59
|
+
# The returned rank (or index) of the member is 0-based for both commands
|
60
|
+
def rank(member)
|
61
|
+
redis.zrank(key, to_redis(member)).to_i
|
62
|
+
end
|
63
|
+
|
64
|
+
def revrank(member)
|
65
|
+
redis.zrevrank(key, to_redis(member)).to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return all members of the sorted set with their scores. Extremely CPU-intensive.
|
69
|
+
# Better to use a range instead.
|
70
|
+
def members(options={})
|
71
|
+
range(0, -1, options)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return a range of values from +start_index+ to +end_index+. Can also use
|
75
|
+
# the familiar list[start,end] Ruby syntax. Redis: ZRANGE
|
76
|
+
def range(start_index, end_index, options={})
|
77
|
+
if options[:withscores]
|
78
|
+
val = from_redis redis.zrange(key, start_index, end_index, 'withscores')
|
79
|
+
ret = []
|
80
|
+
while k = val.shift and v = val.shift
|
81
|
+
ret << [k, v.to_i]
|
82
|
+
end
|
83
|
+
ret
|
84
|
+
else
|
85
|
+
from_redis redis.zrange(key, start_index, end_index)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return a range of values from +start_index+ to +end_index+ in reverse order. Redis: ZREVRANGE
|
90
|
+
def revrange(start_index, end_index, options={})
|
91
|
+
if options[:withscores]
|
92
|
+
val = from_redis redis.zrevrange(key, start_index, end_index, 'withscores')
|
93
|
+
ret = []
|
94
|
+
while k = val.shift and v = val.shift
|
95
|
+
ret << [k, v.to_i]
|
96
|
+
end
|
97
|
+
ret
|
98
|
+
else
|
99
|
+
from_redis redis.zrevrange(key, start_index, end_index)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return the all the elements in the sorted set at key with a score between min and max
|
104
|
+
# (including elements with score equal to min or max). Options:
|
105
|
+
# :count, :offset - passed to LIMIT
|
106
|
+
# :withscores - if true, scores are returned as well
|
107
|
+
# Redis: ZRANGEBYSCORE
|
108
|
+
def rangebyscore(min, max, options={})
|
109
|
+
args = []
|
110
|
+
args += ['limit', options[:offset] || 0, options[:limit] || options[:count]] if
|
111
|
+
options[:offset] || options[:limit] || options[:count]
|
112
|
+
args += ['withscores'] if options[:withscores]
|
113
|
+
from_redis redis.zrangebyscore(key, min, max, *args)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Forwards compat (not yet implemented in Redis)
|
117
|
+
def revrangebyscore(min, max, options={})
|
118
|
+
args = []
|
119
|
+
args += ['limit', options[:offset] || 0, options[:limit] || options[:count]] if
|
120
|
+
options[:offset] || options[:limit] || options[:count]
|
121
|
+
args += ['withscores'] if options[:withscores]
|
122
|
+
from_redis redis.zrevrangebyscore(key, min, max, *args)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Remove all elements in the sorted set at key with rank between start and end. Start and end are
|
126
|
+
# 0-based with rank 0 being the element with the lowest score. Both start and end can be negative
|
127
|
+
# numbers, where they indicate offsets starting at the element with the highest rank. For example:
|
128
|
+
# -1 is the element with the highest score, -2 the element with the second highest score and so forth.
|
129
|
+
# Redis: ZREMRANGEBYRANK
|
130
|
+
def remrangebyrank(min, max)
|
131
|
+
redis.zremrangebyrank(key, min, max)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Remove all the elements in the sorted set at key with a score between min and max (including
|
135
|
+
# elements with score equal to min or max). Redis: ZREMRANGEBYSCORE
|
136
|
+
def remrangebyscore(min, max)
|
137
|
+
redis.zremrangebyscore(key, min, max)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Delete the value from the set. Redis: ZREM
|
141
|
+
def delete(value)
|
142
|
+
redis.zrem(key, value)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Increment the rank of that member atomically and return the new value. This
|
146
|
+
# method is aliased as incr() for brevity. Redis: ZINCRBY
|
147
|
+
def increment(member, by=1)
|
148
|
+
redis.zincrby(key, by, member).to_i
|
149
|
+
end
|
150
|
+
alias_method :incr, :increment
|
151
|
+
alias_method :incrby, :increment
|
152
|
+
|
153
|
+
# Convenience to calling increment() with a negative number.
|
154
|
+
def decrement(by=1)
|
155
|
+
redis.zincrby(key, -by).to_i
|
156
|
+
end
|
157
|
+
alias_method :decr, :decrement
|
158
|
+
alias_method :decrby, :decrement
|
159
|
+
|
160
|
+
# Return the intersection with another set. Can pass it either another set
|
161
|
+
# object or set name. Also available as & which is a bit cleaner:
|
162
|
+
#
|
163
|
+
# members_in_both = set1 & set2
|
164
|
+
#
|
165
|
+
# If you want to specify multiple sets, you must use +intersection+:
|
166
|
+
#
|
167
|
+
# members_in_all = set1.intersection(set2, set3, set4)
|
168
|
+
# members_in_all = set1.inter(set2, set3, set4) # alias
|
169
|
+
#
|
170
|
+
# Redis: SINTER
|
171
|
+
def intersection(*sets)
|
172
|
+
from_redis redis.zinter(key, *keys_from_objects(sets))
|
173
|
+
end
|
174
|
+
alias_method :intersect, :intersection
|
175
|
+
alias_method :inter, :intersection
|
176
|
+
alias_method :&, :intersection
|
177
|
+
|
178
|
+
# Calculate the intersection and store it in Redis as +name+. Returns the number
|
179
|
+
# of elements in the stored intersection. Redis: SUNIONSTORE
|
180
|
+
def interstore(name, *sets)
|
181
|
+
redis.zinterstore(name, key, *keys_from_objects(sets))
|
182
|
+
end
|
183
|
+
|
184
|
+
# Return the union with another set. Can pass it either another set
|
185
|
+
# object or set name. Also available as | and + which are a bit cleaner:
|
186
|
+
#
|
187
|
+
# members_in_either = set1 | set2
|
188
|
+
# members_in_either = set1 + set2
|
189
|
+
#
|
190
|
+
# If you want to specify multiple sets, you must use +union+:
|
191
|
+
#
|
192
|
+
# members_in_all = set1.union(set2, set3, set4)
|
193
|
+
#
|
194
|
+
# Redis: SUNION
|
195
|
+
def union(*sets)
|
196
|
+
from_redis redis.zunion(key, *keys_from_objects(sets))
|
197
|
+
end
|
198
|
+
alias_method :|, :union
|
199
|
+
alias_method :+, :union
|
200
|
+
|
201
|
+
# Calculate the union and store it in Redis as +name+. Returns the number
|
202
|
+
# of elements in the stored union. Redis: SUNIONSTORE
|
203
|
+
def unionstore(name, *sets)
|
204
|
+
redis.zunionstore(name, key, *keys_from_objects(sets))
|
205
|
+
end
|
206
|
+
|
207
|
+
# Return the difference vs another set. Can pass it either another set
|
208
|
+
# object or set name. Also available as ^ or - which is a bit cleaner:
|
209
|
+
#
|
210
|
+
# members_difference = set1 ^ set2
|
211
|
+
# members_difference = set1 - set2
|
212
|
+
#
|
213
|
+
# If you want to specify multiple sets, you must use +difference+:
|
214
|
+
#
|
215
|
+
# members_difference = set1.difference(set2, set3, set4)
|
216
|
+
# members_difference = set1.diff(set2, set3, set4)
|
217
|
+
#
|
218
|
+
# Redis: SDIFF
|
219
|
+
def difference(*sets)
|
220
|
+
from_redis redis.zdiff(key, *keys_from_objects(sets))
|
221
|
+
end
|
222
|
+
alias_method :diff, :difference
|
223
|
+
alias_method :^, :difference
|
224
|
+
alias_method :-, :difference
|
225
|
+
|
226
|
+
# Calculate the diff and store it in Redis as +name+. Returns the number
|
227
|
+
# of elements in the stored union. Redis: SDIFFSTORE
|
228
|
+
def diffstore(name, *sets)
|
229
|
+
redis.zdiffstore(name, key, *keys_from_objects(sets))
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns true if the set has no members. Redis: SCARD == 0
|
233
|
+
def empty?
|
234
|
+
length == 0
|
235
|
+
end
|
236
|
+
|
237
|
+
def ==(x)
|
238
|
+
members == x
|
239
|
+
end
|
240
|
+
|
241
|
+
def to_s
|
242
|
+
members.join(', ')
|
243
|
+
end
|
244
|
+
|
245
|
+
# Return the value at the given index. Can also use familiar list[index] syntax.
|
246
|
+
# Redis: ZRANGE
|
247
|
+
def at(index)
|
248
|
+
range(index, index).first
|
249
|
+
end
|
250
|
+
|
251
|
+
# Return the first element in the list. Redis: ZRANGE(0)
|
252
|
+
def first
|
253
|
+
at(0)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Return the last element in the list. Redis: ZRANGE(-1)
|
257
|
+
def last
|
258
|
+
at(-1)
|
259
|
+
end
|
260
|
+
|
261
|
+
# The number of members in the set. Aliased as size. Redis: ZCARD
|
262
|
+
def length
|
263
|
+
redis.zcard(key)
|
264
|
+
end
|
265
|
+
alias_method :size, :length
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
def keys_from_objects(sets)
|
270
|
+
raise ArgumentError, "Must pass in one or more set names" if sets.empty?
|
271
|
+
sets.collect{|set| set.is_a?(Redis::SortedSet) ? set.key : set}
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
end
|
data/lib/redis/value.rb
CHANGED
@@ -9,10 +9,10 @@ class Redis
|
|
9
9
|
include Redis::Helpers::Serialize
|
10
10
|
|
11
11
|
attr_reader :key, :options, :redis
|
12
|
-
def initialize(key,
|
12
|
+
def initialize(key, *args)
|
13
13
|
@key = key
|
14
|
-
@
|
15
|
-
@
|
14
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
15
|
+
@redis = args.first || $redis
|
16
16
|
@redis.setnx(key, @options[:default]) if @options[:default]
|
17
17
|
end
|
18
18
|
|
@@ -3,9 +3,10 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
3
3
|
|
4
4
|
require 'redis/counter'
|
5
5
|
require 'redis/list'
|
6
|
-
require 'redis/set'
|
7
6
|
require 'redis/value'
|
8
7
|
require 'redis/lock'
|
8
|
+
require 'redis/set'
|
9
|
+
require 'redis/sorted_set'
|
9
10
|
|
10
11
|
describe Redis::Value do
|
11
12
|
before :all do
|
@@ -47,6 +48,7 @@ describe Redis::Value do
|
|
47
48
|
old.should be_nil
|
48
49
|
old.value = 'Tuff'
|
49
50
|
@value.renamenx('spec/value').should be_false
|
51
|
+
@value.value.should == 'Peter Pan'
|
50
52
|
end
|
51
53
|
|
52
54
|
after :all do
|
@@ -160,9 +162,11 @@ describe Redis::List do
|
|
160
162
|
old = Redis::List.new('spec/list')
|
161
163
|
old.should be_empty
|
162
164
|
old << 'Tuff'
|
165
|
+
old.values.should == ['Tuff']
|
163
166
|
@list.renamenx('spec/list').should be_false
|
164
167
|
@list.renamenx(old).should be_false
|
165
168
|
@list.renamenx('spec/foo').should be_true
|
169
|
+
old.values.should == ['Tuff']
|
166
170
|
@list.clear
|
167
171
|
@list.redis.del('spec/list2')
|
168
172
|
end
|
@@ -172,6 +176,134 @@ describe Redis::List do
|
|
172
176
|
end
|
173
177
|
end
|
174
178
|
|
179
|
+
describe Redis::Counter do
|
180
|
+
before :all do
|
181
|
+
@counter = Redis::Counter.new('spec/counter')
|
182
|
+
@counter2 = Redis::Counter.new('spec/counter')
|
183
|
+
end
|
184
|
+
|
185
|
+
before :each do
|
186
|
+
@counter.reset
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should support increment/decrement of counters" do
|
190
|
+
@counter.key.should == 'spec/counter'
|
191
|
+
@counter.incr(10)
|
192
|
+
@counter.should == 10
|
193
|
+
|
194
|
+
# math proxy ops
|
195
|
+
(@counter == 10).should be_true
|
196
|
+
(@counter <= 10).should be_true
|
197
|
+
(@counter < 11).should be_true
|
198
|
+
(@counter > 9).should be_true
|
199
|
+
(@counter >= 10).should be_true
|
200
|
+
"#{@counter}".should == "10"
|
201
|
+
|
202
|
+
@counter.increment.should == 11
|
203
|
+
@counter.increment.should == 12
|
204
|
+
@counter2.increment.should == 13
|
205
|
+
@counter2.increment(2).should == 15
|
206
|
+
@counter.decrement.should == 14
|
207
|
+
@counter2.decrement.should == 13
|
208
|
+
@counter.decrement.should == 12
|
209
|
+
@counter2.decrement(4).should == 8
|
210
|
+
@counter.should == 8
|
211
|
+
@counter.reset.should be_true
|
212
|
+
@counter.should == 0
|
213
|
+
@counter.reset(15).should be_true
|
214
|
+
@counter.should == 15
|
215
|
+
end
|
216
|
+
|
217
|
+
after :all do
|
218
|
+
@counter.delete
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe Redis::Lock do
|
223
|
+
before :each do
|
224
|
+
$redis.flushall
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should set the value to the expiration" do
|
228
|
+
start = Time.now
|
229
|
+
expiry = 15
|
230
|
+
lock = Redis::Lock.new(:test_lock, :expiration => expiry)
|
231
|
+
lock.lock do
|
232
|
+
expiration = $redis.get("test_lock").to_f
|
233
|
+
|
234
|
+
# The expiration stored in redis should be 15 seconds from when we started
|
235
|
+
# or a little more
|
236
|
+
expiration.should be_close((start + expiry).to_f, 2.0)
|
237
|
+
end
|
238
|
+
|
239
|
+
# key should have been cleaned up
|
240
|
+
$redis.get("test_lock").should be_nil
|
241
|
+
end
|
242
|
+
|
243
|
+
it "should set value to 1 when no expiration is set" do
|
244
|
+
lock = Redis::Lock.new(:test_lock)
|
245
|
+
lock.lock do
|
246
|
+
$redis.get('test_lock').should == '1'
|
247
|
+
end
|
248
|
+
|
249
|
+
# key should have been cleaned up
|
250
|
+
$redis.get("test_lock").should be_nil
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should let lock be gettable when lock is expired" do
|
254
|
+
expiry = 15
|
255
|
+
lock = Redis::Lock.new(:test_lock, :expiration => expiry, :timeout => 0.1)
|
256
|
+
|
257
|
+
# create a fake lock in the past
|
258
|
+
$redis.set("test_lock", Time.now-(expiry + 60))
|
259
|
+
|
260
|
+
gotit = false
|
261
|
+
lock.lock do
|
262
|
+
gotit = true
|
263
|
+
end
|
264
|
+
|
265
|
+
# should get the lock because it has expired
|
266
|
+
gotit.should be_true
|
267
|
+
$redis.get("test_lock").should be_nil
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should not let non-expired locks be gettable" do
|
271
|
+
expiry = 15
|
272
|
+
lock = Redis::Lock.new(:test_lock, :expiration => expiry, :timeout => 0.1)
|
273
|
+
|
274
|
+
# create a fake lock
|
275
|
+
$redis.set("test_lock", (Time.now + expiry).to_f)
|
276
|
+
|
277
|
+
gotit = false
|
278
|
+
error = nil
|
279
|
+
begin
|
280
|
+
lock.lock do
|
281
|
+
gotit = true
|
282
|
+
end
|
283
|
+
rescue => error
|
284
|
+
end
|
285
|
+
|
286
|
+
error.should be_kind_of(Redis::Lock::LockTimeout)
|
287
|
+
|
288
|
+
# should not have the lock
|
289
|
+
gotit.should_not be_true
|
290
|
+
|
291
|
+
# lock value should still be set
|
292
|
+
$redis.get("test_lock").should_not be_nil
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should not remove the key if lock is held past expiration" do
|
296
|
+
lock = Redis::Lock.new(:test_lock, :expiration => 0.0)
|
297
|
+
|
298
|
+
lock.lock do
|
299
|
+
sleep 1.1
|
300
|
+
end
|
301
|
+
|
302
|
+
# lock value should still be set since the lock was held for more than the expiry
|
303
|
+
$redis.get("test_lock").should_not be_nil
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
175
307
|
describe Redis::Set do
|
176
308
|
before :all do
|
177
309
|
@set = Redis::Set.new('spec/set')
|
@@ -289,132 +421,157 @@ describe Redis::Set do
|
|
289
421
|
end
|
290
422
|
end
|
291
423
|
|
292
|
-
describe Redis::
|
424
|
+
describe Redis::SortedSet do
|
293
425
|
before :all do
|
294
|
-
@
|
295
|
-
@
|
426
|
+
@set = Redis::SortedSet.new('spec/zset')
|
427
|
+
@set_1 = Redis::SortedSet.new('spec/zset_1')
|
428
|
+
@set_2 = Redis::SortedSet.new('spec/zset_2')
|
429
|
+
@set_3 = Redis::SortedSet.new('spec/zset_3')
|
296
430
|
end
|
297
431
|
|
298
432
|
before :each do
|
299
|
-
@
|
433
|
+
@set.clear
|
434
|
+
@set_1.clear
|
435
|
+
@set_2.clear
|
436
|
+
@set_3.clear
|
300
437
|
end
|
301
438
|
|
302
|
-
it "should
|
303
|
-
@
|
304
|
-
@
|
305
|
-
@
|
439
|
+
it "should handle sets of simple values" do
|
440
|
+
@set.should be_empty
|
441
|
+
@set['a'] = 11
|
442
|
+
@set['a'] = 21
|
443
|
+
@set.add('a', 5)
|
444
|
+
@set.score('a').should == 5
|
445
|
+
@set['a'] = 3
|
446
|
+
@set['b'] = 5
|
447
|
+
@set['c'] = 4
|
448
|
+
|
449
|
+
@set[0,-1].should == ['a','c','b']
|
450
|
+
@set.range(0,-1).should == ['a','c','b']
|
451
|
+
@set.revrange(0,-1).should == ['b','c','a']
|
452
|
+
@set[0..1].should == ['a','c']
|
453
|
+
@set[1].should == 'c'
|
454
|
+
@set.at(1).should == 'c'
|
455
|
+
@set.first.should == 'a'
|
456
|
+
@set.last.should == 'b'
|
457
|
+
|
458
|
+
@set.members.should == ['a','c','b']
|
459
|
+
@set.members(:withscores => true).should == [['a',3],['c',4],['b',5]]
|
460
|
+
|
461
|
+
@set['b'] = 5
|
462
|
+
@set['b'] = 6
|
463
|
+
@set.score('b').should == 6
|
464
|
+
@set.delete('c')
|
465
|
+
@set.to_s.should == 'a, b'
|
466
|
+
@set.should == ['a','b']
|
467
|
+
@set.members.should == ['a','b']
|
468
|
+
@set['d'] = 0
|
306
469
|
|
307
|
-
|
308
|
-
(
|
309
|
-
(
|
310
|
-
(
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
@
|
316
|
-
@
|
317
|
-
@
|
318
|
-
|
319
|
-
@
|
320
|
-
@
|
321
|
-
@
|
322
|
-
@
|
323
|
-
@
|
324
|
-
@
|
325
|
-
@
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
@
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
end
|
340
|
-
|
341
|
-
it "should set the value to the expiration" do
|
342
|
-
start = Time.now
|
343
|
-
expiry = 15
|
344
|
-
lock = Redis::Lock.new(:test_lock, $redis, :expiration => expiry, :init => false)
|
345
|
-
lock.lock do
|
346
|
-
expiration = $redis.get("test_lock").to_f
|
347
|
-
|
348
|
-
# The expiration stored in redis should be 15 seconds from when we started
|
349
|
-
# or a little more
|
350
|
-
expiration.should be_close((start + expiry).to_f, 2.0)
|
351
|
-
end
|
352
|
-
|
353
|
-
# key should have been cleaned up
|
354
|
-
$redis.get("test_lock").should be_nil
|
470
|
+
@set.rangebyscore(0, 4).should == ['d','a']
|
471
|
+
@set.rangebyscore(0, 4, :count => 1).should == ['d']
|
472
|
+
@set.rangebyscore(0, 4, :count => 2).should == ['d','a']
|
473
|
+
@set.rangebyscore(0, 4, :limit => 2).should == ['d','a']
|
474
|
+
|
475
|
+
# Redis 1.3.5
|
476
|
+
# @set.rangebyscore(0,4, :withscores => true).should == [['d',4],['a',3]]
|
477
|
+
# @set.revrangebyscore(0,4).should == ['d','a']
|
478
|
+
# @set.revrangebyscore(0,4, :count => 2).should == ['a','d']
|
479
|
+
# @set.rank('b').should == 2
|
480
|
+
# @set.revrank('b').should == 3
|
481
|
+
|
482
|
+
@set['f'] = 100
|
483
|
+
@set['g'] = 110
|
484
|
+
@set['h'] = 120
|
485
|
+
@set['j'] = 130
|
486
|
+
@set.incr('h', 20)
|
487
|
+
@set.remrangebyscore(100, 120)
|
488
|
+
@set.members.should == ['d','a','b','j','h']
|
489
|
+
|
490
|
+
# Redis 1.3.5
|
491
|
+
# @set['h'] = 12
|
492
|
+
# @set['j'] = 13
|
493
|
+
# @set.remrangebyrank(4,-1)
|
494
|
+
# @set.members.should == ['d','a','b']
|
495
|
+
|
496
|
+
@set.delete('d')
|
497
|
+
@set['c'] = 200
|
498
|
+
@set.members.should == ['a','b','j','h','c']
|
499
|
+
@set.delete('c')
|
500
|
+
@set.length.should == 4
|
501
|
+
@set.size.should == 4
|
355
502
|
end
|
356
503
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
504
|
+
# Not until Redis 1.3.5 with hashes
|
505
|
+
xit "Redis 1.3.5: should handle set intersections, unions, and diffs" do
|
506
|
+
@set_1['a'] = 5
|
507
|
+
@set_2['b'] = 18
|
508
|
+
@set_2['c'] = 12
|
362
509
|
|
363
|
-
|
364
|
-
|
365
|
-
|
510
|
+
@set_2['a'] = 10
|
511
|
+
@set_2['b'] = 15
|
512
|
+
@set_2['c'] = 15
|
366
513
|
|
367
|
-
|
368
|
-
expiry = 15
|
369
|
-
lock = Redis::Lock.new(:test_lock, $redis, :expiration => expiry, :timeout => 0.1, :init => false)
|
514
|
+
(@set_1 & @set_2).sort.should == ['c','d','e']
|
370
515
|
|
371
|
-
|
372
|
-
|
516
|
+
@set_1 << 'a' << 'b' << 'c' << 'd' << 'e'
|
517
|
+
@set_2 << 'c' << 'd' << 'e' << 'f' << 'g'
|
518
|
+
@set_3 << 'a' << 'd' << 'g' << 'l' << 'm'
|
519
|
+
@set_1.sort.should == %w(a b c d e)
|
520
|
+
@set_2.sort.should == %w(c d e f g)
|
521
|
+
@set_3.sort.should == %w(a d g l m)
|
522
|
+
(@set_1 & @set_2).sort.should == ['c','d','e']
|
523
|
+
@set_1.intersection(@set_2).sort.should == ['c','d','e']
|
524
|
+
@set_1.intersection(@set_2, @set_3).sort.should == ['d']
|
525
|
+
@set_1.intersect(@set_2).sort.should == ['c','d','e']
|
526
|
+
@set_1.inter(@set_2, @set_3).sort.should == ['d']
|
527
|
+
@set_1.interstore(INTERSTORE_KEY, @set_2).should == 3
|
528
|
+
@set_1.redis.smembers(INTERSTORE_KEY).sort.should == ['c','d','e']
|
529
|
+
@set_1.interstore(INTERSTORE_KEY, @set_2, @set_3).should == 1
|
530
|
+
@set_1.redis.smembers(INTERSTORE_KEY).sort.should == ['d']
|
373
531
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
532
|
+
(@set_1 | @set_2).sort.should == ['a','b','c','d','e','f','g']
|
533
|
+
(@set_1 + @set_2).sort.should == ['a','b','c','d','e','f','g']
|
534
|
+
@set_1.union(@set_2).sort.should == ['a','b','c','d','e','f','g']
|
535
|
+
@set_1.union(@set_2, @set_3).sort.should == ['a','b','c','d','e','f','g','l','m']
|
536
|
+
@set_1.unionstore(UNIONSTORE_KEY, @set_2).should == 7
|
537
|
+
@set_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g']
|
538
|
+
@set_1.unionstore(UNIONSTORE_KEY, @set_2, @set_3).should == 9
|
539
|
+
@set_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g','l','m']
|
378
540
|
|
379
|
-
|
380
|
-
|
381
|
-
|
541
|
+
(@set_1 ^ @set_2).sort.should == ["a", "b"]
|
542
|
+
(@set_1 - @set_2).sort.should == ["a", "b"]
|
543
|
+
(@set_2 - @set_1).sort.should == ["f", "g"]
|
544
|
+
@set_1.difference(@set_2).sort.should == ["a", "b"]
|
545
|
+
@set_1.diff(@set_2).sort.should == ["a", "b"]
|
546
|
+
@set_1.difference(@set_2, @set_3).sort.should == ['b']
|
547
|
+
@set_1.diffstore(DIFFSTORE_KEY, @set_2).should == 2
|
548
|
+
@set_1.redis.smembers(DIFFSTORE_KEY).sort.should == ['a','b']
|
549
|
+
@set_1.diffstore(DIFFSTORE_KEY, @set_2, @set_3).should == 1
|
550
|
+
@set_1.redis.smembers(DIFFSTORE_KEY).sort.should == ['b']
|
382
551
|
end
|
383
552
|
|
384
|
-
it "should
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
error.should be_kind_of(Redis::Lock::LockTimeout)
|
401
|
-
|
402
|
-
# should not have the lock
|
403
|
-
gotit.should_not be_true
|
404
|
-
|
405
|
-
# lock value should still be set
|
406
|
-
$redis.get("test_lock").should_not be_nil
|
553
|
+
it "should support renaming sets" do
|
554
|
+
@set.should be_empty
|
555
|
+
@set['zynga'] = 151
|
556
|
+
@set['playfish'] = 202
|
557
|
+
@set.members.should == ['zynga','playfish']
|
558
|
+
@set.key.should == 'spec/zset'
|
559
|
+
@set.rename('spec/zset2').should be_true
|
560
|
+
@set.key.should == 'spec/zset2'
|
561
|
+
old = Redis::SortedSet.new('spec/zset')
|
562
|
+
old.should be_empty
|
563
|
+
old['tuff'] = 54
|
564
|
+
@set.renamenx('spec/zset').should be_false
|
565
|
+
@set.renamenx(old).should be_false
|
566
|
+
@set.renamenx('spec/zfoo').should be_true
|
567
|
+
@set.clear
|
568
|
+
@set.redis.del('spec/zset2')
|
407
569
|
end
|
408
570
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
end
|
415
|
-
|
416
|
-
# lock value should still be set since the lock was held for more than the expiry
|
417
|
-
$redis.get("test_lock").should_not be_nil
|
571
|
+
after :all do
|
572
|
+
@set.clear
|
573
|
+
@set_1.clear
|
574
|
+
@set_2.clear
|
575
|
+
@set_3.clear
|
418
576
|
end
|
419
|
-
|
420
|
-
end
|
577
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-objects
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
- 4
|
9
|
+
version: 0.2.4
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Nate Wiger
|
@@ -9,19 +14,23 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-
|
17
|
+
date: 2010-04-09 00:00:00 -07:00
|
13
18
|
default_executable:
|
14
19
|
dependencies:
|
15
20
|
- !ruby/object:Gem::Dependency
|
16
21
|
name: redis
|
17
|
-
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
24
|
requirements:
|
21
25
|
- - ">="
|
22
26
|
- !ruby/object:Gem::Version
|
23
|
-
|
24
|
-
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 0
|
30
|
+
- 3
|
31
|
+
version: 1.0.3
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
25
34
|
description: Map Redis types directly to Ruby objects. Works with any class or ORM.
|
26
35
|
email: nate@wiger.org
|
27
36
|
executables: []
|
@@ -42,9 +51,11 @@ files:
|
|
42
51
|
- lib/redis/objects/lists.rb
|
43
52
|
- lib/redis/objects/locks.rb
|
44
53
|
- lib/redis/objects/sets.rb
|
54
|
+
- lib/redis/objects/sorted_sets.rb
|
45
55
|
- lib/redis/objects/values.rb
|
46
56
|
- lib/redis/objects.rb
|
47
57
|
- lib/redis/set.rb
|
58
|
+
- lib/redis/sorted_set.rb
|
48
59
|
- lib/redis/value.rb
|
49
60
|
- spec/redis_objects_instance_spec.rb
|
50
61
|
- spec/redis_objects_model_spec.rb
|
@@ -66,18 +77,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
77
|
requirements:
|
67
78
|
- - ">="
|
68
79
|
- !ruby/object:Gem::Version
|
80
|
+
segments:
|
81
|
+
- 0
|
69
82
|
version: "0"
|
70
|
-
version:
|
71
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
84
|
requirements:
|
73
85
|
- - ">="
|
74
86
|
- !ruby/object:Gem::Version
|
87
|
+
segments:
|
88
|
+
- 0
|
75
89
|
version: "0"
|
76
|
-
version:
|
77
90
|
requirements:
|
78
|
-
- redis,
|
91
|
+
- redis, v1.0.3 or greater
|
79
92
|
rubyforge_project: redis-objects
|
80
|
-
rubygems_version: 1.3.
|
93
|
+
rubygems_version: 1.3.6
|
81
94
|
signing_key:
|
82
95
|
specification_version: 3
|
83
96
|
summary: Maps Redis types to Ruby objects
|