redis-objects-legacy 1.6.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.
- 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
|