redis-objects 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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:
|