redcord 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.scan_each(match: "#{model.model_key}:#{index_attr}:*") do |key|
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
- _remove_stale_ids_from_sorted_set(model, range_index_set_key)
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
- range_index_set_nil_key = "#{range_index_set_key}:"
37
- _remove_stale_ids_from_set(model, range_index_set_nil_key)
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
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/create_hash_returning_id.erb.lua
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
- rubyforge_project:
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