redis-objects 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +108 -68
- data/lib/redis/set.rb +57 -0
- data/spec/redis_objects_instance_spec.rb +18 -0
- data/spec/redis_objects_model_spec.rb +40 -1
- metadata +15 -5
data/README.rdoc
CHANGED
@@ -6,97 +6,43 @@ the point.
|
|
6
6
|
The killer feature of Redis that it allows you to perform atomic operations
|
7
7
|
on _individual_ data structures, like counters, lists, and sets. You can then use
|
8
8
|
these *with* your existing ActiveRecord/DataMapper/etc models, or in classes that have
|
9
|
-
nothing to do with an ORM or even a database.
|
9
|
+
nothing to do with an ORM or even a database. This gem maps Ezra's +redis+ library
|
10
|
+
to Ruby objects to make use seamless.
|
10
11
|
|
11
12
|
This gem originally arose out of a need for high-concurrency atomic operations;
|
12
13
|
for a fun rant on the topic, see
|
13
14
|
{ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc],
|
14
15
|
or scroll down to "Atomicity" in this README.
|
15
16
|
|
16
|
-
There are two ways to use Redis::Objects, either as an +include+ in a class,
|
17
|
-
by using +new+ with the type of data structure you want to create.
|
17
|
+
There are two ways to use Redis::Objects, either as an +include+ in a model class,
|
18
|
+
or by using +new+ with the type of data structure you want to create.
|
18
19
|
|
19
20
|
== Installation
|
20
21
|
|
21
22
|
gem install gemcutter
|
22
23
|
gem tumble
|
23
24
|
gem install redis-objects
|
24
|
-
|
25
|
-
== Example 1: Class Usage
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
# If on Rails, config/initializers/redis.rb is a good place for this
|
30
|
-
require 'redis'
|
31
|
-
require 'redis/objects'
|
32
|
-
Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
|
33
|
-
|
34
|
-
=== Model Class
|
26
|
+
== Example 1: Individual Usage
|
35
27
|
|
36
|
-
|
37
|
-
|
38
|
-
class Team < ActiveRecord::Base
|
39
|
-
include Redis::Objects
|
40
|
-
|
41
|
-
counter :hits
|
42
|
-
counter :runs
|
43
|
-
counter :outs
|
44
|
-
counter :inning, :start => 1
|
45
|
-
list :on_base
|
46
|
-
set :outfielders
|
47
|
-
value :at_bat
|
48
|
-
end
|
49
|
-
|
50
|
-
Familiar Ruby array operations Just Work (TM):
|
51
|
-
|
52
|
-
@team = Team.find(1)
|
53
|
-
@team.on_base << 'player1'
|
54
|
-
@team.on_base << 'player2'
|
55
|
-
@team.on_base << 'player3'
|
56
|
-
@team.on_base # ['player1', 'player2']
|
57
|
-
@team.on_base.pop
|
58
|
-
@team.on_base.shift
|
59
|
-
@team.on_base.length # 1
|
60
|
-
@team.on_base.delete('player3')
|
61
|
-
|
62
|
-
Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
|
63
|
-
|
64
|
-
@team.outfielders << 'outfielder1' << 'outfielder1'
|
65
|
-
@team.outfielders << 'outfielder2'
|
66
|
-
@team.outfielders # ['outfielder1', 'outfielder2']
|
67
|
-
@team.outfielders.each do |player|
|
68
|
-
puts player
|
69
|
-
end
|
70
|
-
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
71
|
-
|
72
|
-
Note counters cannot be assigned to, only incremented/decremented:
|
73
|
-
|
74
|
-
@team.hits.increment # or incr
|
75
|
-
@team.hits.decrement # or decr
|
76
|
-
@team.runs = 4 # exception
|
77
|
-
@team.runs += 1 # exception
|
78
|
-
|
79
|
-
It would be cool to get that last one working, but Ruby's implementation of +=
|
80
|
-
is problematic.
|
81
|
-
|
82
|
-
== Example 2: Instance Usage
|
83
|
-
|
84
|
-
Each data type can be used independently.
|
28
|
+
There is a Ruby object that maps to each Redis type.
|
85
29
|
|
86
30
|
=== Initialization
|
87
31
|
|
88
|
-
|
32
|
+
You can either set the $redis global variable to your Redis handle:
|
89
33
|
|
90
34
|
$redis = Redis.new(:host => 'localhost', :port => 6379)
|
91
35
|
@value = Redis::Value.new('myvalue')
|
92
36
|
|
93
|
-
Or can pass the handle into the new method:
|
94
|
-
|
37
|
+
Or you can pass the Redis handle into the new method:
|
38
|
+
|
95
39
|
redis = Redis.new(:host => 'localhost', :port => 6379)
|
96
40
|
@value = Redis::Value.new('myvalue', redis)
|
97
41
|
|
98
42
|
=== Counters
|
99
43
|
|
44
|
+
Create a new counter. The +counter_name+ is the key stored in Redis.
|
45
|
+
|
100
46
|
@counter = Redis::Counter.new('counter_name')
|
101
47
|
@counter.increment
|
102
48
|
@counter.decrement
|
@@ -109,13 +55,18 @@ See the section on "Atomicity" for cool uses of atomic counter blocks.
|
|
109
55
|
|
110
56
|
=== Lists
|
111
57
|
|
112
|
-
|
58
|
+
Lists work just like Ruby arrays:
|
113
59
|
|
114
60
|
@list = Redis::List.new('list_name')
|
115
61
|
@list << 'a'
|
116
62
|
@list << 'b'
|
117
63
|
@list.include? 'c' # false
|
118
64
|
@list.values # ['a','b']
|
65
|
+
@list << 'c'
|
66
|
+
@list.delete('c')
|
67
|
+
@list[0]
|
68
|
+
@list[0,1]
|
69
|
+
@list[0..1]
|
119
70
|
@list.shift
|
120
71
|
@list.pop
|
121
72
|
@list.clear
|
@@ -128,7 +79,7 @@ Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
|
|
128
79
|
@set = Redis::Set.new('set_name')
|
129
80
|
@set << 'a'
|
130
81
|
@set << 'b'
|
131
|
-
@set << 'a'
|
82
|
+
@set << 'a' # dup ignored
|
132
83
|
@set.member? 'c' # false
|
133
84
|
@set.members # ['a','b']
|
134
85
|
@set.each do |member|
|
@@ -137,16 +88,105 @@ Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
|
|
137
88
|
@set.clear
|
138
89
|
# etc
|
139
90
|
|
91
|
+
You can perform Redis intersections/unions easily:
|
92
|
+
|
93
|
+
@set1 = Redis::Set.new('set1')
|
94
|
+
@set2 = Redis::Set.new('set2')
|
95
|
+
@set3 = Redis::Set.new('set3')
|
96
|
+
members = @set1 & @set2 # intersection
|
97
|
+
members = @set1 | @set2 # union
|
98
|
+
members = @set1 + @set2 # union
|
99
|
+
members = @set1.intersection(@set2, @set3) # multiple
|
100
|
+
members = @set1.union(@set2, @set3) # multiple
|
101
|
+
|
102
|
+
Or store them in Redis:
|
103
|
+
|
104
|
+
@set1.interstore('intername', @set2, @set3)
|
105
|
+
members = @set1.redis.get('intername')
|
106
|
+
@set1.unionstore('unionname', @set2, @set3)
|
107
|
+
members = @set1.redis.get('unionname')
|
108
|
+
|
140
109
|
=== Values
|
141
110
|
|
111
|
+
Simple values are easy as well:
|
112
|
+
|
142
113
|
@value = Redis::Value.new('value_name')
|
143
114
|
@value.value = 'a'
|
144
115
|
@value.delete
|
145
116
|
|
117
|
+
== Example 2: Class Usage
|
118
|
+
|
119
|
+
This method of use makes it easy to integrate Redis types with an existing
|
120
|
+
ActiveRecord model, DataMapper resource, or other class. Redis::Objects
|
121
|
+
will work with any class that provides an +id+ method that returns a unique
|
122
|
+
value. Redis::Objects will automatically create keys that are unique to
|
123
|
+
each object.
|
124
|
+
|
125
|
+
=== Initialization
|
126
|
+
|
127
|
+
If on Rails, config/initializers/redis.rb is a good place for this:
|
128
|
+
|
129
|
+
require 'redis'
|
130
|
+
require 'redis/objects'
|
131
|
+
Redis::Objects.redis = Redis.new(:host => 127.0.0.1, :port => 6379)
|
132
|
+
|
133
|
+
=== Model Class
|
134
|
+
|
135
|
+
Include Redis::Objects in any type of class:
|
136
|
+
|
137
|
+
class Team < ActiveRecord::Base
|
138
|
+
include Redis::Objects
|
139
|
+
|
140
|
+
counter :hits
|
141
|
+
counter :runs
|
142
|
+
counter :outs
|
143
|
+
counter :inning, :start => 1
|
144
|
+
list :on_base
|
145
|
+
set :outfielders
|
146
|
+
value :at_bat
|
147
|
+
end
|
148
|
+
|
149
|
+
Familiar Ruby array operations Just Work (TM):
|
150
|
+
|
151
|
+
@team = Team.find_by_name('New York Yankees')
|
152
|
+
@team.on_base << 'player1'
|
153
|
+
@team.on_base << 'player2'
|
154
|
+
@team.on_base << 'player3'
|
155
|
+
@team.on_base # ['player1', 'player2']
|
156
|
+
@team.on_base.pop
|
157
|
+
@team.on_base.shift
|
158
|
+
@team.on_base.length # 1
|
159
|
+
@team.on_base.delete('player3')
|
160
|
+
|
161
|
+
Sets work like the Ruby {Set}[http://ruby-doc.org/core/classes/Set.html] class:
|
162
|
+
|
163
|
+
@team.outfielders << 'outfielder1' << 'outfielder1'
|
164
|
+
@team.outfielders << 'outfielder2'
|
165
|
+
@team.outfielders # ['outfielder1', 'outfielder2']
|
166
|
+
@team.outfielders.each do |player|
|
167
|
+
puts player
|
168
|
+
end
|
169
|
+
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
170
|
+
|
171
|
+
Note counters cannot be assigned to, only incremented/decremented:
|
172
|
+
|
173
|
+
@team.hits.increment # or incr
|
174
|
+
@team.hits.decrement # or decr
|
175
|
+
@team.hits.incr(3) # add 3
|
176
|
+
@team.runs = 4 # exception
|
177
|
+
|
178
|
+
For free, you get a +redis+ handle usable in your class:
|
179
|
+
|
180
|
+
@team.redis.get('somekey')
|
181
|
+
@team.redis.smembers('someset')
|
182
|
+
|
183
|
+
You can call any operation supported by {Redis}[http://code.google.com/p/redis/wiki/CommandReference]
|
184
|
+
|
146
185
|
== Atomicity
|
147
186
|
|
148
187
|
You are probably not handling atomicity correctly in your app. For a fun rant
|
149
|
-
on the topic, see
|
188
|
+
on the topic, see
|
189
|
+
{ATOMICITY}[http://github.com/nateware/redis-objects/blob/master/ATOMICITY.rdoc].
|
150
190
|
|
151
191
|
Atomic counters are a good way to handle concurrency:
|
152
192
|
|
data/lib/redis/set.rb
CHANGED
@@ -7,6 +7,8 @@ class Redis
|
|
7
7
|
include Enumerable
|
8
8
|
|
9
9
|
attr_reader :key, :options, :redis
|
10
|
+
|
11
|
+
# Create a new Set.
|
10
12
|
def initialize(key, redis=$redis, options={})
|
11
13
|
@key = key
|
12
14
|
@redis = redis
|
@@ -54,6 +56,53 @@ class Redis
|
|
54
56
|
members.each(&block)
|
55
57
|
end
|
56
58
|
|
59
|
+
# Return the intersection with another set. Can pass it either another set
|
60
|
+
# object or set name. Also available as & which is a bit cleaner:
|
61
|
+
#
|
62
|
+
# members_in_both = set1 & set2
|
63
|
+
#
|
64
|
+
# If you want to specify multiple sets, you must use +intersection+:
|
65
|
+
#
|
66
|
+
# members_in_all = set1.intersection(set2, set3, set4)
|
67
|
+
# members_in_all = set1.inter(set2, set3, set4) # alias
|
68
|
+
#
|
69
|
+
# Redis: SINTER
|
70
|
+
def intersection(*sets)
|
71
|
+
redis.sinter(key, *keys_from_objects(sets))
|
72
|
+
end
|
73
|
+
alias_method :intersect, :intersection
|
74
|
+
alias_method :inter, :intersection
|
75
|
+
alias_method :&, :intersection
|
76
|
+
|
77
|
+
# Calculate the intersection and store it in Redis as +name+. Returns the number
|
78
|
+
# of elements in the stored intersection. Redis: SUNIONSTORE
|
79
|
+
def interstore(name, *sets)
|
80
|
+
redis.sinterstore(name, key, *keys_from_objects(sets))
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return the union with another set. Can pass it either another set
|
84
|
+
# object or set name. Also available as | and + which are a bit cleaner:
|
85
|
+
#
|
86
|
+
# members_in_either = set1 | set2
|
87
|
+
# members_in_either = set1 + set2
|
88
|
+
#
|
89
|
+
# If you want to specify multiple sets, you must use +union+:
|
90
|
+
#
|
91
|
+
# members_in_all = set1.union(set2, set3, set4)
|
92
|
+
#
|
93
|
+
# Redis: SUNION
|
94
|
+
def union(*sets)
|
95
|
+
redis.sunion(key, *keys_from_objects(sets))
|
96
|
+
end
|
97
|
+
alias_method :|, :union
|
98
|
+
alias_method :+, :union
|
99
|
+
|
100
|
+
# Calculate the union and store it in Redis as +name+. Returns the number
|
101
|
+
# of elements in the stored union. Redis: SINTERSTORE
|
102
|
+
def unionstore(name, *sets)
|
103
|
+
redis.sunionstore(name, key, *keys_from_objects(sets))
|
104
|
+
end
|
105
|
+
|
57
106
|
# The number of members in the set. Aliased as size. Redis: SCARD
|
58
107
|
def length
|
59
108
|
redis.scard(key)
|
@@ -72,5 +121,13 @@ class Redis
|
|
72
121
|
def to_s
|
73
122
|
members.join(', ')
|
74
123
|
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def keys_from_objects(sets)
|
128
|
+
raise ArgumentError, "Must pass in one or more set names" if sets.empty?
|
129
|
+
sets.collect{|set| set.is_a?(Redis::Set) ? set.key : set}
|
130
|
+
end
|
131
|
+
|
75
132
|
end
|
76
133
|
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
|
2
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
3
|
|
4
|
+
UNIONSTORE_KEY = 'test:unionstore'
|
5
|
+
INTERSTORE_KEY = 'test:interstore'
|
6
|
+
|
4
7
|
class Roster
|
5
8
|
include Redis::Objects
|
6
9
|
counter :available_slots, :start => 10
|
@@ -21,6 +24,10 @@ describe Redis::Objects do
|
|
21
24
|
before :all do
|
22
25
|
@roster = Roster.new
|
23
26
|
@roster2 = Roster.new
|
27
|
+
|
28
|
+
@roster_1 = Roster.new(1)
|
29
|
+
@roster_2 = Roster.new(2)
|
30
|
+
@roster_3 = Roster.new(3)
|
24
31
|
end
|
25
32
|
|
26
33
|
before :each do
|
@@ -31,6 +38,11 @@ describe Redis::Objects do
|
|
31
38
|
@roster.starting_pitcher.delete
|
32
39
|
@roster.player_stats.clear
|
33
40
|
@roster.outfielders.clear
|
41
|
+
@roster_1.outfielders.clear
|
42
|
+
@roster_2.outfielders.clear
|
43
|
+
@roster_3.outfielders.clear
|
44
|
+
@roster.redis.del(UNIONSTORE_KEY)
|
45
|
+
@roster.redis.del(INTERSTORE_KEY)
|
34
46
|
end
|
35
47
|
|
36
48
|
it "should provide a connection method" do
|
@@ -361,6 +373,33 @@ describe Redis::Objects do
|
|
361
373
|
coll.should == ['c']
|
362
374
|
@roster.outfielders.sort.should == ['a','b','c']
|
363
375
|
end
|
376
|
+
|
377
|
+
it "should handle set intersections and unions" do
|
378
|
+
@roster_1.outfielders << 'a' << 'b' << 'c' << 'd' << 'e'
|
379
|
+
@roster_2.outfielders << 'c' << 'd' << 'e' << 'f' << 'g'
|
380
|
+
@roster_3.outfielders << 'a' << 'd' << 'g' << 'l' << 'm'
|
381
|
+
@roster_1.outfielders.sort.should == %w(a b c d e)
|
382
|
+
@roster_2.outfielders.sort.should == %w(c d e f g)
|
383
|
+
@roster_3.outfielders.sort.should == %w(a d g l m)
|
384
|
+
(@roster_1.outfielders & @roster_2.outfielders).sort.should == ['c','d','e']
|
385
|
+
@roster_1.outfielders.intersection(@roster_2.outfielders).sort.should == ['c','d','e']
|
386
|
+
@roster_1.outfielders.intersection(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['d']
|
387
|
+
@roster_1.outfielders.intersect(@roster_2.outfielders).sort.should == ['c','d','e']
|
388
|
+
@roster_1.outfielders.inter(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['d']
|
389
|
+
@roster_1.outfielders.interstore(INTERSTORE_KEY, @roster_2.outfielders).should == 3
|
390
|
+
@roster_1.redis.smembers(INTERSTORE_KEY).sort.should == ['c','d','e']
|
391
|
+
@roster_1.outfielders.interstore(INTERSTORE_KEY, @roster_2.outfielders, @roster_3.outfielders).should == 1
|
392
|
+
@roster_1.redis.smembers(INTERSTORE_KEY).sort.should == ['d']
|
393
|
+
|
394
|
+
(@roster_1.outfielders | @roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
|
395
|
+
(@roster_1.outfielders + @roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
|
396
|
+
@roster_1.outfielders.union(@roster_2.outfielders).sort.should == ['a','b','c','d','e','f','g']
|
397
|
+
@roster_1.outfielders.union(@roster_2.outfielders, @roster_3.outfielders).sort.should == ['a','b','c','d','e','f','g','l','m']
|
398
|
+
@roster_1.outfielders.unionstore(UNIONSTORE_KEY, @roster_2.outfielders).should == 7
|
399
|
+
@roster_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g']
|
400
|
+
@roster_1.outfielders.unionstore(UNIONSTORE_KEY, @roster_2.outfielders, @roster_3.outfielders).should == 9
|
401
|
+
@roster_1.redis.smembers(UNIONSTORE_KEY).sort.should == ['a','b','c','d','e','f','g','l','m']
|
402
|
+
end
|
364
403
|
|
365
404
|
it "should provide a lock method that accepts a block" do
|
366
405
|
@roster.resort_lock.key.should == 'roster:1:resort_lock'
|
@@ -371,7 +410,7 @@ describe Redis::Objects do
|
|
371
410
|
a.should be_true
|
372
411
|
end
|
373
412
|
|
374
|
-
|
413
|
+
it "should raise an exception if the timeout is exceeded" do
|
375
414
|
@roster.redis.set(@roster.resort_lock.key, 1)
|
376
415
|
error = nil
|
377
416
|
begin
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-objects
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Wiger
|
@@ -11,8 +11,17 @@ cert_chain: []
|
|
11
11
|
|
12
12
|
date: 2009-11-26 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: redis
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.1"
|
24
|
+
version:
|
16
25
|
description: Map Redis types directly to Ruby objects. Works with any class or ORM.
|
17
26
|
email: nate@wiger.org
|
18
27
|
executables: []
|
@@ -36,6 +45,7 @@ files:
|
|
36
45
|
- lib/redis/objects.rb
|
37
46
|
- lib/redis/set.rb
|
38
47
|
- lib/redis/value.rb
|
48
|
+
- spec/redis_objects_instance_spec.rb
|
39
49
|
- spec/redis_objects_model_spec.rb
|
40
50
|
- spec/spec_helper.rb
|
41
51
|
- ATOMICITY.rdoc
|
@@ -62,8 +72,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
72
|
- !ruby/object:Gem::Version
|
63
73
|
version: "0"
|
64
74
|
version:
|
65
|
-
requirements:
|
66
|
-
|
75
|
+
requirements:
|
76
|
+
- redis, v0.1 or greater
|
67
77
|
rubyforge_project: redis-objects
|
68
78
|
rubygems_version: 1.3.5
|
69
79
|
signing_key:
|