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
@@ -16,21 +16,24 @@ nil
|
|
16
16
|
-- accessed by Lua using the ARGV global variable, very similarly to what
|
17
17
|
-- happens with keys (so ARGV[1], ARGV[2], ...).
|
18
18
|
--
|
19
|
-
-- KEYS
|
20
|
-
--
|
21
|
-
--
|
19
|
+
-- KEYS = redcord_instance.id hash_tag
|
20
|
+
-- ARGV = Model.name ttl index_attr_size range_index_attr_size custom_index_attrs_flat_size [index_attr_key ...] [range_index_attr_key ...]
|
21
|
+
-- [custom_index_name attrs_size [custom_index_attr_key ...] ...] attr_key attr_val [attr_key attr_val ..]
|
22
22
|
<%= include_lua 'shared/lua_helper_methods' %>
|
23
23
|
<%= include_lua 'shared/index_helper_methods' %>
|
24
24
|
|
25
25
|
if #KEYS ~= 2 then
|
26
26
|
error('Expected keys of be of size 2')
|
27
27
|
end
|
28
|
-
if #ARGV % 2 ~= 0 then
|
29
|
-
error('Expected an even number of arguments')
|
30
|
-
end
|
31
28
|
|
32
|
-
local model =
|
33
|
-
local id = KEYS
|
29
|
+
local model, ttl = unpack(ARGV)
|
30
|
+
local id, hash_tag = unpack(KEYS)
|
31
|
+
|
32
|
+
local index_attr_pos = 6
|
33
|
+
local range_attr_pos = index_attr_pos + ARGV[3]
|
34
|
+
local custom_attr_pos = range_attr_pos + ARGV[4]
|
35
|
+
-- Starting position of the attr_key-attr_val pairs
|
36
|
+
local attr_pos = custom_attr_pos + ARGV[5]
|
34
37
|
|
35
38
|
-- key = "#{model}:id:{id}"
|
36
39
|
local key = model .. ':id:' .. id
|
@@ -44,8 +47,8 @@ if redis.call('exists', key) == 0 then
|
|
44
47
|
end
|
45
48
|
|
46
49
|
-- Modify the id sets for any indexed attributes
|
47
|
-
local attrs_hash = to_hash(ARGV)
|
48
|
-
local indexed_attr_keys =
|
50
|
+
local attrs_hash = to_hash(unpack(ARGV, attr_pos))
|
51
|
+
local indexed_attr_keys = {unpack(ARGV, index_attr_pos, range_attr_pos - 1)}
|
49
52
|
if #indexed_attr_keys > 0 then
|
50
53
|
-- Get the previous and new values for indexed attributes
|
51
54
|
local prev_attrs = redis.call('hmget', key, unpack(indexed_attr_keys))
|
@@ -53,11 +56,11 @@ if #indexed_attr_keys > 0 then
|
|
53
56
|
local prev_attr_val, curr_attr_val = prev_attrs[i], attrs_hash[attr_key]
|
54
57
|
-- Skip attr values not present in the argument hash
|
55
58
|
if curr_attr_val then
|
56
|
-
replace_id_in_index_attr(model, attr_key, prev_attr_val, curr_attr_val, id)
|
59
|
+
replace_id_in_index_attr(hash_tag, model, attr_key, prev_attr_val, curr_attr_val, id)
|
57
60
|
end
|
58
61
|
end
|
59
62
|
end
|
60
|
-
local range_index_attr_keys =
|
63
|
+
local range_index_attr_keys = {unpack(ARGV, range_attr_pos, custom_attr_pos - 1)}
|
61
64
|
if #range_index_attr_keys > 0 then
|
62
65
|
-- Get the previous and new values for indexed attributes
|
63
66
|
local prev_attrs = redis.call('hmget', key, unpack(range_index_attr_keys))
|
@@ -65,27 +68,38 @@ if #range_index_attr_keys > 0 then
|
|
65
68
|
local prev_attr_val, curr_attr_val = prev_attrs[i], attrs_hash[attr_key]
|
66
69
|
-- Skip attr values not present in the argument hash
|
67
70
|
if curr_attr_val then
|
68
|
-
replace_id_in_range_index_attr(model, attr_key, prev_attr_val, curr_attr_val, id)
|
71
|
+
replace_id_in_range_index_attr(hash_tag, model, attr_key, prev_attr_val, curr_attr_val, id)
|
69
72
|
end
|
70
73
|
end
|
71
74
|
end
|
72
75
|
|
73
76
|
-- Forward the script arguments to the Redis command HSET and update the args.
|
74
77
|
-- Call the Redis command: HSET key [field value ...]
|
75
|
-
redis.call('hset', key, unpack(ARGV))
|
76
|
-
|
77
|
-
-- Call the Redis command: GET "#{Model.name}:ttl"
|
78
|
-
local ttl = redis.call('get', model .. ':ttl')
|
78
|
+
redis.call('hset', key, unpack(ARGV, attr_pos))
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
80
|
+
-- Update custom indexes
|
81
|
+
local updated_hash = to_hash(unpack(redis.call('hgetall', key)))
|
82
|
+
local custom_index_attr_keys = {unpack(ARGV, custom_attr_pos, attr_pos - 1)}
|
83
|
+
local i = 1
|
84
|
+
while i < #custom_index_attr_keys do
|
85
|
+
local index_name, attrs_num = custom_index_attr_keys[i], custom_index_attr_keys[i+1]
|
86
|
+
local attr_values = {}
|
87
|
+
for j, attr_key in ipairs({unpack(custom_index_attr_keys, i + 2, i + attrs_num + 1)}) do
|
88
|
+
attr_values[j] = updated_hash[attr_key]
|
89
89
|
end
|
90
|
+
delete_record_from_custom_index(hash_tag, model, index_name, id)
|
91
|
+
add_record_to_custom_index(hash_tag, model, index_name, attr_values, id)
|
92
|
+
i = i + 2 + attrs_num
|
93
|
+
end
|
94
|
+
|
95
|
+
-- Call the Redis command: GET "#{Model.name}:ttl"
|
96
|
+
if ttl == '-1' then
|
97
|
+
-- Persist the object if the ttl is set to -1
|
98
|
+
redis.call('persist', key)
|
99
|
+
else
|
100
|
+
-- Reset the TTL for this object. We do this manually becaues altering the
|
101
|
+
-- field value of a hash with HSET, etc. will leave the TTL
|
102
|
+
-- untouched: https://redis.io/commands/expire
|
103
|
+
redis.call('expire', key, ttl)
|
90
104
|
end
|
91
105
|
return nil
|
@@ -16,12 +16,16 @@ module Redcord::VacuumHelper
|
|
16
16
|
puts "Vacuuming range index attribute: #{range_index_attr}"
|
17
17
|
_vacuum_range_index_attribute(model, range_index_attr)
|
18
18
|
end
|
19
|
+
model.class_variable_get(:@@custom_index_attributes).keys.each do |index_name|
|
20
|
+
puts "Vacuuming custom index: #{index_name}"
|
21
|
+
_vacuum_custom_index(model, index_name)
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
sig { params(model: T.class_of(Redcord::Base), index_attr: Symbol).void }
|
22
26
|
def self._vacuum_index_attribute(model, index_attr)
|
23
27
|
# Scan through all index attribute values by matching on Redcord:Model:index_attr:*
|
24
|
-
model.redis.
|
28
|
+
model.redis.scan_each_shard("#{model.model_key}:#{index_attr}:*") do |key|
|
25
29
|
_remove_stale_ids_from_set(model, key)
|
26
30
|
end
|
27
31
|
end
|
@@ -29,12 +33,26 @@ module Redcord::VacuumHelper
|
|
29
33
|
sig { params(model: T.class_of(Redcord::Base), range_index_attr: Symbol).void }
|
30
34
|
def self._vacuum_range_index_attribute(model, range_index_attr)
|
31
35
|
range_index_set_key = "#{model.model_key}:#{range_index_attr}"
|
32
|
-
|
36
|
+
range_index_set_nil_key = "#{range_index_set_key}:"
|
33
37
|
|
34
38
|
# Handle nil values for range index attributes, which are stored in a normal
|
35
39
|
# set at Redcord:Model:range_index_attr:
|
36
|
-
|
37
|
-
|
40
|
+
model.redis.scan_each_shard("#{range_index_set_nil_key}*") do |key|
|
41
|
+
_remove_stale_ids_from_set(model, key)
|
42
|
+
end
|
43
|
+
|
44
|
+
model.redis.scan_each_shard("#{range_index_set_key}*") do |key|
|
45
|
+
_remove_stale_ids_from_sorted_set(model, key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(model: T.class_of(Redcord::Base), index_name: Symbol).void }
|
50
|
+
def self._vacuum_custom_index(model, index_name)
|
51
|
+
custom_index_content_key = "#{model.model_key}:custom_index:#{index_name}_content"
|
52
|
+
model.redis.scan_each_shard("#{custom_index_content_key}*") do |key|
|
53
|
+
hash_tag = key.split(custom_index_content_key)[1] || ""
|
54
|
+
_remove_stale_records_from_custom_index(model, hash_tag, index_name)
|
55
|
+
end
|
38
56
|
end
|
39
57
|
|
40
58
|
sig { params(model: T.class_of(Redcord::Base), set_key: String).void }
|
@@ -54,4 +72,16 @@ module Redcord::VacuumHelper
|
|
54
72
|
end
|
55
73
|
end
|
56
74
|
end
|
75
|
+
|
76
|
+
sig { params(model: T.class_of(Redcord::Base), hash_tag: String, index_name: Symbol).void }
|
77
|
+
def self._remove_stale_records_from_custom_index(model, hash_tag, index_name)
|
78
|
+
index_key = "#{model.model_key}:custom_index:#{index_name}#{hash_tag}"
|
79
|
+
index_content_key = "#{model.model_key}:custom_index:#{index_name}_content#{hash_tag}"
|
80
|
+
model.redis.hscan_each(index_content_key).each do |id, index_string|
|
81
|
+
if !model.redis.exists?("#{model.model_key}:id:#{id}")
|
82
|
+
model.redis.hdel(index_content_key, id)
|
83
|
+
model.redis.zremrangebylex(index_key, "[#{index_string}", "[#{index_string}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
57
87
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redcord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chan Zuckerberg Initiative
|
@@ -169,13 +169,13 @@ files:
|
|
169
169
|
- lib/redcord/migration/migrator.rb
|
170
170
|
- lib/redcord/migration/ttl.rb
|
171
171
|
- lib/redcord/migration/version.rb
|
172
|
-
- lib/redcord/prepared_redis.rb
|
173
172
|
- lib/redcord/railtie.rb
|
174
173
|
- lib/redcord/range_interval.rb
|
174
|
+
- lib/redcord/redis.rb
|
175
175
|
- lib/redcord/redis_connection.rb
|
176
176
|
- lib/redcord/relation.rb
|
177
177
|
- lib/redcord/serializer.rb
|
178
|
-
- lib/redcord/server_scripts/
|
178
|
+
- lib/redcord/server_scripts/create_hash.erb.lua
|
179
179
|
- lib/redcord/server_scripts/delete_hash.erb.lua
|
180
180
|
- lib/redcord/server_scripts/find_by_attr.erb.lua
|
181
181
|
- lib/redcord/server_scripts/find_by_attr_count.erb.lua
|
@@ -205,8 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
205
|
- !ruby/object:Gem::Version
|
206
206
|
version: '0'
|
207
207
|
requirements: []
|
208
|
-
|
209
|
-
rubygems_version: 2.7.6.2
|
208
|
+
rubygems_version: 3.0.8
|
210
209
|
signing_key:
|
211
210
|
specification_version: 4
|
212
211
|
summary: A Ruby ORM like Active Record, but for Redis
|
@@ -1,147 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
require 'redis'
|
3
|
-
|
4
|
-
# TODO: Rename Redcord::PreparedRedis -> Redcord::Redis
|
5
|
-
class Redcord::PreparedRedis < Redis
|
6
|
-
extend T::Sig
|
7
|
-
|
8
|
-
sig do
|
9
|
-
params(
|
10
|
-
key: T.any(String, Symbol),
|
11
|
-
args: T::Hash[T.untyped, T.untyped],
|
12
|
-
).returns(Integer)
|
13
|
-
end
|
14
|
-
def create_hash_returning_id(key, args)
|
15
|
-
Redcord::Base.trace(
|
16
|
-
'redcord_redis_create_hash_returning_id',
|
17
|
-
model_name: key,
|
18
|
-
) do
|
19
|
-
evalsha(
|
20
|
-
self.class.server_script_shas[:create_hash_returning_id],
|
21
|
-
keys: [key],
|
22
|
-
argv: args.to_a.flatten,
|
23
|
-
).to_i
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
sig do
|
28
|
-
params(
|
29
|
-
model: String,
|
30
|
-
id: Integer,
|
31
|
-
args: T::Hash[T.untyped, T.untyped],
|
32
|
-
).void
|
33
|
-
end
|
34
|
-
def update_hash(model, id, args)
|
35
|
-
Redcord::Base.trace(
|
36
|
-
'redcord_redis_update_hash',
|
37
|
-
model_name: model,
|
38
|
-
) do
|
39
|
-
evalsha(
|
40
|
-
self.class.server_script_shas[:update_hash],
|
41
|
-
keys: [model, id],
|
42
|
-
argv: args.to_a.flatten,
|
43
|
-
)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
sig do
|
48
|
-
params(
|
49
|
-
model: String,
|
50
|
-
id: Integer
|
51
|
-
).returns(Integer)
|
52
|
-
end
|
53
|
-
def delete_hash(model, id)
|
54
|
-
Redcord::Base.trace(
|
55
|
-
'redcord_redis_delete_hash',
|
56
|
-
model_name: model,
|
57
|
-
) do
|
58
|
-
evalsha(
|
59
|
-
self.class.server_script_shas[:delete_hash],
|
60
|
-
keys: [model, id]
|
61
|
-
)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
sig do
|
66
|
-
params(
|
67
|
-
model: String,
|
68
|
-
query_conditions: T::Hash[T.untyped, T.untyped],
|
69
|
-
select_attrs: T::Set[Symbol]
|
70
|
-
).returns(T::Hash[Integer, T::Hash[T.untyped, T.untyped]])
|
71
|
-
end
|
72
|
-
def find_by_attr(model, query_conditions, select_attrs=Set.new)
|
73
|
-
Redcord::Base.trace(
|
74
|
-
'redcord_redis_find_by_attr',
|
75
|
-
model_name: model,
|
76
|
-
) do
|
77
|
-
res = evalsha(
|
78
|
-
self.class.server_script_shas[:find_by_attr],
|
79
|
-
keys: [model] + query_conditions.to_a.flatten,
|
80
|
-
argv: select_attrs.to_a.flatten
|
81
|
-
)
|
82
|
-
# The Lua script will return this as a flattened array.
|
83
|
-
# Convert the result into a hash of {id -> model hash}
|
84
|
-
res_hash = res.each_slice(2)
|
85
|
-
res_hash.map { |key, val| [key.to_i, val.each_slice(2).to_h] }.to_h
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
sig do
|
90
|
-
params(
|
91
|
-
model: String,
|
92
|
-
query_conditions: T::Hash[T.untyped, T.untyped]
|
93
|
-
).returns(Integer)
|
94
|
-
end
|
95
|
-
def find_by_attr_count(model, query_conditions)
|
96
|
-
Redcord::Base.trace(
|
97
|
-
'redcord_redis_find_by_attr_count',
|
98
|
-
model_name: model,
|
99
|
-
) do
|
100
|
-
evalsha(
|
101
|
-
self.class.server_script_shas[:find_by_attr_count],
|
102
|
-
keys: [model] + query_conditions.to_a.flatten,
|
103
|
-
)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
sig { void }
|
108
|
-
def load_server_scripts!
|
109
|
-
script_names = Dir[File.join(
|
110
|
-
__dir__,
|
111
|
-
'server_scripts/*.lua',
|
112
|
-
)].map do |filename|
|
113
|
-
# lib/redcord/server_scripts/find_by_attr.erb.lua -> find_by_attr
|
114
|
-
T.must(filename.split('/').last).split('.').first&.to_sym
|
115
|
-
end
|
116
|
-
|
117
|
-
res = pipelined do
|
118
|
-
script_names.each do |script_name|
|
119
|
-
script(
|
120
|
-
:load,
|
121
|
-
Redcord::LuaScriptReader.read_lua_script(script_name.to_s),
|
122
|
-
)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
if self.class.class_variable_get(:@@server_script_shas).nil?
|
127
|
-
self.class.class_variable_set(
|
128
|
-
:@@server_script_shas,
|
129
|
-
script_names.zip(res).to_h
|
130
|
-
)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
@@server_script_shas = T.let(nil, T.nilable(T::Hash[Symbol, String]))
|
135
|
-
|
136
|
-
sig { returns(T::Hash[Symbol, String]) }
|
137
|
-
def self.server_script_shas
|
138
|
-
T.must(@@server_script_shas)
|
139
|
-
end
|
140
|
-
|
141
|
-
sig { void }
|
142
|
-
def self.load_server_scripts!
|
143
|
-
Redcord::Base.configurations[Rails.env].each do |_, config|
|
144
|
-
new(**(config.symbolize_keys)).load_server_scripts!
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
--[[
|
2
|
-
EVALSHA SHA1(__FILE__) [field value ...]
|
3
|
-
> Time complexity: O(N) where N is the number of fields being set.
|
4
|
-
|
5
|
-
Create a hash with the specified fields to their respective values stored at
|
6
|
-
key when key does not exist.
|
7
|
-
|
8
|
-
# Return value
|
9
|
-
The id of the created hash as a string.
|
10
|
-
--]]
|
11
|
-
|
12
|
-
-- The arguments can be accessed by Lua using the KEYS global variable in the
|
13
|
-
-- form of a one-based array (so KEYS[1], KEYS[2], ...).
|
14
|
-
-- All the additional arguments should not represent key names and can be
|
15
|
-
-- accessed by Lua using the ARGV global variable, very similarly to what
|
16
|
-
-- happens with keys (so ARGV[1], ARGV[2], ...).
|
17
|
-
|
18
|
-
-- KEYS[1] = Model.name
|
19
|
-
-- ARGV[1...2N] = attr_key attr_val [attr_key attr_val ..]
|
20
|
-
<%= include_lua 'shared/lua_helper_methods' %>
|
21
|
-
<%= include_lua 'shared/index_helper_methods' %>
|
22
|
-
|
23
|
-
-- Validate input to script before making Redis db calls
|
24
|
-
if #KEYS ~= 1 then
|
25
|
-
error('Expected keys to be of size 1')
|
26
|
-
end
|
27
|
-
if #ARGV % 2 ~= 0 then
|
28
|
-
error('Expected an even number of arguments')
|
29
|
-
end
|
30
|
-
|
31
|
-
local model = KEYS[1]
|
32
|
-
|
33
|
-
-- Call the Redis command: INCR "#{Model.name}:id_seq". If "#{Model.name}:id_seq" does
|
34
|
-
-- not exist, the command returns 0. It errors if the id_seq overflows a 64 bit
|
35
|
-
-- signed integer.
|
36
|
-
redis.call('incr', model .. ':id_seq')
|
37
|
-
|
38
|
-
-- The Lua version used by Redis does not support 64 bit integers:
|
39
|
-
-- https://github.com/antirez/redis/issues/5261
|
40
|
-
-- We ignore the integer response from INCR and use the string response from
|
41
|
-
-- the GET/MGET command.
|
42
|
-
local id, ttl = unpack(redis.call('mget', model .. ':id_seq', model .. ':ttl'))
|
43
|
-
local key = model .. ':id:' .. id
|
44
|
-
|
45
|
-
-- Forward the script arguments to the Redis command HSET.
|
46
|
-
-- Call the Redis command: HSET "#{Model.name}:id:#{id}" field value ...
|
47
|
-
redis.call('hset', key, unpack(ARGV))
|
48
|
-
|
49
|
-
-- Set TTL on key
|
50
|
-
if ttl and ttl ~= '-1' then
|
51
|
-
redis.call('expire', key, ttl)
|
52
|
-
end
|
53
|
-
|
54
|
-
-- Add id value for any index and range index attributes
|
55
|
-
local attrs_hash = to_hash(ARGV)
|
56
|
-
local index_attr_keys = redis.call('smembers', model .. ':index_attrs')
|
57
|
-
if #index_attr_keys > 0 then
|
58
|
-
for _, attr_key in ipairs(index_attr_keys) do
|
59
|
-
add_id_to_index_attr(model, attr_key, attrs_hash[attr_key], id)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
local range_index_attr_keys = redis.call('smembers', model .. ':range_index_attrs')
|
63
|
-
if #range_index_attr_keys > 0 then
|
64
|
-
for _, attr_key in ipairs(range_index_attr_keys) do
|
65
|
-
add_id_to_range_index_attr(model, attr_key, attrs_hash[attr_key], id)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
return id
|