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
@@ -1,61 +1,90 @@
|
|
1
1
|
-- Add an id to the id set of the index attribute
|
2
|
-
local function add_id_to_index_attr(model, attr_key, attr_val, id)
|
2
|
+
local function add_id_to_index_attr(hash_tag, model, attr_key, attr_val, id)
|
3
3
|
if attr_val then
|
4
4
|
-- Call the Redis command: SADD "#{Model.name}:#{attr_name}:#{attr_val}" member ..
|
5
|
-
redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val, id)
|
5
|
+
redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
|
6
6
|
end
|
7
7
|
end
|
8
8
|
|
9
9
|
-- Remove an id from the id set of the index attribute
|
10
|
-
local function delete_id_from_index_attr(model, attr_key, attr_val, id)
|
10
|
+
local function delete_id_from_index_attr(hash_tag, model, attr_key, attr_val, id)
|
11
11
|
if attr_val then
|
12
12
|
-- Call the Redis command: SREM "#{Model.name}:#{attr_name}:#{attr_val}" member ..
|
13
|
-
redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val, id)
|
13
|
+
redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
-- Move an id from one id set to another for the index attribute
|
18
|
-
local function replace_id_in_index_attr(model, attr_key, prev_attr_val,curr_attr_val, id)
|
18
|
+
local function replace_id_in_index_attr(hash_tag, model, attr_key, prev_attr_val,curr_attr_val, id)
|
19
19
|
-- If previous and new value differs, then modify the id sets accordingly
|
20
20
|
if prev_attr_val ~= curr_attr_val then
|
21
|
-
delete_id_from_index_attr(model, attr_key, prev_attr_val, id)
|
22
|
-
add_id_to_index_attr(model, attr_key, curr_attr_val, id)
|
21
|
+
delete_id_from_index_attr(hash_tag, model, attr_key, prev_attr_val, id)
|
22
|
+
add_id_to_index_attr(hash_tag, model, attr_key, curr_attr_val, id)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
26
|
-- Add an id to the sorted id set of the range index attribute
|
27
|
-
local function add_id_to_range_index_attr(model, attr_key, attr_val, id)
|
27
|
+
local function add_id_to_range_index_attr(hash_tag, model, attr_key, attr_val, id)
|
28
28
|
if attr_val then
|
29
29
|
-- Nil values of range indices are sent to Redis as an empty string. They are stored
|
30
30
|
-- as a regular set at key "#{Model.name}:#{attr_name}:"
|
31
31
|
if attr_val == "" then
|
32
|
-
redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val, id)
|
32
|
+
redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
|
33
33
|
else
|
34
34
|
-- Call the Redis command: ZADD "#{Model.name}:#{attr_name}" #{attr_val} member ..,
|
35
35
|
-- where attr_val is the score of the sorted set
|
36
|
-
redis.call('zadd', model .. ':' .. attr_key, attr_val, id)
|
36
|
+
redis.call('zadd', model .. ':' .. attr_key .. hash_tag, attr_val, id)
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
-- Remove an id from the sorted id set of the range index attribute
|
42
|
-
local function delete_id_from_range_index_attr(model, attr_key, attr_val, id)
|
42
|
+
local function delete_id_from_range_index_attr(hash_tag, model, attr_key, attr_val, id)
|
43
43
|
if attr_val then
|
44
44
|
-- Nil values of range indices are sent to Redis as an empty string. They are stored
|
45
45
|
-- as a regular set at key "#{Model.name}:#{attr_name}:"
|
46
46
|
if attr_val == "" then
|
47
|
-
redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val, id)
|
47
|
+
redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
|
48
48
|
else
|
49
49
|
-- Call the Redis command: ZREM "#{Model.name}:#{attr_name}:#{attr_val}" member ..
|
50
|
-
redis.call('zrem', model .. ':' .. attr_key, id)
|
50
|
+
redis.call('zrem', model .. ':' .. attr_key .. hash_tag, id)
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
55
|
-- Move an id from one sorted id set to another for the range index attribute
|
56
|
-
local function replace_id_in_range_index_attr(model, attr_key, prev_attr_val, curr_attr_val, id)
|
56
|
+
local function replace_id_in_range_index_attr(hash_tag, model, attr_key, prev_attr_val, curr_attr_val, id)
|
57
57
|
if prev_attr_val ~= curr_attr_val then
|
58
|
-
delete_id_from_range_index_attr(model, attr_key, prev_attr_val, id)
|
59
|
-
add_id_to_range_index_attr(model, attr_key, curr_attr_val, id)
|
58
|
+
delete_id_from_range_index_attr(hash_tag, model, attr_key, prev_attr_val, id)
|
59
|
+
add_id_to_range_index_attr(hash_tag, model, attr_key, curr_attr_val, id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
-- Add an index record to the sorted set of the custom index
|
64
|
+
local function add_record_to_custom_index(hash_tag, model, index_name, attr_values, id)
|
65
|
+
local sep = ':'
|
66
|
+
if attr_values then
|
67
|
+
local index_string = ''
|
68
|
+
local attr_value_string = ''
|
69
|
+
for i, attr_value in ipairs(attr_values) do
|
70
|
+
if i > 1 then
|
71
|
+
index_string = index_string .. sep
|
72
|
+
end
|
73
|
+
attr_value_string = adjust_string_length(attr_value)
|
74
|
+
index_string = index_string .. attr_value_string
|
75
|
+
end
|
76
|
+
redis.call('zadd', model .. sep .. 'custom_index' .. sep .. index_name .. hash_tag, 0, index_string .. sep .. id)
|
77
|
+
redis.call('hset', model .. sep .. 'custom_index' .. sep .. index_name .. '_content' .. hash_tag, id, index_string .. sep .. id)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
-- Remove a record from the sorted set of the custom index
|
82
|
+
local function delete_record_from_custom_index(hash_tag, model, index_name, id)
|
83
|
+
local sep = ':'
|
84
|
+
local index_key = model .. sep .. 'custom_index' .. sep .. index_name
|
85
|
+
local index_string = redis.call('hget', index_key .. '_content' .. hash_tag, id)
|
86
|
+
if index_string then
|
87
|
+
redis.call('zremrangebylex', index_key .. hash_tag, '[' .. index_string, '[' .. index_string)
|
88
|
+
redis.call('hdel', index_key .. '_content' .. hash_tag, id)
|
60
89
|
end
|
61
90
|
end
|
@@ -1,9 +1,8 @@
|
|
1
1
|
-- Helper function to convert argument array to hash set
|
2
|
-
local function to_hash(
|
2
|
+
local function to_hash(...)
|
3
3
|
local hash = {}
|
4
|
-
|
5
|
-
|
6
|
-
hash[list[i]] = list[i+1]
|
4
|
+
for i=1, #arg, 2 do
|
5
|
+
hash[arg[i]] = arg[i+1]
|
7
6
|
end
|
8
7
|
return hash
|
9
8
|
end
|
@@ -31,3 +30,20 @@ local function set_list_intersect(set, list)
|
|
31
30
|
end
|
32
31
|
return set_intersect
|
33
32
|
end
|
33
|
+
|
34
|
+
-- Helper function to transform attribute values so that they become comparable as strings
|
35
|
+
local function adjust_string_length(value)
|
36
|
+
if value == '' or value == nil then
|
37
|
+
return '!'
|
38
|
+
end
|
39
|
+
if string.sub(value, 1, 1) == '-' then
|
40
|
+
error("Custom index currently doesn't support negative values")
|
41
|
+
end
|
42
|
+
local whole_digits_count = 19
|
43
|
+
local res = string.rep('0', whole_digits_count - string.len(value)) .. value
|
44
|
+
if string.len(res) > whole_digits_count then
|
45
|
+
error("Custom index can't be used if string representation of whole part of attribute value is longer than " ..
|
46
|
+
whole_digits_count .. ' characters')
|
47
|
+
end
|
48
|
+
return res
|
49
|
+
end
|
@@ -10,18 +10,37 @@ local function intersect_range_index_sets(set, tuples)
|
|
10
10
|
return set
|
11
11
|
end
|
12
12
|
|
13
|
+
-- Runs a query against a sorted set, extracts ids.
|
14
|
+
-- Response from redis: attr_value:[attr_value ...]:id
|
15
|
+
-- Returns a set of ids.
|
16
|
+
local function get_id_set_from_custom_index(set, query)
|
17
|
+
local ids = {}
|
18
|
+
local index_strings = {}
|
19
|
+
local sep = ':'
|
20
|
+
local id = ''
|
21
|
+
local key, min, max = unpack(query)
|
22
|
+
index_strings = redis.call('zrangebylex', key, min, max)
|
23
|
+
for _, index_string in ipairs(index_strings) do
|
24
|
+
id = string.match(index_string, '[^' .. sep .. ']+$')
|
25
|
+
table.insert(ids, id)
|
26
|
+
end
|
27
|
+
set = to_set(ids)
|
28
|
+
|
29
|
+
return set
|
30
|
+
end
|
31
|
+
|
13
32
|
-- Gets the hash of all the ids given. Returns the results in a
|
14
33
|
-- table, as well as any ids not found in Redis as a separate table
|
15
|
-
local function batch_hget(model, ids_set,
|
34
|
+
local function batch_hget(model, ids_set, ...)
|
16
35
|
local res, stale_ids = {}, {}
|
17
36
|
for id, _ in pairs(ids_set) do
|
18
37
|
local instance = nil
|
19
|
-
if
|
20
|
-
local values = redis.call('hmget', model .. ':id:' .. id,
|
38
|
+
if #{...}> 0 then
|
39
|
+
local values = redis.call('hmget', model .. ':id:' .. id, ...)
|
21
40
|
-- HMGET returns the value in the order of the fields given. Map back to
|
22
41
|
-- field value [field value ..]
|
23
42
|
instance = {}
|
24
|
-
for i, field in ipairs(
|
43
|
+
for i, field in ipairs({...}) do
|
25
44
|
if not values[i] then
|
26
45
|
instance = nil
|
27
46
|
break
|
@@ -76,30 +95,28 @@ end
|
|
76
95
|
-- attributes. Parse query conditions into two separate tables:
|
77
96
|
-- 1. index_sets formatted as the id set keys in Redis '#{Model.name}:#{attr_key}:#{attr_val}'
|
78
97
|
-- 2. range_index_sets formatted as a tuple {id set key, min, max} => { '#{Model.name}:#{attr_key}' min max }
|
79
|
-
local function validate_and_parse_query_conditions(model,
|
80
|
-
local index_attrs = to_set(redis.call('smembers', model .. ':index_attrs'))
|
81
|
-
local range_index_attrs = to_set(redis.call('smembers', model .. ':range_index_attrs'))
|
98
|
+
local function validate_and_parse_query_conditions(hash_tag, model, index_attrs, range_index_attrs, ...)
|
82
99
|
-- Iterate through the arguments of the script to form the redis keys at which the
|
83
100
|
-- indexed id sets are stored.
|
84
101
|
local index_sets, range_index_sets = {}, {}
|
85
|
-
local i =
|
86
|
-
while i <= #
|
87
|
-
local attr_key, attr_val =
|
102
|
+
local i = 1
|
103
|
+
while i <= #arg do
|
104
|
+
local attr_key, attr_val = arg[i], arg[i+1]
|
88
105
|
if index_attrs[attr_key] then
|
89
106
|
validate_attr_vals(attr_key, {attr_val})
|
90
107
|
-- For normal index attributes, keys are stored at "#{Model.name}:#{attr_key}:#{attr_val}"
|
91
|
-
table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val)
|
108
|
+
table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag)
|
92
109
|
i = i + 2
|
93
110
|
elseif range_index_attrs[attr_key] then
|
94
111
|
-- For range attributes, nil values are stored as normal sets
|
95
112
|
if attr_val == "" then
|
96
|
-
table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val)
|
113
|
+
table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag)
|
97
114
|
i = i + 2
|
98
115
|
else
|
99
|
-
local min, max =
|
116
|
+
local min, max = arg[i+1], arg[i+2]
|
100
117
|
validate_attr_vals(attr_key, {min, max})
|
101
118
|
-- For range index attributes, they are stored at "#{Model.name}:#{attr_key}"
|
102
|
-
table.insert(range_index_sets, {model .. ':' .. attr_key, min, max})
|
119
|
+
table.insert(range_index_sets, {model .. ':' .. attr_key .. hash_tag, min, max})
|
103
120
|
i = i + 3
|
104
121
|
end
|
105
122
|
else
|
@@ -108,3 +125,53 @@ local function validate_and_parse_query_conditions(model, args)
|
|
108
125
|
end
|
109
126
|
return {index_sets, range_index_sets}
|
110
127
|
end
|
128
|
+
|
129
|
+
-- Validates that attributes in query are in correct order and range condition is applied only on the last attribute.
|
130
|
+
-- '~' is used as a character that is lexicographically greater than any alphanumerical. '[' makes range inclusive (exclusive are not yet supported)
|
131
|
+
-- Returns a table {index_key, min_string, max_string} to be used for index query.
|
132
|
+
local function validate_and_parse_query_conditions_custom(hash_tag, model, index_name, custom_index_attrs, args)
|
133
|
+
if #custom_index_attrs == 0 then
|
134
|
+
error('Index ' .. index_name .. ' does not exist')
|
135
|
+
end
|
136
|
+
local sep = ':'
|
137
|
+
local i = 1
|
138
|
+
local j = 1
|
139
|
+
local min, value_string_min, query_string_min = '', '', ''
|
140
|
+
local max, value_string_max, query_string_max = '', '', ''
|
141
|
+
local is_prev_attr_query_range = false
|
142
|
+
while i <= #args do
|
143
|
+
if is_prev_attr_query_range then
|
144
|
+
error('Range can be applied to the last attribute of query only')
|
145
|
+
end
|
146
|
+
local attr_key = args[i]
|
147
|
+
if custom_index_attrs[j] == attr_key then
|
148
|
+
min, max = args[i+1], args[i+2]
|
149
|
+
if j > 1 then
|
150
|
+
query_string_min = query_string_min .. sep
|
151
|
+
query_string_max = query_string_max .. sep
|
152
|
+
else
|
153
|
+
query_string_min = query_string_min .. '['
|
154
|
+
query_string_max = query_string_max .. '['
|
155
|
+
end
|
156
|
+
if min ~= '-inf' then
|
157
|
+
value_string_min = adjust_string_length(min)
|
158
|
+
query_string_min = query_string_min .. value_string_min
|
159
|
+
end
|
160
|
+
if max ~= '+inf' then
|
161
|
+
value_string_max = adjust_string_length(max)
|
162
|
+
query_string_max = query_string_max .. value_string_max
|
163
|
+
else
|
164
|
+
query_string_max = query_string_max .. '~'
|
165
|
+
end
|
166
|
+
if min ~= max then
|
167
|
+
is_prev_attr_query_range = true
|
168
|
+
end
|
169
|
+
j = j + 1
|
170
|
+
i = i + 3
|
171
|
+
else
|
172
|
+
error(attr_key .. ' in position ' .. j .. ' is not supported by index ' .. index_name)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
query_string_max = query_string_max .. sep .. '~'
|
176
|
+
return {model .. sep .. 'custom_index' .. sep .. index_name .. hash_tag, query_string_min, query_string_max}
|
177
|
+
end
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# typed: strict
|
2
2
|
require 'redcord/migration/version'
|
3
3
|
require 'redcord/migration/migrator'
|
4
|
+
require 'redcord/vacuum_helper'
|
4
5
|
|
5
6
|
db_namespace = namespace :redis do
|
6
7
|
task migrate: :environment do
|
@@ -31,4 +32,18 @@ db_namespace = namespace :redis do
|
|
31
32
|
migration_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
32
33
|
puts "\nFinished in #{(migration_end - migration_start).round(3)} seconds"
|
33
34
|
end
|
35
|
+
|
36
|
+
task :vacuum, [:model_name] => :environment do |t, args|
|
37
|
+
desc "Vacuum index attributes for stale ids on a Redcord model"
|
38
|
+
$stdout.sync = true
|
39
|
+
model_name = args[:model_name]
|
40
|
+
puts "Attempting to vacuum the index attributes of the Redcord model: #{model_name}"
|
41
|
+
vacuum_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
42
|
+
|
43
|
+
Redcord::VacuumHelper.vacuum(Object.const_get(args[:model_name]))
|
44
|
+
vacuum_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
45
|
+
puts "Finished vacuuming #{model_name} in #{(vacuum_end - vacuum_start).round(3)} seconds"
|
46
|
+
rescue NameError => e
|
47
|
+
raise StandardError.new("#{args[:model_name]} is not a valid Redcord model.")
|
48
|
+
end
|
34
49
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module Redcord::VacuumHelper
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Helpers
|
8
|
+
|
9
|
+
sig { params(model: T.class_of(Redcord::Base)).void }
|
10
|
+
def self.vacuum(model)
|
11
|
+
model.class_variable_get(:@@index_attributes).each do |index_attr|
|
12
|
+
puts "Vacuuming index attribute: #{index_attr}"
|
13
|
+
_vacuum_index_attribute(model, index_attr)
|
14
|
+
end
|
15
|
+
model.class_variable_get(:@@range_index_attributes).each do |range_index_attr|
|
16
|
+
puts "Vacuuming range index attribute: #{range_index_attr}"
|
17
|
+
_vacuum_range_index_attribute(model, range_index_attr)
|
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
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(model: T.class_of(Redcord::Base), index_attr: Symbol).void }
|
26
|
+
def self._vacuum_index_attribute(model, index_attr)
|
27
|
+
# Scan through all index attribute values by matching on Redcord:Model:index_attr:*
|
28
|
+
model.redis.scan_each_shard("#{model.model_key}:#{index_attr}:*") do |key|
|
29
|
+
_remove_stale_ids_from_set(model, key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { params(model: T.class_of(Redcord::Base), range_index_attr: Symbol).void }
|
34
|
+
def self._vacuum_range_index_attribute(model, range_index_attr)
|
35
|
+
key_suffix = model.shard_by_attribute.nil? ? nil : '{*}'
|
36
|
+
range_index_set_key = "#{model.model_key}:#{range_index_attr}"
|
37
|
+
range_index_set_nil_key = "#{range_index_set_key}:"
|
38
|
+
|
39
|
+
# Handle nil values for range index attributes, which are stored in a normal
|
40
|
+
# set at Redcord:Model:range_index_attr:
|
41
|
+
model.redis.scan_each_shard("#{range_index_set_nil_key}#{key_suffix}") do |key|
|
42
|
+
_remove_stale_ids_from_set(model, key)
|
43
|
+
end
|
44
|
+
|
45
|
+
model.redis.scan_each_shard("#{range_index_set_key}#{key_suffix}") do |key|
|
46
|
+
_remove_stale_ids_from_sorted_set(model, key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(model: T.class_of(Redcord::Base), index_name: Symbol).void }
|
51
|
+
def self._vacuum_custom_index(model, index_name)
|
52
|
+
key_suffix = model.shard_by_attribute.nil? ? nil : '{*}'
|
53
|
+
custom_index_content_key = "#{model.model_key}:custom_index:#{index_name}_content"
|
54
|
+
|
55
|
+
model.redis.scan_each_shard("#{custom_index_content_key}#{key_suffix}") do |key|
|
56
|
+
hash_tag = key.split(custom_index_content_key)[1] || ""
|
57
|
+
_remove_stale_records_from_custom_index(model, hash_tag, index_name)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { params(model: T.class_of(Redcord::Base), set_key: String).void }
|
62
|
+
def self._remove_stale_ids_from_set(model, set_key)
|
63
|
+
model.redis.sscan_each(set_key) do |id|
|
64
|
+
if !model.redis.exists?("#{model.model_key}:id:#{id}")
|
65
|
+
model.redis.srem(set_key, id)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { params(model: T.class_of(Redcord::Base), sorted_set_key: String).void }
|
71
|
+
def self._remove_stale_ids_from_sorted_set(model, sorted_set_key)
|
72
|
+
model.redis.zscan_each(sorted_set_key) do |id, _|
|
73
|
+
if !model.redis.exists?("#{model.model_key}:id:#{id}")
|
74
|
+
model.redis.zrem(sorted_set_key, id)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { params(model: T.class_of(Redcord::Base), hash_tag: String, index_name: Symbol).void }
|
80
|
+
def self._remove_stale_records_from_custom_index(model, hash_tag, index_name)
|
81
|
+
index_key = "#{model.model_key}:custom_index:#{index_name}#{hash_tag}"
|
82
|
+
index_content_key = "#{model.model_key}:custom_index:#{index_name}_content#{hash_tag}"
|
83
|
+
model.redis.hscan_each(index_content_key).each do |id, index_string|
|
84
|
+
if !model.redis.exists?("#{model.model_key}:id:#{id}")
|
85
|
+
model.redis.hdel(index_content_key, id)
|
86
|
+
model.redis.zremrangebylex(index_key, "[#{index_string}", "[#{index_string}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|