redis-objects-legacy 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +16 -0
- data/ATOMICITY.rdoc +154 -0
- data/CHANGELOG.rdoc +362 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +600 -0
- data/Rakefile +14 -0
- data/lib/redis/base_object.rb +62 -0
- data/lib/redis/counter.rb +145 -0
- data/lib/redis/enumerable_object.rb +28 -0
- data/lib/redis/hash_key.rb +163 -0
- data/lib/redis/helpers/core_commands.rb +89 -0
- data/lib/redis/list.rb +160 -0
- data/lib/redis/lock.rb +89 -0
- data/lib/redis/objects/connection_pool_proxy.rb +31 -0
- data/lib/redis/objects/counters.rb +155 -0
- data/lib/redis/objects/hashes.rb +60 -0
- data/lib/redis/objects/lists.rb +58 -0
- data/lib/redis/objects/locks.rb +73 -0
- data/lib/redis/objects/sets.rb +58 -0
- data/lib/redis/objects/sorted_sets.rb +49 -0
- data/lib/redis/objects/values.rb +64 -0
- data/lib/redis/objects/version.rb +5 -0
- data/lib/redis/objects.rb +199 -0
- data/lib/redis/set.rb +182 -0
- data/lib/redis/sorted_set.rb +325 -0
- data/lib/redis/value.rb +65 -0
- data/lib/redis-objects-legacy.rb +1 -0
- data/spec/redis_autoload_objects_spec.rb +46 -0
- data/spec/redis_namespace_compat_spec.rb +24 -0
- data/spec/redis_objects_active_record_spec.rb +162 -0
- data/spec/redis_objects_conn_spec.rb +276 -0
- data/spec/redis_objects_custom_serializer.rb +198 -0
- data/spec/redis_objects_instance_spec.rb +1666 -0
- data/spec/redis_objects_model_spec.rb +1097 -0
- data/spec/spec_helper.rb +92 -0
- metadata +214 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
# Redis::Objects - Lightweight object layer around redis-rb
|
2
|
+
# See README.rdoc for usage and approach.
|
3
|
+
require 'redis'
|
4
|
+
require 'redis/objects/connection_pool_proxy'
|
5
|
+
|
6
|
+
class Redis
|
7
|
+
autoload :Counter, 'redis/counter'
|
8
|
+
autoload :List, 'redis/list'
|
9
|
+
autoload :Lock, 'redis/lock'
|
10
|
+
autoload :Set, 'redis/set'
|
11
|
+
autoload :SortedSet, 'redis/sorted_set'
|
12
|
+
autoload :Value, 'redis/value'
|
13
|
+
autoload :HashKey, 'redis/hash_key'
|
14
|
+
|
15
|
+
#
|
16
|
+
# Redis::Objects enables high-performance atomic operations in your app
|
17
|
+
# by leveraging the atomic features of the Redis server. To use Redis::Objects,
|
18
|
+
# first include it in any class you want. (This example uses an ActiveRecord
|
19
|
+
# subclass, but that is *not* required.) Then, use +counter+, +lock+, +set+, etc
|
20
|
+
# to define your primitives:
|
21
|
+
#
|
22
|
+
# class Game < ActiveRecord::Base
|
23
|
+
# include Redis::Objects
|
24
|
+
#
|
25
|
+
# counter :joined_players
|
26
|
+
# counter :active_players, :key => 'game:#{id}:act_plyr'
|
27
|
+
# lock :archive_game
|
28
|
+
# set :player_ids
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# The, you can use these counters both for bookkeeping and as atomic actions:
|
32
|
+
#
|
33
|
+
# @game = Game.find(id)
|
34
|
+
# @game_user = @game.joined_players.increment do |val|
|
35
|
+
# break if val > @game.max_players
|
36
|
+
# gu = @game.game_users.create!(:user_id => @user.id)
|
37
|
+
# @game.active_players.increment
|
38
|
+
# gu
|
39
|
+
# end
|
40
|
+
# if @game_user.nil?
|
41
|
+
# # game is full - error screen
|
42
|
+
# else
|
43
|
+
# # success
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
#
|
47
|
+
#
|
48
|
+
module Objects
|
49
|
+
autoload :Counters, 'redis/objects/counters'
|
50
|
+
autoload :Lists, 'redis/objects/lists'
|
51
|
+
autoload :Locks, 'redis/objects/locks'
|
52
|
+
autoload :Sets, 'redis/objects/sets'
|
53
|
+
autoload :SortedSets, 'redis/objects/sorted_sets'
|
54
|
+
autoload :Values, 'redis/objects/values'
|
55
|
+
autoload :Hashes, 'redis/objects/hashes'
|
56
|
+
|
57
|
+
class NotConnected < StandardError; end
|
58
|
+
class NilObjectId < StandardError; end
|
59
|
+
|
60
|
+
class << self
|
61
|
+
def redis=(conn)
|
62
|
+
@redis = Objects::ConnectionPoolProxy.proxy_if_needed(conn)
|
63
|
+
end
|
64
|
+
def redis
|
65
|
+
@redis || $redis || Redis.current ||
|
66
|
+
raise(NotConnected, "Redis::Objects.redis not set to a Redis.new connection")
|
67
|
+
end
|
68
|
+
|
69
|
+
def included(klass)
|
70
|
+
# Core (this file)
|
71
|
+
klass.instance_variable_set(:@redis, nil)
|
72
|
+
klass.instance_variable_set(:@redis_objects, {})
|
73
|
+
klass.send :include, InstanceMethods
|
74
|
+
klass.extend ClassMethods
|
75
|
+
|
76
|
+
# Pull in each object type
|
77
|
+
klass.send :include, Redis::Objects::Counters
|
78
|
+
klass.send :include, Redis::Objects::Lists
|
79
|
+
klass.send :include, Redis::Objects::Locks
|
80
|
+
klass.send :include, Redis::Objects::Sets
|
81
|
+
klass.send :include, Redis::Objects::SortedSets
|
82
|
+
klass.send :include, Redis::Objects::Values
|
83
|
+
klass.send :include, Redis::Objects::Hashes
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
88
|
+
module ClassMethods
|
89
|
+
# Enable per-class connections (eg, User and Post can use diff redis-server)
|
90
|
+
def redis=(conn)
|
91
|
+
@redis = Objects::ConnectionPoolProxy.proxy_if_needed(conn)
|
92
|
+
end
|
93
|
+
|
94
|
+
def redis
|
95
|
+
@redis || Objects.redis
|
96
|
+
end
|
97
|
+
|
98
|
+
# Internal list of objects
|
99
|
+
attr_writer :redis_objects
|
100
|
+
def redis_objects
|
101
|
+
@redis_objects ||= {}
|
102
|
+
end
|
103
|
+
|
104
|
+
# Set the Redis redis_prefix to use. Defaults to model_name
|
105
|
+
def redis_prefix=(redis_prefix) @redis_prefix = redis_prefix end
|
106
|
+
def redis_prefix(klass = self) #:nodoc:
|
107
|
+
@redis_prefix ||= klass.name.to_s.
|
108
|
+
sub(%r{(.*::)}, '').
|
109
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
110
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
111
|
+
downcase
|
112
|
+
end
|
113
|
+
|
114
|
+
def redis_options(name)
|
115
|
+
klass = first_ancestor_with(name)
|
116
|
+
return klass.redis_objects[name.to_sym] || {}
|
117
|
+
end
|
118
|
+
|
119
|
+
def redis_field_redis(name) #:nodoc:
|
120
|
+
klass = first_ancestor_with(name)
|
121
|
+
override_redis = klass.redis_objects[name.to_sym][:redis]
|
122
|
+
if override_redis
|
123
|
+
Objects::ConnectionPoolProxy.proxy_if_needed(override_redis)
|
124
|
+
else
|
125
|
+
self.redis
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def redis_field_key(name, id=nil, context=self) #:nodoc:
|
130
|
+
klass = first_ancestor_with(name)
|
131
|
+
# READ THIS: This can never ever ever ever change or upgrades will corrupt all data
|
132
|
+
# I don't think people were using Proc as keys before (that would create a weird key). Should be ok
|
133
|
+
if key = klass.redis_objects[name.to_sym][:key]
|
134
|
+
if key.respond_to?(:call)
|
135
|
+
key = key.call context
|
136
|
+
else
|
137
|
+
context.instance_eval "%(#{key})"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
if id.nil? and !klass.redis_objects[name.to_sym][:global]
|
141
|
+
raise NilObjectId,
|
142
|
+
"[#{klass.redis_objects[name.to_sym]}] Attempt to address redis-object " +
|
143
|
+
":#{name} on class #{klass.name} with nil id (unsaved record?) [object_id=#{object_id}]"
|
144
|
+
end
|
145
|
+
"#{redis_prefix(klass)}:#{id}:#{name}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def first_ancestor_with(name)
|
150
|
+
if redis_objects && redis_objects.key?(name.to_sym)
|
151
|
+
self
|
152
|
+
elsif superclass && superclass.respond_to?(:redis_objects)
|
153
|
+
superclass.first_ancestor_with(name)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def redis_id_field(id=nil)
|
158
|
+
@redis_id_field = id || @redis_id_field
|
159
|
+
|
160
|
+
if superclass && superclass.respond_to?(:redis_id_field)
|
161
|
+
@redis_id_field ||= superclass.redis_id_field
|
162
|
+
end
|
163
|
+
|
164
|
+
@redis_id_field ||= :id
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Instance methods that appear in your class when you include Redis::Objects.
|
169
|
+
module InstanceMethods
|
170
|
+
# Map up one level to make modular extend/include approach sane
|
171
|
+
def redis() self.class.redis end
|
172
|
+
def redis_objects() self.class.redis_objects end
|
173
|
+
|
174
|
+
def redis_delete_objects
|
175
|
+
redis.del(redis_instance_keys)
|
176
|
+
end
|
177
|
+
|
178
|
+
def redis_instance_keys
|
179
|
+
redis_objects
|
180
|
+
.reject { |_, value| value[:global] }
|
181
|
+
.keys
|
182
|
+
.collect { |name| redis_field_key(name) }
|
183
|
+
end
|
184
|
+
|
185
|
+
def redis_options(name) #:nodoc:
|
186
|
+
return self.class.redis_options(name)
|
187
|
+
end
|
188
|
+
|
189
|
+
def redis_field_redis(name) #:nodoc:
|
190
|
+
return self.class.redis_field_redis(name)
|
191
|
+
end
|
192
|
+
|
193
|
+
def redis_field_key(name) #:nodoc:
|
194
|
+
id = send(self.class.redis_id_field)
|
195
|
+
self.class.redis_field_key(name, id, self)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/redis/set.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/enumerable_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a set.
|
6
|
+
#
|
7
|
+
class Set < EnumerableObject
|
8
|
+
# Works like add. Can chain together: list << 'a' << 'b'
|
9
|
+
def <<(value)
|
10
|
+
add(value)
|
11
|
+
self # for << 'a' << 'b'
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add the specified value to the set only if it does not exist already.
|
15
|
+
# Redis: SADD
|
16
|
+
def add(value)
|
17
|
+
allow_expiration do
|
18
|
+
redis.sadd(key, marshal(value)) if value.nil? || !Array(value).empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Remove and return a random member. Redis: SPOP
|
23
|
+
def pop(count = nil)
|
24
|
+
unmarshal redis.spop(key, count)
|
25
|
+
end
|
26
|
+
|
27
|
+
# return a random member. Redis: SRANDMEMBER
|
28
|
+
def randmember(count = nil)
|
29
|
+
unmarshal redis.srandmember(key, count)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds the specified values to the set. Only works on redis > 2.4
|
33
|
+
# Redis: SADD
|
34
|
+
def merge(*values)
|
35
|
+
allow_expiration do
|
36
|
+
redis.sadd(key, values.flatten.map{|v| marshal(v)})
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return all members in the set. Redis: SMEMBERS
|
41
|
+
def members
|
42
|
+
vals = redis.smembers(key)
|
43
|
+
vals.nil? ? [] : vals.map{|v| unmarshal(v) }
|
44
|
+
end
|
45
|
+
alias_method :get, :members
|
46
|
+
alias_method :value, :members
|
47
|
+
|
48
|
+
# Returns true if the specified value is in the set. Redis: SISMEMBER
|
49
|
+
def member?(value)
|
50
|
+
redis.sismember(key, marshal(value))
|
51
|
+
end
|
52
|
+
alias_method :include?, :member?
|
53
|
+
|
54
|
+
# Delete the value from the set. Redis: SREM
|
55
|
+
def delete(value)
|
56
|
+
redis.srem(key, marshal(value))
|
57
|
+
end
|
58
|
+
|
59
|
+
# Delete if matches block
|
60
|
+
def delete_if(&block)
|
61
|
+
res = false
|
62
|
+
redis.smembers(key).each do |m|
|
63
|
+
if block.call(unmarshal(m))
|
64
|
+
res = redis.srem(key, m)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
res
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return the intersection with another set. Can pass it either another set
|
71
|
+
# object or set name. Also available as & which is a bit cleaner:
|
72
|
+
#
|
73
|
+
# members_in_both = set1 & set2
|
74
|
+
#
|
75
|
+
# If you want to specify multiple sets, you must use +intersection+:
|
76
|
+
#
|
77
|
+
# members_in_all = set1.intersection(set2, set3, set4)
|
78
|
+
# members_in_all = set1.inter(set2, set3, set4) # alias
|
79
|
+
#
|
80
|
+
# Redis: SINTER
|
81
|
+
def intersection(*sets)
|
82
|
+
redis.sinter(key, *keys_from_objects(sets)).map{|v| unmarshal(v)}
|
83
|
+
end
|
84
|
+
alias_method :intersect, :intersection
|
85
|
+
alias_method :inter, :intersection
|
86
|
+
alias_method :&, :intersection
|
87
|
+
|
88
|
+
# Calculate the intersection and store it in Redis as +name+. Returns the number
|
89
|
+
# of elements in the stored intersection. Redis: SUNIONSTORE
|
90
|
+
def interstore(name, *sets)
|
91
|
+
redis.sinterstore(name, key, *keys_from_objects(sets))
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return the union with another set. Can pass it either another set
|
95
|
+
# object or set name. Also available as | and + which are a bit cleaner:
|
96
|
+
#
|
97
|
+
# members_in_either = set1 | set2
|
98
|
+
# members_in_either = set1 + set2
|
99
|
+
#
|
100
|
+
# If you want to specify multiple sets, you must use +union+:
|
101
|
+
#
|
102
|
+
# members_in_all = set1.union(set2, set3, set4)
|
103
|
+
#
|
104
|
+
# Redis: SUNION
|
105
|
+
def union(*sets)
|
106
|
+
redis.sunion(key, *keys_from_objects(sets)).map{|v| unmarshal(v)}
|
107
|
+
end
|
108
|
+
alias_method :|, :union
|
109
|
+
alias_method :+, :union
|
110
|
+
|
111
|
+
# Calculate the union and store it in Redis as +name+. Returns the number
|
112
|
+
# of elements in the stored union. Redis: SUNIONSTORE
|
113
|
+
def unionstore(name, *sets)
|
114
|
+
redis.sunionstore(name, key, *keys_from_objects(sets))
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return the difference vs another set. Can pass it either another set
|
118
|
+
# object or set name. Also available as ^ or - which is a bit cleaner:
|
119
|
+
#
|
120
|
+
# members_difference = set1 ^ set2
|
121
|
+
# members_difference = set1 - set2
|
122
|
+
#
|
123
|
+
# If you want to specify multiple sets, you must use +difference+:
|
124
|
+
#
|
125
|
+
# members_difference = set1.difference(set2, set3, set4)
|
126
|
+
# members_difference = set1.diff(set2, set3, set4)
|
127
|
+
#
|
128
|
+
# Redis: SDIFF
|
129
|
+
def difference(*sets)
|
130
|
+
redis.sdiff(key, *keys_from_objects(sets)).map{|v| unmarshal(v)}
|
131
|
+
end
|
132
|
+
alias_method :diff, :difference
|
133
|
+
alias_method :^, :difference
|
134
|
+
alias_method :-, :difference
|
135
|
+
|
136
|
+
# Calculate the diff and store it in Redis as +name+. Returns the number
|
137
|
+
# of elements in the stored union. Redis: SDIFFSTORE
|
138
|
+
def diffstore(name, *sets)
|
139
|
+
redis.sdiffstore(name, key, *keys_from_objects(sets))
|
140
|
+
end
|
141
|
+
|
142
|
+
# Moves value from one set to another. Destination can be a String
|
143
|
+
# or Redis::Set.
|
144
|
+
#
|
145
|
+
# set.move(value, "name_of_key_in_redis")
|
146
|
+
# set.move(value, set2)
|
147
|
+
#
|
148
|
+
# Returns true if moved successfully.
|
149
|
+
#
|
150
|
+
# Redis: SMOVE
|
151
|
+
def move(value, destination)
|
152
|
+
redis.smove(key, destination.is_a?(Redis::Set) ? destination.key : destination.to_s, value)
|
153
|
+
end
|
154
|
+
|
155
|
+
# The number of members in the set. Aliased as size or count. Redis: SCARD
|
156
|
+
def length
|
157
|
+
redis.scard(key)
|
158
|
+
end
|
159
|
+
alias_method :size, :length
|
160
|
+
alias_method :count, :length
|
161
|
+
|
162
|
+
# Returns true if the set has no members. Redis: SCARD == 0
|
163
|
+
def empty?
|
164
|
+
length == 0
|
165
|
+
end
|
166
|
+
|
167
|
+
def ==(x)
|
168
|
+
members == x
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_s
|
172
|
+
members.join(', ')
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def keys_from_objects(sets)
|
178
|
+
raise ArgumentError, "Must pass in one or more set names" if sets.empty?
|
179
|
+
sets.collect{|set| set.is_a?(Redis::Set) ? set.key : set}
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/enumerable_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a sorted set.
|
6
|
+
#
|
7
|
+
class SortedSet < EnumerableObject
|
8
|
+
# How to add values using a sorted set. The key is the member, eg,
|
9
|
+
# "Peter", and the value is the score, eg, 163. So:
|
10
|
+
# num_posts['Peter'] = 163
|
11
|
+
def []=(member, score)
|
12
|
+
add(member, score)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add a member and its corresponding value to Redis. Note that the
|
16
|
+
# arguments to this are flipped; the member comes first rather than
|
17
|
+
# the score, since the member is the unique item (not the score).
|
18
|
+
def add(member, score)
|
19
|
+
allow_expiration do
|
20
|
+
redis.zadd(key, score, marshal(member))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add a list of members and their corresponding value (or a hash mapping
|
25
|
+
# values to scores) to Redis. Note that the arguments to this are flipped;
|
26
|
+
# the member comes first rather than the score, since the member is the unique
|
27
|
+
# item (not the score).
|
28
|
+
def merge(values)
|
29
|
+
allow_expiration do
|
30
|
+
vals = values.map{|v,s| [s, marshal(v)] }
|
31
|
+
redis.zadd(key, vals)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
alias_method :add_all, :merge
|
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.max)
|
42
|
+
elsif length
|
43
|
+
case length <=> 0
|
44
|
+
when 1 then range(index, index + length - 1)
|
45
|
+
when 0 then []
|
46
|
+
when -1 then nil # Ruby does this (a bit weird)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
result = score(index) || 0 # handles a nil score
|
50
|
+
end
|
51
|
+
end
|
52
|
+
alias_method :slice, :[]
|
53
|
+
|
54
|
+
# Return the score of the specified element of the sorted set at key. If the
|
55
|
+
# specified element does not exist in the sorted set, or the key does not exist
|
56
|
+
# at all, nil is returned. Redis: ZSCORE.
|
57
|
+
def score(member)
|
58
|
+
result = redis.zscore(key, marshal(member))
|
59
|
+
|
60
|
+
result.to_f unless result.nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the rank of the member in the sorted set, with scores ordered from
|
64
|
+
# low to high. +revrank+ returns the rank with scores ordered from high to low.
|
65
|
+
# When the given member does not exist in the sorted set, nil is returned.
|
66
|
+
# The returned rank (or index) of the member is 0-based for both commands
|
67
|
+
def rank(member)
|
68
|
+
if n = redis.zrank(key, marshal(member))
|
69
|
+
n.to_i
|
70
|
+
else
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def revrank(member)
|
76
|
+
if n = redis.zrevrank(key, marshal(member))
|
77
|
+
n.to_i
|
78
|
+
else
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return all members of the sorted set with their scores. Extremely CPU-intensive.
|
84
|
+
# Better to use a range instead.
|
85
|
+
def members(options={})
|
86
|
+
range(0, -1, options) || []
|
87
|
+
end
|
88
|
+
alias_method :value, :members
|
89
|
+
|
90
|
+
# Return a range of values from +start_index+ to +end_index+. Can also use
|
91
|
+
# the familiar list[start,end] Ruby syntax. Redis: ZRANGE
|
92
|
+
def range(start_index, end_index, options={})
|
93
|
+
if options[:withscores] || options[:with_scores]
|
94
|
+
redis.zrange(key, start_index, end_index, :with_scores => true).map{|v,s| [unmarshal(v), s] }
|
95
|
+
else
|
96
|
+
redis.zrange(key, start_index, end_index).map{|v| unmarshal(v) }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return a range of values from +start_index+ to +end_index+ in reverse order. Redis: ZREVRANGE
|
101
|
+
def revrange(start_index, end_index, options={})
|
102
|
+
if options[:withscores] || options[:with_scores]
|
103
|
+
redis.zrevrange(key, start_index, end_index, :with_scores => true).map{|v,s| [unmarshal(v), s] }
|
104
|
+
else
|
105
|
+
redis.zrevrange(key, start_index, end_index).map{|v| unmarshal(v) }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Return the all the elements in the sorted set at key with a score between min and max
|
110
|
+
# (including elements with score equal to min or max). Options:
|
111
|
+
# :count, :offset - passed to LIMIT
|
112
|
+
# :withscores - if true, scores are returned as well
|
113
|
+
# Redis: ZRANGEBYSCORE
|
114
|
+
def rangebyscore(min, max, options={})
|
115
|
+
args = {}
|
116
|
+
args[:limit] = [options[:offset] || 0, options[:limit] || options[:count]] if
|
117
|
+
options[:offset] || options[:limit] || options[:count]
|
118
|
+
args[:with_scores] = true if options[:withscores] || options[:with_scores]
|
119
|
+
|
120
|
+
redis.zrangebyscore(key, min, max, **args).map{|v| unmarshal(v) }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns all the elements in the sorted set at key with a score between max and min
|
124
|
+
# (including elements with score equal to max or min). In contrary to the default ordering of sorted sets,
|
125
|
+
# for this command the elements are considered to be ordered from high to low scores.
|
126
|
+
# Options:
|
127
|
+
# :count, :offset - passed to LIMIT
|
128
|
+
# :withscores - if true, scores are returned as well
|
129
|
+
# Redis: ZREVRANGEBYSCORE
|
130
|
+
def revrangebyscore(max, min, options={})
|
131
|
+
args = {}
|
132
|
+
args[:limit] = [options[:offset] || 0, options[:limit] || options[:count]] if
|
133
|
+
options[:offset] || options[:limit] || options[:count]
|
134
|
+
args[:with_scores] = true if options[:withscores] || options[:with_scores]
|
135
|
+
|
136
|
+
redis.zrevrangebyscore(key, max, min, **args).map{|v| unmarshal(v) }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Remove all elements in the sorted set at key with rank between start and end. Start and end are
|
140
|
+
# 0-based with rank 0 being the element with the lowest score. Both start and end can be negative
|
141
|
+
# numbers, where they indicate offsets starting at the element with the highest rank. For example:
|
142
|
+
# -1 is the element with the highest score, -2 the element with the second highest score and so forth.
|
143
|
+
# Redis: ZREMRANGEBYRANK
|
144
|
+
def remrangebyrank(min, max)
|
145
|
+
redis.zremrangebyrank(key, min, max)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Remove all the elements in the sorted set at key with a score between min and max (including
|
149
|
+
# elements with score equal to min or max). Redis: ZREMRANGEBYSCORE
|
150
|
+
def remrangebyscore(min, max)
|
151
|
+
redis.zremrangebyscore(key, min, max)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Delete the value from the set. Redis: ZREM
|
155
|
+
def delete(value)
|
156
|
+
allow_expiration do
|
157
|
+
redis.zrem(key, marshal(value))
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Delete element if it matches block
|
162
|
+
def delete_if(&block)
|
163
|
+
raise ArgumentError, "Missing block to SortedSet#delete_if" unless block_given?
|
164
|
+
res = false
|
165
|
+
redis.zrange(key, 0, -1).each do |m|
|
166
|
+
if block.call(unmarshal(m))
|
167
|
+
res = redis.zrem(key, m)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
res
|
171
|
+
end
|
172
|
+
|
173
|
+
# Increment the rank of that member atomically and return the new value. This
|
174
|
+
# method is aliased as incr() for brevity. Redis: ZINCRBY
|
175
|
+
def increment(member, by=1)
|
176
|
+
allow_expiration do
|
177
|
+
zincrby(member, by)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
alias_method :incr, :increment
|
181
|
+
alias_method :incrby, :increment
|
182
|
+
|
183
|
+
# Convenience to calling increment() with a negative number.
|
184
|
+
def decrement(member, by=1)
|
185
|
+
allow_expiration do
|
186
|
+
zincrby(member, -by)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
alias_method :decr, :decrement
|
190
|
+
alias_method :decrby, :decrement
|
191
|
+
|
192
|
+
# Return the intersection with another set. Can pass it either another set
|
193
|
+
# object or set name. Also available as & which is a bit cleaner:
|
194
|
+
#
|
195
|
+
# members_in_both = set1 & set2
|
196
|
+
#
|
197
|
+
# If you want to specify multiple sets, you must use +intersection+:
|
198
|
+
#
|
199
|
+
# members_in_all = set1.intersection(set2, set3, set4)
|
200
|
+
# members_in_all = set1.inter(set2, set3, set4) # alias
|
201
|
+
#
|
202
|
+
# Redis: SINTER
|
203
|
+
def intersection(*sets)
|
204
|
+
result = nil
|
205
|
+
temp_key = :"#{key}:intersection:#{Time.current.to_i + rand}"
|
206
|
+
|
207
|
+
redis.multi do
|
208
|
+
interstore(temp_key, *sets)
|
209
|
+
redis.expire(temp_key, 1)
|
210
|
+
|
211
|
+
result = redis.zrange(temp_key, 0, -1)
|
212
|
+
end
|
213
|
+
|
214
|
+
result.value
|
215
|
+
end
|
216
|
+
alias_method :intersect, :intersection
|
217
|
+
alias_method :inter, :intersection
|
218
|
+
alias_method :&, :intersection
|
219
|
+
|
220
|
+
# Calculate the intersection and store it in Redis as +name+. Returns the number
|
221
|
+
# of elements in the stored intersection. Redis: SUNIONSTORE
|
222
|
+
def interstore(name, *sets)
|
223
|
+
allow_expiration do
|
224
|
+
opts = sets.last.is_a?(Hash) ? sets.pop : {}
|
225
|
+
redis.zinterstore(key_from_object(name), keys_from_objects([self] + sets), **opts)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Return the union with another set. Can pass it either another set
|
230
|
+
# object or set name. Also available as | and + which are a bit cleaner:
|
231
|
+
#
|
232
|
+
# members_in_either = set1 | set2
|
233
|
+
# members_in_either = set1 + set2
|
234
|
+
#
|
235
|
+
# If you want to specify multiple sets, you must use +union+:
|
236
|
+
#
|
237
|
+
# members_in_all = set1.union(set2, set3, set4)
|
238
|
+
#
|
239
|
+
# Redis: SUNION
|
240
|
+
def union(*sets)
|
241
|
+
result = nil
|
242
|
+
temp_key = :"#{key}:union:#{Time.current.to_i + rand}"
|
243
|
+
|
244
|
+
redis.multi do
|
245
|
+
unionstore(temp_key, *sets)
|
246
|
+
redis.expire(temp_key, 1)
|
247
|
+
|
248
|
+
result = redis.zrange(temp_key, 0, -1)
|
249
|
+
end
|
250
|
+
|
251
|
+
result.value
|
252
|
+
end
|
253
|
+
alias_method :|, :union
|
254
|
+
alias_method :+, :union
|
255
|
+
|
256
|
+
# Calculate the union and store it in Redis as +name+. Returns the number
|
257
|
+
# of elements in the stored union. Redis: SUNIONSTORE
|
258
|
+
def unionstore(name, *sets)
|
259
|
+
allow_expiration do
|
260
|
+
opts = sets.last.is_a?(Hash) ? sets.pop : {}
|
261
|
+
redis.zunionstore(key_from_object(name), keys_from_objects([self] + sets), **opts)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Returns true if the set has no members. Redis: SCARD == 0
|
266
|
+
def empty?
|
267
|
+
length == 0
|
268
|
+
end
|
269
|
+
|
270
|
+
def ==(x)
|
271
|
+
members == x
|
272
|
+
end
|
273
|
+
|
274
|
+
def to_s
|
275
|
+
members.join(', ')
|
276
|
+
end
|
277
|
+
|
278
|
+
# Return the value at the given index. Can also use familiar list[index] syntax.
|
279
|
+
# Redis: ZRANGE
|
280
|
+
def at(index)
|
281
|
+
range(index, index).first
|
282
|
+
end
|
283
|
+
|
284
|
+
# Return the first element in the list. Redis: ZRANGE(0)
|
285
|
+
def first
|
286
|
+
at(0)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Return the last element in the list. Redis: ZRANGE(-1)
|
290
|
+
def last
|
291
|
+
at(-1)
|
292
|
+
end
|
293
|
+
|
294
|
+
# The number of members in the set. Aliased as size or count. Redis: ZCARD
|
295
|
+
def length
|
296
|
+
redis.zcard(key)
|
297
|
+
end
|
298
|
+
alias_method :size, :length
|
299
|
+
alias_method :count, :length
|
300
|
+
|
301
|
+
# The number of members within a range of scores. Redis: ZCOUNT
|
302
|
+
def range_size(min, max)
|
303
|
+
redis.zcount(key, min, max)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Return a boolean indicating whether +value+ is a member.
|
307
|
+
def member?(value)
|
308
|
+
!redis.zscore(key, marshal(value)).nil?
|
309
|
+
end
|
310
|
+
|
311
|
+
private
|
312
|
+
def key_from_object(set)
|
313
|
+
set.is_a?(Redis::SortedSet) ? set.key : set
|
314
|
+
end
|
315
|
+
|
316
|
+
def keys_from_objects(sets)
|
317
|
+
raise ArgumentError, "Must pass in one or more set names" if sets.empty?
|
318
|
+
sets.collect{|set| set.is_a?(Redis::SortedSet) || set.is_a?(Redis::Set) ? set.key : set}
|
319
|
+
end
|
320
|
+
|
321
|
+
def zincrby(member, by)
|
322
|
+
redis.zincrby(key, by, marshal(member)).to_i
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|