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.
@@ -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(list)
2
+ local function to_hash(...)
3
3
  local hash = {}
4
- if not list then return hash end
5
- for i=1, #list, 2 do
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, fields)
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 fields and #fields > 0 then
20
- local values = redis.call('hmget', model .. ':id:' .. id, unpack(fields))
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(fields) do
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, args)
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 = 2
86
- while i <= #args do
87
- local attr_key, attr_val = args[i], args[i+1]
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 = args[i+1], args[i+2]
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[1] = redcord_instance.class.name
20
- -- KEYS[2] = redcord_instance.id
21
- -- ARGV[1...2N] = attr_key attr_val [attr_key attr_val ..]
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 = KEYS[1]
33
- local id = KEYS[2]
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 = redis.call('smembers', model .. ':index_attrs')
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 = redis.call('smembers', model .. ':range_index_attrs')
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
- if ttl then
81
- if ttl == '-1' then
82
- -- Persist the object if the ttl is set to -1
83
- redis.call('persist', key)
84
- else
85
- -- Reset the TTL for this object. We do this manually becaues altering the
86
- -- field value of a hash with HSET, etc. will leave the TTL
87
- -- untouched: https://redis.io/commands/expire
88
- redis.call('expire', key, ttl)
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