redcord 0.0.3 → 0.1.3
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 +4 -4
- data/lib/redcord.rb +1 -0
- data/lib/redcord/actions.rb +78 -6
- data/lib/redcord/attribute.rb +110 -13
- data/lib/redcord/base.rb +13 -3
- data/lib/redcord/connection_pool.rb +28 -0
- data/lib/redcord/migration.rb +2 -0
- data/lib/redcord/migration/index.rb +57 -0
- data/lib/redcord/migration/ttl.rb +9 -4
- data/lib/redcord/railtie.rb +0 -1
- data/lib/redcord/redis.rb +200 -0
- data/lib/redcord/redis_connection.rb +29 -23
- data/lib/redcord/relation.rb +112 -14
- data/lib/redcord/serializer.rb +84 -33
- data/lib/redcord/server_scripts/create_hash.erb.lua +81 -0
- data/lib/redcord/server_scripts/delete_hash.erb.lua +17 -8
- data/lib/redcord/server_scripts/find_by_attr.erb.lua +51 -16
- data/lib/redcord/server_scripts/find_by_attr_count.erb.lua +45 -14
- data/lib/redcord/server_scripts/shared/index_helper_methods.erb.lua +45 -16
- data/lib/redcord/server_scripts/shared/lua_helper_methods.erb.lua +20 -4
- data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +81 -14
- data/lib/redcord/server_scripts/update_hash.erb.lua +40 -26
- data/lib/redcord/tasks/redis.rake +15 -0
- data/lib/redcord/vacuum_helper.rb +90 -0
- metadata +21 -5
- data/lib/redcord/prepared_redis.rb +0 -147
- data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f33907b35b938f14be13d1d20f89f1530d72a28cb55e7e38a53f61743a2bfe5
|
4
|
+
data.tar.gz: 1297ac81f293f35572a0015782aa621b1d5ba0e3e691ea86abadbdde26ca094a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4d23f458c6f6de224293010b60f19d7c07199c66eb382e5bd5a7917a429900658bfd42202107c3e72ace588ea526bcf3edb6b56dfeaa7a6b58f3eea4b91dc81
|
7
|
+
data.tar.gz: d1aa8dedf1dccad372489072405725fe51e3712b634524c73c574130ab8473bab1fefdedfbd47466c956ab7e96e121153798f08a4250b30e7dc30d33039bf801
|
data/lib/redcord.rb
CHANGED
data/lib/redcord/actions.rb
CHANGED
@@ -9,6 +9,7 @@ require 'redcord/relation'
|
|
9
9
|
module Redcord
|
10
10
|
# Raised by Model.find
|
11
11
|
class RecordNotFound < StandardError; end
|
12
|
+
class InvalidAction < StandardError; end
|
12
13
|
end
|
13
14
|
|
14
15
|
module Redcord::Actions
|
@@ -24,15 +25,36 @@ module Redcord::Actions
|
|
24
25
|
module ClassMethods
|
25
26
|
extend T::Sig
|
26
27
|
|
28
|
+
sig { returns(Integer) }
|
29
|
+
def count
|
30
|
+
Redcord::Base.trace(
|
31
|
+
'redcord_actions_class_methods_count',
|
32
|
+
model_name: name,
|
33
|
+
) do
|
34
|
+
res = 0
|
35
|
+
redis.scan_each_shard("#{model_key}:id:*") { res += 1 }
|
36
|
+
res
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
27
40
|
sig { params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
28
41
|
def create!(args)
|
29
42
|
Redcord::Base.trace(
|
30
43
|
'redcord_actions_class_methods_create!',
|
31
44
|
model_name: name,
|
32
45
|
) do
|
46
|
+
self.props.keys.each { |attr_key| args[attr_key] = nil unless args.key?(attr_key) }
|
33
47
|
args[:created_at] = args[:updated_at] = Time.zone.now
|
34
48
|
instance = TypeCoerce[self].new.from(args)
|
35
|
-
id = redis.create_hash_returning_id(
|
49
|
+
id = redis.create_hash_returning_id(
|
50
|
+
model_key,
|
51
|
+
to_redis_hash(args),
|
52
|
+
ttl: _script_arg_ttl,
|
53
|
+
index_attrs: _script_arg_index_attrs,
|
54
|
+
range_index_attrs: _script_arg_range_index_attrs,
|
55
|
+
custom_index_attrs: _script_arg_custom_index_attrs,
|
56
|
+
hash_tag: instance.hash_tag,
|
57
|
+
)
|
36
58
|
instance.send(:id=, id)
|
37
59
|
instance
|
38
60
|
end
|
@@ -75,7 +97,13 @@ module Redcord::Actions
|
|
75
97
|
'redcord_actions_class_methods_destroy',
|
76
98
|
model_name: name,
|
77
99
|
) do
|
78
|
-
redis.delete_hash(
|
100
|
+
redis.delete_hash(
|
101
|
+
model_key,
|
102
|
+
id,
|
103
|
+
index_attrs: _script_arg_index_attrs,
|
104
|
+
range_index_attrs: _script_arg_range_index_attrs,
|
105
|
+
custom_index_attrs: _script_arg_custom_index_attrs,
|
106
|
+
) == 1
|
79
107
|
end
|
80
108
|
end
|
81
109
|
end
|
@@ -115,10 +143,19 @@ module Redcord::Actions
|
|
115
143
|
self.updated_at = Time.zone.now
|
116
144
|
_id = id
|
117
145
|
if _id.nil?
|
146
|
+
serialized_instance = serialize
|
147
|
+
self.class.props.keys.each do |attr_key|
|
148
|
+
serialized_instance[attr_key.to_s] = nil unless serialized_instance.key?(attr_key.to_s)
|
149
|
+
end
|
118
150
|
self.created_at = T.must(self.updated_at)
|
119
151
|
_id = redis.create_hash_returning_id(
|
120
152
|
self.class.model_key,
|
121
|
-
self.class.to_redis_hash(
|
153
|
+
self.class.to_redis_hash(serialized_instance),
|
154
|
+
ttl: self.class._script_arg_ttl,
|
155
|
+
index_attrs: self.class._script_arg_index_attrs,
|
156
|
+
range_index_attrs: self.class._script_arg_range_index_attrs,
|
157
|
+
custom_index_attrs: self.class._script_arg_custom_index_attrs,
|
158
|
+
hash_tag: hash_tag,
|
122
159
|
)
|
123
160
|
send(:id=, _id)
|
124
161
|
else
|
@@ -126,17 +163,37 @@ module Redcord::Actions
|
|
126
163
|
self.class.model_key,
|
127
164
|
_id,
|
128
165
|
self.class.to_redis_hash(serialize),
|
166
|
+
ttl: self.class._script_arg_ttl,
|
167
|
+
index_attrs: self.class._script_arg_index_attrs,
|
168
|
+
range_index_attrs: self.class._script_arg_range_index_attrs,
|
169
|
+
custom_index_attrs: self.class._script_arg_custom_index_attrs,
|
170
|
+
hash_tag: hash_tag,
|
129
171
|
)
|
130
172
|
end
|
131
173
|
end
|
132
174
|
end
|
133
175
|
|
176
|
+
sig { returns(T::Boolean) }
|
177
|
+
def save
|
178
|
+
save!
|
179
|
+
|
180
|
+
true
|
181
|
+
rescue Redis::CommandError
|
182
|
+
# TODO: break down Redis::CommandError by parsing the error message
|
183
|
+
false
|
184
|
+
end
|
185
|
+
|
134
186
|
sig { params(args: T::Hash[Symbol, T.untyped]).void }
|
135
|
-
def update!(args
|
187
|
+
def update!(args)
|
136
188
|
Redcord::Base.trace(
|
137
189
|
'redcord_actions_instance_methods_update!',
|
138
190
|
model_name: self.class.name,
|
139
191
|
) do
|
192
|
+
shard_by_attr = self.class.shard_by_attribute
|
193
|
+
if args.keys.include?(shard_by_attr)
|
194
|
+
raise Redcord::InvalidAction, "Cannot update shard_by attribute #{shard_by_attr}"
|
195
|
+
end
|
196
|
+
|
140
197
|
_id = id
|
141
198
|
if _id.nil?
|
142
199
|
_set_args!(args)
|
@@ -148,11 +205,26 @@ module Redcord::Actions
|
|
148
205
|
self.class.model_key,
|
149
206
|
_id,
|
150
207
|
self.class.to_redis_hash(args),
|
208
|
+
ttl: self.class._script_arg_ttl,
|
209
|
+
index_attrs: self.class._script_arg_index_attrs,
|
210
|
+
range_index_attrs: self.class._script_arg_range_index_attrs,
|
211
|
+
custom_index_attrs: self.class._script_arg_custom_index_attrs,
|
212
|
+
hash_tag: hash_tag,
|
151
213
|
)
|
152
214
|
end
|
153
215
|
end
|
154
216
|
end
|
155
217
|
|
218
|
+
sig { params(args: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
|
219
|
+
def update(args)
|
220
|
+
update!(args)
|
221
|
+
|
222
|
+
true
|
223
|
+
rescue Redis::CommandError
|
224
|
+
# TODO: break down Redis::CommandError by parsing the error message
|
225
|
+
false
|
226
|
+
end
|
227
|
+
|
156
228
|
sig { returns(T::Boolean) }
|
157
229
|
def destroy
|
158
230
|
Redcord::Base.trace(
|
@@ -177,14 +249,14 @@ module Redcord::Actions
|
|
177
249
|
end
|
178
250
|
end
|
179
251
|
|
180
|
-
sig { returns(T.nilable(
|
252
|
+
sig { returns(T.nilable(String)) }
|
181
253
|
def id
|
182
254
|
instance_variable_get(:@_id)
|
183
255
|
end
|
184
256
|
|
185
257
|
private
|
186
258
|
|
187
|
-
sig { params(id:
|
259
|
+
sig { params(id: String).returns(String) }
|
188
260
|
def id=(id)
|
189
261
|
instance_variable_set(:@_id, id)
|
190
262
|
end
|
data/lib/redcord/attribute.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# typed: strict
|
4
|
+
module Redcord
|
5
|
+
class InvalidAttribute < StandardError; end
|
6
|
+
end
|
4
7
|
|
5
8
|
module Redcord::Attribute
|
6
9
|
extend T::Sig
|
@@ -10,18 +13,33 @@ module Redcord::Attribute
|
|
10
13
|
# type.
|
11
14
|
RangeIndexType = T.type_alias {
|
12
15
|
T.any(
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
Float,
|
17
|
+
Integer,
|
18
|
+
NilClass,
|
19
|
+
Numeric,
|
20
|
+
Time,
|
16
21
|
)
|
17
22
|
}
|
18
23
|
|
24
|
+
# Implicitly determine what data type can be a used in custom index on Redis based on Ruby type.
|
25
|
+
# Custom index currently supports positive integers with up to 19 characters in decimal notation,
|
26
|
+
# will raise error in Lua if bigger numbers are used.
|
27
|
+
CustomIndexType = T.type_alias {
|
28
|
+
T.any(
|
29
|
+
Integer,
|
30
|
+
Time,
|
31
|
+
)
|
32
|
+
}
|
33
|
+
|
19
34
|
sig { params(klass: T.class_of(T::Struct)).void }
|
20
35
|
def self.included(klass)
|
21
36
|
klass.extend(ClassMethods)
|
37
|
+
klass.include(InstanceMethods)
|
22
38
|
klass.class_variable_set(:@@index_attributes, Set.new)
|
23
39
|
klass.class_variable_set(:@@range_index_attributes, Set.new)
|
40
|
+
klass.class_variable_set(:@@custom_index_attributes, Hash.new { |h, k| h[k] = [] })
|
24
41
|
klass.class_variable_set(:@@ttl, nil)
|
42
|
+
klass.class_variable_set(:@@shard_by_attribute, nil)
|
25
43
|
end
|
26
44
|
|
27
45
|
module ClassMethods
|
@@ -46,30 +64,81 @@ module Redcord::Attribute
|
|
46
64
|
def index_attribute(attr, type)
|
47
65
|
if should_range_index?(type)
|
48
66
|
class_variable_get(:@@range_index_attributes) << attr
|
49
|
-
sadd_proc_on_redis_connection('range_index_attrs', attr.to_s)
|
50
67
|
else
|
51
68
|
class_variable_get(:@@index_attributes) << attr
|
52
|
-
sadd_proc_on_redis_connection('index_attrs', attr.to_s)
|
53
69
|
end
|
54
70
|
end
|
71
|
+
|
72
|
+
sig { params(index_name: Symbol, attrs: T::Array[Symbol]).void }
|
73
|
+
def custom_index(index_name, attrs)
|
74
|
+
attrs.each do |attr|
|
75
|
+
type = props[attr][:type]
|
76
|
+
if !can_custom_index?(type)
|
77
|
+
raise(Redcord::WrongAttributeType, "Custom index doesn't support '#{type}' attributes.")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
shard_by_attr = class_variable_get(:@@shard_by_attribute)
|
81
|
+
if shard_by_attr and shard_by_attr != attrs.first
|
82
|
+
raise(
|
83
|
+
Redcord::CustomIndexInvalidDesign,
|
84
|
+
"shard_by attribute '#{shard_by_attr}' must be placed first in '#{index_name}' index"
|
85
|
+
)
|
86
|
+
end
|
87
|
+
class_variable_get(:@@custom_index_attributes)[index_name] = attrs
|
88
|
+
end
|
55
89
|
|
56
90
|
sig { params(duration: T.nilable(ActiveSupport::Duration)).void }
|
57
91
|
def ttl(duration)
|
58
92
|
class_variable_set(:@@ttl, duration)
|
59
93
|
end
|
60
94
|
|
61
|
-
|
95
|
+
def shard_by_attribute(attr=nil)
|
96
|
+
return class_variable_get(:@@shard_by_attribute) if attr.nil?
|
62
97
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
# migrations
|
68
|
-
Redcord::RedisConnection.procs_to_prepare << proc do |redis|
|
69
|
-
redis.sadd("#{model_key}:#{redis_key}", item_to_add)
|
98
|
+
# attr must be an non-index attribute (index: false)
|
99
|
+
if class_variable_get(:@@index_attributes).include?(attr) ||
|
100
|
+
class_variable_get(:@@range_index_attributes).include?(attr)
|
101
|
+
raise Redcord::InvalidAttribute, "Cannot shard by an index attribute '#{attr}'"
|
70
102
|
end
|
103
|
+
|
104
|
+
class_variable_get(:@@custom_index_attributes).each do |index_name, attrs|
|
105
|
+
if attr != attrs.first
|
106
|
+
raise(
|
107
|
+
Redcord::CustomIndexInvalidDesign,
|
108
|
+
"shard_by attribute '#{attr}' must be placed first in '#{index_name}' index"
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Delete the shard_by_attribute since it would be a constant in the
|
113
|
+
# custom index set
|
114
|
+
attrs.shift
|
115
|
+
end
|
116
|
+
|
117
|
+
class_variable_set(:@@shard_by_attribute, attr)
|
71
118
|
end
|
72
119
|
|
120
|
+
sig { returns(Integer) }
|
121
|
+
def _script_arg_ttl
|
122
|
+
class_variable_get(:@@ttl)&.to_i || -1
|
123
|
+
end
|
124
|
+
|
125
|
+
sig { returns(T::Array[Symbol]) }
|
126
|
+
def _script_arg_index_attrs
|
127
|
+
class_variable_get(:@@index_attributes).to_a
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { returns(T::Array[Symbol]) }
|
131
|
+
def _script_arg_range_index_attrs
|
132
|
+
class_variable_get(:@@range_index_attributes).to_a
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { returns(T::Hash[Symbol, T::Array]) }
|
136
|
+
def _script_arg_custom_index_attrs
|
137
|
+
class_variable_get(:@@custom_index_attributes)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
73
142
|
sig { params(type: T.any(Class, T::Types::Base)).returns(T::Boolean) }
|
74
143
|
def should_range_index?(type)
|
75
144
|
# Change Ruby raw type to Sorbet type in order to call subtype_of?
|
@@ -77,6 +146,34 @@ module Redcord::Attribute
|
|
77
146
|
|
78
147
|
type.subtype_of?(RangeIndexType)
|
79
148
|
end
|
149
|
+
|
150
|
+
sig { params(type: T.any(Class, T::Types::Base)).returns(T::Boolean) }
|
151
|
+
def can_custom_index?(type)
|
152
|
+
# Change Ruby raw type to Sorbet type in order to call subtype_of?
|
153
|
+
type = T::Types::Simple.new(type) if type.is_a?(Class)
|
154
|
+
type.subtype_of?(CustomIndexType)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
module InstanceMethods
|
159
|
+
extend T::Sig
|
160
|
+
|
161
|
+
sig { returns(T.nilable(String)) }
|
162
|
+
def hash_tag
|
163
|
+
attr = self.class.class_variable_get(:@@shard_by_attribute)
|
164
|
+
|
165
|
+
return nil if attr.nil?
|
166
|
+
|
167
|
+
# A blank hash tag would cause MOVED error in cluster mode
|
168
|
+
tag = send(attr)
|
169
|
+
default_tag = '__redcord_hash_tag_null__'
|
170
|
+
|
171
|
+
if tag == default_tag
|
172
|
+
raise Redcord::InvalidAttribute, "#{attr}=#{default_tag} conflicts with default hash_tag value"
|
173
|
+
end
|
174
|
+
|
175
|
+
"{#{tag || default_tag}}"
|
176
|
+
end
|
80
177
|
end
|
81
178
|
|
82
179
|
mixes_in_class_methods(ClassMethods)
|
data/lib/redcord/base.rb
CHANGED
@@ -56,9 +56,19 @@ module Redcord::Base
|
|
56
56
|
# coerced to the specified attribute types. Like ActiveRecord,
|
57
57
|
# Redcord manages the created_at and updated_at fields behind the
|
58
58
|
# scene.
|
59
|
-
|
60
|
-
|
61
|
-
attribute :updated_at, T.nilable(Time), index: true
|
59
|
+
prop :created_at, T.nilable(Time)
|
60
|
+
prop :updated_at, T.nilable(Time)
|
62
61
|
end
|
63
62
|
end
|
63
|
+
|
64
|
+
sig { returns(T::Array[T.class_of(Redcord::Base)]) }
|
65
|
+
def self.descendants
|
66
|
+
descendants = []
|
67
|
+
# TODO: Use T::Struct instead of Class
|
68
|
+
ObjectSpace.each_object(Class) do |klass|
|
69
|
+
descendants << klass if klass < self
|
70
|
+
end
|
71
|
+
descendants
|
72
|
+
end
|
73
|
+
|
64
74
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'connection_pool'
|
3
|
+
require_relative 'redis'
|
4
|
+
|
5
|
+
class Redcord::ConnectionPool
|
6
|
+
def initialize(pool_size:, timeout:, **client_options)
|
7
|
+
@connection_pool = ::ConnectionPool.new(size: pool_size, timeout: timeout) do
|
8
|
+
# Construct a new client every time the block gets called
|
9
|
+
Redcord::Redis.new(**client_options, logger: Redcord::Logger.proxy)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Avoid method_missing when possible for better performance
|
14
|
+
methods = Set.new(Redcord::Redis.instance_methods(false) + Redis.instance_methods(false))
|
15
|
+
methods.each do |method_name|
|
16
|
+
define_method method_name do |*args, &blk|
|
17
|
+
@connection_pool.with do |redis|
|
18
|
+
redis.send(method_name, *args, &blk)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(method_name, *args, &blk)
|
24
|
+
@connection_pool.with do |redis|
|
25
|
+
redis.send(method_name, *args, &blk)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/redcord/migration.rb
CHANGED
@@ -2,11 +2,13 @@
|
|
2
2
|
class Redcord::Migration
|
3
3
|
end
|
4
4
|
|
5
|
+
require 'redcord/migration/index'
|
5
6
|
require 'redcord/migration/ttl'
|
6
7
|
|
7
8
|
class Redcord::Migration
|
8
9
|
extend T::Sig
|
9
10
|
extend T::Helpers
|
11
|
+
include Redcord::Migration::Index
|
10
12
|
include Redcord::Migration::TTL
|
11
13
|
|
12
14
|
abstract!
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module Redcord::Migration::Index
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { params(model: T.class_of(Redcord::Base), index_name: Symbol).void }
|
9
|
+
def remove_index(model, index_name)
|
10
|
+
model.redis.scan_each_shard("#{model.model_key}:#{index_name}:*") { |key| _del_set(model, key) }
|
11
|
+
|
12
|
+
attr_set = "#{model.model_key}:#{index_name}"
|
13
|
+
nil_attr_set = "#{attr_set}:"
|
14
|
+
|
15
|
+
model.redis.scan_each_shard("#{nil_attr_set}*") { |key| _del_set(model, key) }
|
16
|
+
model.redis.scan_each_shard("#{attr_set}*") { |key| _del_zset(model, key) }
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { params(model: T.class_of(Redcord::Base), index_name: Symbol).void }
|
20
|
+
def remove_custom_index(model, index_name)
|
21
|
+
index_key = "#{model.model_key}:custom_index:#{index_name}"
|
22
|
+
index_content_key = "#{model.model_key}:custom_index:#{index_name}_content"
|
23
|
+
model.redis.scan_each_shard("#{index_key}*") { |key| model.redis.unlink(key) }
|
24
|
+
model.redis.scan_each_shard("#{index_content_key}*") { |key| model.redis.unlink(key) }
|
25
|
+
end
|
26
|
+
|
27
|
+
sig {
|
28
|
+
params(
|
29
|
+
model: T.class_of(Redcord::Base),
|
30
|
+
attr_set_name: String,
|
31
|
+
index_name: Symbol,
|
32
|
+
).void
|
33
|
+
}
|
34
|
+
def _remove_index_from_attr_set(model:, attr_set_name:, index_name:)
|
35
|
+
model.redis.srem("#{model.model_key}:#{attr_set_name}", index_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { params(model: T.class_of(Redcord::Base), key: String).void }
|
39
|
+
def _del_set(model, key)
|
40
|
+
# Use SPOP here to minimize blocking
|
41
|
+
loop do
|
42
|
+
break unless model.redis.spop(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
model.redis.del(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(model: T.class_of(Redcord::Base), key: String).void }
|
49
|
+
def _del_zset(model, key)
|
50
|
+
# ZPOPMIN might not be avaliable on old redis servers
|
51
|
+
model.redis.zscan_each(match: key) do |id, _|
|
52
|
+
model.redis.zrem(key, id)
|
53
|
+
end
|
54
|
+
|
55
|
+
model.redis.del(key)
|
56
|
+
end
|
57
|
+
end
|