redcord 0.0.3 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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