redcord 0.0.4 → 0.1.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 +4 -4
- data/lib/redcord/actions.rb +78 -6
- data/lib/redcord/attribute.rb +110 -13
- data/lib/redcord/migration/index.rb +12 -23
- data/lib/redcord/migration/ttl.rb +1 -8
- data/lib/redcord/railtie.rb +0 -1
- data/lib/redcord/redis.rb +225 -0
- data/lib/redcord/redis_connection.rb +9 -14
- data/lib/redcord/relation.rb +107 -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 +50 -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/vacuum_helper.rb +34 -4
- metadata +4 -5
- data/lib/redcord/prepared_redis.rb +0 -147
- data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +0 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16cb660da0ea843f300ff18cc090ccfed7883c9d1ca48ef0bc5ed8d4e805854c
|
4
|
+
data.tar.gz: f53002cb43b3e5286ba1f914c41ffd6b8a9722d2d33d9764deb7bcba88d2659c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db443ffbf21066c549b30daed2ad44f836aa4469065b7bd8b6d0c13f45597bd7bdbf6c7ef4202ddf8103a1c2fbcfca28ca60f9f59b90584d0ec718d380d0f908
|
7
|
+
data.tar.gz: 40ded5853307f453fe7c5ab2962e29cc081c05ab7f0c2cfddb898259e207ae9c343dfee6a67967449ff43c227c88db6f7a7922c425d8e6df92466b5ed778c5c2
|
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)
|
@@ -7,32 +7,21 @@ module Redcord::Migration::Index
|
|
7
7
|
|
8
8
|
sig { params(model: T.class_of(Redcord::Base), index_name: Symbol).void }
|
9
9
|
def remove_index(model, index_name)
|
10
|
-
|
11
|
-
_remove_index_from_attr_set(
|
12
|
-
model: model,
|
13
|
-
attr_set_name: 'index_attrs',
|
14
|
-
index_name: index_name,
|
15
|
-
)
|
10
|
+
model.redis.scan_each_shard("#{model.model_key}:#{index_name}:*") { |key| _del_set(model, key) }
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
_remove_index_from_attr_set(
|
20
|
-
model: model,
|
21
|
-
attr_set_name: 'range_index_attrs',
|
22
|
-
index_name: index_name,
|
23
|
-
)
|
12
|
+
attr_set = "#{model.model_key}:#{index_name}"
|
13
|
+
nil_attr_set = "#{attr_set}:"
|
24
14
|
|
25
|
-
|
26
|
-
|
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
|
27
18
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
)
|
35
|
-
end
|
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) }
|
36
25
|
end
|
37
26
|
|
38
27
|
sig {
|
@@ -7,16 +7,9 @@ module Redcord::Migration::TTL
|
|
7
7
|
model.class_variable_get(:@@ttl) || -1
|
8
8
|
end
|
9
9
|
|
10
|
-
# This won't change ttl until we call update on a record
|
11
|
-
sig { params(model: T.class_of(Redcord::Base)).void }
|
12
|
-
def change_ttl_passive(model)
|
13
|
-
model.redis.set("#{model.model_key}:ttl", _get_ttl(model))
|
14
|
-
end
|
15
|
-
|
16
10
|
sig { params(model: T.class_of(Redcord::Base)).void }
|
17
11
|
def change_ttl_active(model)
|
18
|
-
|
19
|
-
model.redis.scan_each(match: "#{model.model_key}:id:*") do |key|
|
12
|
+
model.redis.scan_each_shard("#{model.model_key}:id:*") do |key|
|
20
13
|
model.redis.expire(key, _get_ttl(model))
|
21
14
|
end
|
22
15
|
end
|
data/lib/redcord/railtie.rb
CHANGED
@@ -0,0 +1,225 @@
|
|
1
|
+
# typed: true
|
2
|
+
require 'digest'
|
3
|
+
require 'redis'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
class Redcord::Redis < Redis
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig do
|
10
|
+
params(
|
11
|
+
key: T.any(String, Symbol),
|
12
|
+
args: T::Hash[T.untyped, T.untyped],
|
13
|
+
ttl: T.nilable(Integer),
|
14
|
+
index_attrs: T::Array[Symbol],
|
15
|
+
range_index_attrs: T::Array[Symbol],
|
16
|
+
custom_index_attrs: T::Hash[Symbol, T::Array],
|
17
|
+
hash_tag: T.nilable(String),
|
18
|
+
).returns(String)
|
19
|
+
end
|
20
|
+
def create_hash_returning_id(key, args, ttl:, index_attrs:, range_index_attrs:, custom_index_attrs:, hash_tag: nil)
|
21
|
+
Redcord::Base.trace(
|
22
|
+
'redcord_redis_create_hash_returning_id',
|
23
|
+
model_name: key,
|
24
|
+
) do
|
25
|
+
id = "#{SecureRandom.uuid}#{hash_tag}"
|
26
|
+
custom_index_attrs_flat = custom_index_attrs.inject([]) do |result, (index_name, attrs)|
|
27
|
+
result << index_name
|
28
|
+
result << attrs.size
|
29
|
+
result + attrs
|
30
|
+
end
|
31
|
+
run_script(
|
32
|
+
:create_hash,
|
33
|
+
keys: [id, hash_tag],
|
34
|
+
argv: [key, ttl, index_attrs.size, range_index_attrs.size, custom_index_attrs_flat.size] +
|
35
|
+
index_attrs + range_index_attrs + custom_index_attrs_flat + args.to_a.flatten,
|
36
|
+
)
|
37
|
+
id
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
sig do
|
42
|
+
params(
|
43
|
+
model: String,
|
44
|
+
id: String,
|
45
|
+
args: T::Hash[T.untyped, T.untyped],
|
46
|
+
ttl: T.nilable(Integer),
|
47
|
+
index_attrs: T::Array[Symbol],
|
48
|
+
range_index_attrs: T::Array[Symbol],
|
49
|
+
custom_index_attrs: T::Hash[Symbol, T::Array],
|
50
|
+
hash_tag: T.nilable(String),
|
51
|
+
).void
|
52
|
+
end
|
53
|
+
def update_hash(model, id, args, ttl:, index_attrs:, range_index_attrs:, custom_index_attrs:, hash_tag:)
|
54
|
+
Redcord::Base.trace(
|
55
|
+
'redcord_redis_update_hash',
|
56
|
+
model_name: model,
|
57
|
+
) do
|
58
|
+
custom_index_attrs_flat = custom_index_attrs.inject([]) do |result, (index_name, attrs)|
|
59
|
+
if !(args.keys.to_set & attrs.to_set).empty?
|
60
|
+
result << index_name
|
61
|
+
result << attrs.size
|
62
|
+
result + attrs
|
63
|
+
else
|
64
|
+
result
|
65
|
+
end
|
66
|
+
end
|
67
|
+
run_script(
|
68
|
+
:update_hash,
|
69
|
+
keys: [id, hash_tag],
|
70
|
+
argv: [model, ttl, index_attrs.size, range_index_attrs.size, custom_index_attrs_flat.size] +
|
71
|
+
index_attrs + range_index_attrs + custom_index_attrs_flat + args.to_a.flatten,
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
sig do
|
77
|
+
params(
|
78
|
+
model: String,
|
79
|
+
id: String,
|
80
|
+
index_attrs: T::Array[Symbol],
|
81
|
+
range_index_attrs: T::Array[Symbol],
|
82
|
+
custom_index_attrs: T::Hash[Symbol, T::Array],
|
83
|
+
).returns(Integer)
|
84
|
+
end
|
85
|
+
def delete_hash(model, id, index_attrs:, range_index_attrs:, custom_index_attrs:)
|
86
|
+
Redcord::Base.trace(
|
87
|
+
'redcord_redis_delete_hash',
|
88
|
+
model_name: model,
|
89
|
+
) do
|
90
|
+
custom_index_names = custom_index_attrs.keys
|
91
|
+
run_script(
|
92
|
+
:delete_hash,
|
93
|
+
keys: [id, id.match(/\{.*\}$/)&.send(:[], 0)],
|
94
|
+
argv: [model, index_attrs.size, range_index_attrs.size] + index_attrs + range_index_attrs + custom_index_names,
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
sig do
|
100
|
+
params(
|
101
|
+
model: String,
|
102
|
+
query_conditions: T::Hash[T.untyped, T.untyped],
|
103
|
+
index_attrs: T::Array[Symbol],
|
104
|
+
range_index_attrs: T::Array[Symbol],
|
105
|
+
select_attrs: T::Set[Symbol],
|
106
|
+
custom_index_attrs: T::Array[Symbol],
|
107
|
+
hash_tag: T.nilable(String),
|
108
|
+
custom_index_name: T.nilable(Symbol),
|
109
|
+
).returns(T::Hash[Integer, T::Hash[T.untyped, T.untyped]])
|
110
|
+
end
|
111
|
+
def find_by_attr(
|
112
|
+
model,
|
113
|
+
query_conditions,
|
114
|
+
select_attrs: Set.new,
|
115
|
+
index_attrs:,
|
116
|
+
range_index_attrs:,
|
117
|
+
custom_index_attrs: Array.new,
|
118
|
+
hash_tag: nil,
|
119
|
+
custom_index_name: nil
|
120
|
+
)
|
121
|
+
Redcord::Base.trace(
|
122
|
+
'redcord_redis_find_by_attr',
|
123
|
+
model_name: model,
|
124
|
+
) do
|
125
|
+
conditions = flatten_with_partial_sort(query_conditions.clone, custom_index_attrs)
|
126
|
+
res = run_script(
|
127
|
+
:find_by_attr,
|
128
|
+
keys: [hash_tag],
|
129
|
+
argv: [model, custom_index_name, index_attrs.size, range_index_attrs.size, custom_index_attrs.size, conditions.size] +
|
130
|
+
index_attrs + range_index_attrs + custom_index_attrs + conditions + select_attrs.to_a.flatten
|
131
|
+
)
|
132
|
+
# The Lua script will return this as a flattened array.
|
133
|
+
# Convert the result into a hash of {id -> model hash}
|
134
|
+
res_hash = res.each_slice(2)
|
135
|
+
res_hash.map { |key, val| [key, val.each_slice(2).to_h] }.to_h
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
sig do
|
140
|
+
params(
|
141
|
+
model: String,
|
142
|
+
query_conditions: T::Hash[T.untyped, T.untyped],
|
143
|
+
index_attrs: T::Array[Symbol],
|
144
|
+
range_index_attrs: T::Array[Symbol],
|
145
|
+
custom_index_attrs: T::Array[Symbol],
|
146
|
+
hash_tag: T.nilable(String),
|
147
|
+
custom_index_name: T.nilable(Symbol),
|
148
|
+
).returns(Integer)
|
149
|
+
end
|
150
|
+
def find_by_attr_count(
|
151
|
+
model,
|
152
|
+
query_conditions,
|
153
|
+
index_attrs:,
|
154
|
+
range_index_attrs:,
|
155
|
+
custom_index_attrs: Array.new,
|
156
|
+
hash_tag: nil,
|
157
|
+
custom_index_name: nil
|
158
|
+
)
|
159
|
+
Redcord::Base.trace(
|
160
|
+
'redcord_redis_find_by_attr_count',
|
161
|
+
model_name: model,
|
162
|
+
) do
|
163
|
+
conditions = flatten_with_partial_sort(query_conditions.clone, custom_index_attrs)
|
164
|
+
run_script(
|
165
|
+
:find_by_attr_count,
|
166
|
+
keys: [hash_tag],
|
167
|
+
argv: [model, custom_index_name, index_attrs.size, range_index_attrs.size, custom_index_attrs.size] +
|
168
|
+
index_attrs + range_index_attrs + custom_index_attrs + conditions
|
169
|
+
)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def scan_each_shard(key, &blk)
|
174
|
+
clients = instance_variable_get(:@client)
|
175
|
+
&.instance_variable_get(:@node)
|
176
|
+
&.instance_variable_get(:@clients)
|
177
|
+
&.values
|
178
|
+
|
179
|
+
if clients.nil?
|
180
|
+
scan_each(match: key, &blk)
|
181
|
+
else
|
182
|
+
clients.each do |client|
|
183
|
+
cursor = 0
|
184
|
+
loop do
|
185
|
+
cursor, keys = client.call([:scan, cursor, 'match', key])
|
186
|
+
keys.each(&blk)
|
187
|
+
break if cursor == "0"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def run_script(script_name, *args)
|
196
|
+
# Use EVAL when a redis shard has not loaded the script before
|
197
|
+
hash_var_name = :"@script_sha_#{script_name}"
|
198
|
+
hash = instance_variable_get(hash_var_name)
|
199
|
+
|
200
|
+
begin
|
201
|
+
return evalsha(hash, *args) if hash
|
202
|
+
rescue Redis::CommandError => e
|
203
|
+
if e.message != 'NOSCRIPT No matching script. Please use EVAL.'
|
204
|
+
raise e
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
script_content = Redcord::LuaScriptReader.read_lua_script(script_name.to_s)
|
209
|
+
instance_variable_set(hash_var_name, Digest::SHA1.hexdigest(script_content))
|
210
|
+
self.eval(script_content, *args)
|
211
|
+
end
|
212
|
+
|
213
|
+
# When using custom index: On Lua side script expects query conditions sorted
|
214
|
+
# in the order of appearance of attributes in specified index
|
215
|
+
sig { params(query_conditions: T::Hash[T.untyped, T.untyped], partial_order: T::Array[Symbol]).returns(T::Array[T.untyped]) }
|
216
|
+
def flatten_with_partial_sort(query_conditions, partial_order)
|
217
|
+
conditions = partial_order.inject([]) do |result, attr|
|
218
|
+
if !query_conditions[attr].nil?
|
219
|
+
result << attr << query_conditions.delete(attr)
|
220
|
+
end
|
221
|
+
result.flatten
|
222
|
+
end
|
223
|
+
conditions += query_conditions.to_a.flatten
|
224
|
+
end
|
225
|
+
end
|