redcord 0.1.0 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16cb660da0ea843f300ff18cc090ccfed7883c9d1ca48ef0bc5ed8d4e805854c
4
- data.tar.gz: f53002cb43b3e5286ba1f914c41ffd6b8a9722d2d33d9764deb7bcba88d2659c
3
+ metadata.gz: 134041206308ab5d1cb967dca5b5985b2bdd6b9c058601d7040d75d1119a2c98
4
+ data.tar.gz: d04f590786ab4cd9251ac32f5eea7d11e8103c448916c4aaee290db177370eca
5
5
  SHA512:
6
- metadata.gz: db443ffbf21066c549b30daed2ad44f836aa4469065b7bd8b6d0c13f45597bd7bdbf6c7ef4202ddf8103a1c2fbcfca28ca60f9f59b90584d0ec718d380d0f908
7
- data.tar.gz: 40ded5853307f453fe7c5ab2962e29cc081c05ab7f0c2cfddb898259e207ae9c343dfee6a67967449ff43c227c88db6f7a7922c425d8e6df92466b5ed778c5c2
6
+ metadata.gz: 374deab010a1f6b40e038adae19b4a7dee5485c5e17ff3a7e8fc92a4e839a2e9835166acd9b6369e7b628e206c1a280e943a28c040172d107c147d3be158fa8b
7
+ data.tar.gz: 28279ab008fcf6ba226d574250f882f35b05b69fc968794430c3d992c69fd686ec9c2f3fa7821aa6227426c7a027fda07c6e929624a533058c4a396c975f1140
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require 'connection_pool'
3
+ require_relative 'redis'
4
+
5
+ class Redcord::ConnectionPool
6
+ def initialize(pool_size:, timeout:, **client_options)
7
+ @connection_pool = ::ConnectionPool.new(size: pool_size, timeout: timeout) do
8
+ # Construct a new client every time the block gets called
9
+ Redcord::Redis.new(**client_options, logger: Redcord::Logger.proxy)
10
+ end
11
+ end
12
+
13
+ # Avoid method_missing when possible for better performance
14
+ methods = Set.new(Redcord::Redis.instance_methods(false) + Redis.instance_methods(false))
15
+ methods.each do |method_name|
16
+ define_method method_name do |*args, &blk|
17
+ @connection_pool.with do |redis|
18
+ redis.send(method_name, *args, &blk)
19
+ end
20
+ end
21
+ end
22
+
23
+ def method_missing(method_name, *args, &blk)
24
+ @connection_pool.with do |redis|
25
+ redis.send(method_name, *args, &blk)
26
+ end
27
+ end
28
+ end
data/lib/redcord/redis.rb CHANGED
@@ -18,24 +18,19 @@ class Redcord::Redis < Redis
18
18
  ).returns(String)
19
19
  end
20
20
  def create_hash_returning_id(key, args, ttl:, index_attrs:, range_index_attrs:, custom_index_attrs:, hash_tag: nil)
21
- Redcord::Base.trace(
22
- 'redcord_redis_create_hash_returning_id',
23
- model_name: key,
24
- ) do
25
- id = "#{SecureRandom.uuid}#{hash_tag}"
26
- custom_index_attrs_flat = custom_index_attrs.inject([]) do |result, (index_name, attrs)|
27
- result << index_name
28
- result << attrs.size
29
- result + attrs
30
- end
31
- run_script(
32
- :create_hash,
33
- keys: [id, hash_tag],
34
- argv: [key, ttl, index_attrs.size, range_index_attrs.size, custom_index_attrs_flat.size] +
35
- index_attrs + range_index_attrs + custom_index_attrs_flat + args.to_a.flatten,
36
- )
37
- id
21
+ id = "#{SecureRandom.uuid}#{hash_tag}"
22
+ custom_index_attrs_flat = custom_index_attrs.inject([]) do |result, (index_name, attrs)|
23
+ result << index_name
24
+ result << attrs.size
25
+ result + attrs
38
26
  end
27
+ run_script(
28
+ :create_hash,
29
+ keys: [id, hash_tag],
30
+ argv: [key, ttl, index_attrs.size, range_index_attrs.size, custom_index_attrs_flat.size] +
31
+ index_attrs + range_index_attrs + custom_index_attrs_flat + args.to_a.flatten,
32
+ )
33
+ id
39
34
  end
40
35
 
41
36
  sig do
@@ -51,26 +46,21 @@ class Redcord::Redis < Redis
51
46
  ).void
52
47
  end
53
48
  def update_hash(model, id, args, ttl:, index_attrs:, range_index_attrs:, custom_index_attrs:, hash_tag:)
54
- Redcord::Base.trace(
55
- 'redcord_redis_update_hash',
56
- model_name: model,
57
- ) do
58
- custom_index_attrs_flat = custom_index_attrs.inject([]) do |result, (index_name, attrs)|
59
- if !(args.keys.to_set & attrs.to_set).empty?
60
- result << index_name
61
- result << attrs.size
62
- result + attrs
63
- else
64
- result
65
- end
49
+ custom_index_attrs_flat = custom_index_attrs.inject([]) do |result, (index_name, attrs)|
50
+ if !(args.keys.to_set & attrs.to_set).empty?
51
+ result << index_name
52
+ result << attrs.size
53
+ result + attrs
54
+ else
55
+ result
66
56
  end
67
- run_script(
68
- :update_hash,
69
- keys: [id, hash_tag],
70
- argv: [model, ttl, index_attrs.size, range_index_attrs.size, custom_index_attrs_flat.size] +
71
- index_attrs + range_index_attrs + custom_index_attrs_flat + args.to_a.flatten,
72
- )
73
57
  end
58
+ run_script(
59
+ :update_hash,
60
+ keys: [id, hash_tag],
61
+ argv: [model, ttl, index_attrs.size, range_index_attrs.size, custom_index_attrs_flat.size] +
62
+ index_attrs + range_index_attrs + custom_index_attrs_flat + args.to_a.flatten,
63
+ )
74
64
  end
75
65
 
76
66
  sig do
@@ -83,17 +73,12 @@ class Redcord::Redis < Redis
83
73
  ).returns(Integer)
84
74
  end
85
75
  def delete_hash(model, id, index_attrs:, range_index_attrs:, custom_index_attrs:)
86
- Redcord::Base.trace(
87
- 'redcord_redis_delete_hash',
88
- model_name: model,
89
- ) do
90
- custom_index_names = custom_index_attrs.keys
91
- run_script(
92
- :delete_hash,
93
- keys: [id, id.match(/\{.*\}$/)&.send(:[], 0)],
94
- argv: [model, index_attrs.size, range_index_attrs.size] + index_attrs + range_index_attrs + custom_index_names,
95
- )
96
- end
76
+ custom_index_names = custom_index_attrs.keys
77
+ run_script(
78
+ :delete_hash,
79
+ keys: [id, id.match(/\{.*\}$/)&.send(:[], 0)],
80
+ argv: [model, index_attrs.size, range_index_attrs.size] + index_attrs + range_index_attrs + custom_index_names,
81
+ )
97
82
  end
98
83
 
99
84
  sig do
@@ -118,22 +103,17 @@ class Redcord::Redis < Redis
118
103
  hash_tag: nil,
119
104
  custom_index_name: nil
120
105
  )
121
- Redcord::Base.trace(
122
- 'redcord_redis_find_by_attr',
123
- model_name: model,
124
- ) do
125
- conditions = flatten_with_partial_sort(query_conditions.clone, custom_index_attrs)
126
- res = run_script(
127
- :find_by_attr,
128
- keys: [hash_tag],
129
- argv: [model, custom_index_name, index_attrs.size, range_index_attrs.size, custom_index_attrs.size, conditions.size] +
130
- index_attrs + range_index_attrs + custom_index_attrs + conditions + select_attrs.to_a.flatten
131
- )
132
- # The Lua script will return this as a flattened array.
133
- # Convert the result into a hash of {id -> model hash}
134
- res_hash = res.each_slice(2)
135
- res_hash.map { |key, val| [key, val.each_slice(2).to_h] }.to_h
136
- end
106
+ conditions = flatten_with_partial_sort(query_conditions.clone, custom_index_attrs)
107
+ res = run_script(
108
+ :find_by_attr,
109
+ keys: [hash_tag],
110
+ argv: [model, custom_index_name, index_attrs.size, range_index_attrs.size, custom_index_attrs.size, conditions.size] +
111
+ index_attrs + range_index_attrs + custom_index_attrs + conditions + select_attrs.to_a.flatten
112
+ )
113
+ # The Lua script will return this as a flattened array.
114
+ # Convert the result into a hash of {id -> model hash}
115
+ res_hash = res.each_slice(2)
116
+ res_hash.map { |key, val| [key, val.each_slice(2).to_h] }.to_h
137
117
  end
138
118
 
139
119
  sig do
@@ -156,33 +136,28 @@ class Redcord::Redis < Redis
156
136
  hash_tag: nil,
157
137
  custom_index_name: nil
158
138
  )
159
- Redcord::Base.trace(
160
- 'redcord_redis_find_by_attr_count',
161
- model_name: model,
162
- ) do
163
- conditions = flatten_with_partial_sort(query_conditions.clone, custom_index_attrs)
164
- run_script(
165
- :find_by_attr_count,
166
- keys: [hash_tag],
167
- argv: [model, custom_index_name, index_attrs.size, range_index_attrs.size, custom_index_attrs.size] +
168
- index_attrs + range_index_attrs + custom_index_attrs + conditions
169
- )
170
- end
139
+ conditions = flatten_with_partial_sort(query_conditions.clone, custom_index_attrs)
140
+ run_script(
141
+ :find_by_attr_count,
142
+ keys: [hash_tag],
143
+ argv: [model, custom_index_name, index_attrs.size, range_index_attrs.size, custom_index_attrs.size] +
144
+ index_attrs + range_index_attrs + custom_index_attrs + conditions
145
+ )
171
146
  end
172
147
 
173
- def scan_each_shard(key, &blk)
148
+ def scan_each_shard(key, count: 1000, &blk)
174
149
  clients = instance_variable_get(:@client)
175
150
  &.instance_variable_get(:@node)
176
151
  &.instance_variable_get(:@clients)
177
152
  &.values
178
153
 
179
154
  if clients.nil?
180
- scan_each(match: key, &blk)
155
+ scan_each(match: key, count: count, &blk)
181
156
  else
182
157
  clients.each do |client|
183
158
  cursor = 0
184
159
  loop do
185
- cursor, keys = client.call([:scan, cursor, 'match', key])
160
+ cursor, keys = client.call([:scan, cursor, 'match', key, 'count', count])
186
161
  keys.each(&blk)
187
162
  break if cursor == "0"
188
163
  end
@@ -6,11 +6,14 @@ require 'rails'
6
6
 
7
7
  require 'redcord/lua_script_reader'
8
8
  require 'redcord/redis'
9
+ require 'redcord/connection_pool'
9
10
 
10
11
  module Redcord::RedisConnection
11
12
  extend T::Sig
12
13
  extend T::Helpers
13
14
 
15
+ RedcordClientType = T.type_alias { T.any(Redcord::Redis, Redcord::ConnectionPool) }
16
+
14
17
  @connections = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
15
18
  @procs_to_prepare = T.let([], T::Array[Proc])
16
19
 
@@ -29,17 +32,17 @@ module Redcord::RedisConnection
29
32
  (env_config[name.underscore] || env_config['default']).symbolize_keys
30
33
  end
31
34
 
32
- sig { returns(Redcord::Redis) }
35
+ sig { returns(RedcordClientType) }
33
36
  def redis
34
37
  Redcord::RedisConnection.connections[name.underscore] ||= prepare_redis!
35
38
  end
36
39
 
37
- sig { returns(Redcord::Redis) }
40
+ sig { returns(RedcordClientType) }
38
41
  def establish_connection
39
42
  Redcord::RedisConnection.connections[name.underscore] = prepare_redis!
40
43
  end
41
44
 
42
- sig { params(redis: Redis).returns(Redcord::Redis) }
45
+ sig { params(redis: Redis).returns(RedcordClientType) }
43
46
  def redis=(redis)
44
47
  Redcord::RedisConnection.connections[name.underscore] =
45
48
  prepare_redis!(redis)
@@ -50,20 +53,21 @@ module Redcord::RedisConnection
50
53
  # definitions in each Redis query.
51
54
  #
52
55
  # TODO: Replace this with Redcord migrations
53
- sig { params(client: T.nilable(Redis)).returns(Redcord::Redis) }
56
+ sig { params(client: T.nilable(Redis)).returns(RedcordClientType) }
54
57
  def prepare_redis!(client = nil)
55
- return client if client.is_a?(Redcord::Redis)
56
-
57
- client = Redcord::Redis.new(
58
- **(
59
- if client.nil?
60
- connection_config
61
- else
62
- client.instance_variable_get(:@options)
63
- end
64
- ),
65
- logger: Redcord::Logger.proxy,
66
- )
58
+ return client if client.is_a?(Redcord::Redis) || client.is_a?(Redcord::ConnectionPool)
59
+
60
+ options = client.nil? ? connection_config : client.instance_variable_get(:@options)
61
+ client =
62
+ if options[:pool]
63
+ Redcord::ConnectionPool.new(
64
+ pool_size: options[:pool],
65
+ timeout: options[:connection_timeout] || 1.0,
66
+ **options
67
+ )
68
+ else
69
+ Redcord::Redis.new(**options, logger: Redcord::Logger.proxy)
70
+ end
67
71
 
68
72
  client.ping
69
73
  client
@@ -73,7 +77,7 @@ module Redcord::RedisConnection
73
77
  module InstanceMethods
74
78
  extend T::Sig
75
79
 
76
- sig { returns(Redcord::Redis) }
80
+ sig { returns(RedcordClientType) }
77
81
  def redis
78
82
  self.class.redis
79
83
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  # typed: strict
4
4
 
5
+ require 'active_support'
5
6
  require 'active_support/core_ext/array'
6
7
  require 'active_support/core_ext/module'
7
8
 
@@ -88,16 +89,21 @@ class Redcord::Relation
88
89
 
89
90
  sig { returns(Integer) }
90
91
  def count
91
- model.validate_index_attributes(query_conditions.keys, custom_index_name: custom_index_name)
92
- redis.find_by_attr_count(
93
- model.model_key,
94
- extract_query_conditions!,
95
- index_attrs: model._script_arg_index_attrs,
96
- range_index_attrs: model._script_arg_range_index_attrs,
97
- custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
98
- hash_tag: extract_hash_tag!,
99
- custom_index_name: custom_index_name
100
- )
92
+ Redcord::Base.trace(
93
+ 'redcord_relation_count',
94
+ model_name: model.name,
95
+ ) do
96
+ model.validate_index_attributes(query_conditions.keys, custom_index_name: custom_index_name)
97
+ redis.find_by_attr_count(
98
+ model.model_key,
99
+ extract_query_conditions!,
100
+ index_attrs: model._script_arg_index_attrs,
101
+ range_index_attrs: model._script_arg_range_index_attrs,
102
+ custom_index_attrs: model._script_arg_custom_index_attrs[custom_index_name],
103
+ hash_tag: extract_hash_tag!,
104
+ custom_index_name: custom_index_name
105
+ )
106
+ end
101
107
  end
102
108
 
103
109
  sig { params(index_name: T.nilable(Symbol)).returns(Redcord::Relation) }
@@ -243,7 +249,7 @@ class Redcord::Relation
243
249
  end
244
250
  end
245
251
 
246
- sig { returns(Redcord::Redis) }
252
+ sig { returns(Redcord::RedisConnection::RedcordClientType) }
247
253
  def redis
248
254
  model.redis
249
255
  end
@@ -46,10 +46,11 @@ local attr_selection_pos = query_cond_pos + ARGV[6]
46
46
 
47
47
  -- Get all ids which have the corresponding attribute values.
48
48
  local ids_set = nil
49
+ local index_sets, range_index_sets = {}, {}
49
50
 
50
51
  -- If custom index name is empty -> use single-attribute indices
51
52
  if index_name == '' then
52
- local index_sets, range_index_sets = unpack(validate_and_parse_query_conditions(
53
+ index_sets, range_index_sets = unpack(validate_and_parse_query_conditions(
53
54
  KEYS[1],
54
55
  model,
55
56
  to_set({unpack(ARGV, index_attr_pos, range_attr_pos - 1)}),
@@ -9,15 +9,15 @@ module Redcord::VacuumHelper
9
9
  sig { params(model: T.class_of(Redcord::Base)).void }
10
10
  def self.vacuum(model)
11
11
  model.class_variable_get(:@@index_attributes).each do |index_attr|
12
- puts "Vacuuming index attribute: #{index_attr}"
12
+ puts "Vacuuming index attribute: #{index_attr} for model: #{model.name}"
13
13
  _vacuum_index_attribute(model, index_attr)
14
14
  end
15
15
  model.class_variable_get(:@@range_index_attributes).each do |range_index_attr|
16
- puts "Vacuuming range index attribute: #{range_index_attr}"
16
+ puts "Vacuuming range index attribute: #{range_index_attr} for model: #{model.name}"
17
17
  _vacuum_range_index_attribute(model, range_index_attr)
18
18
  end
19
19
  model.class_variable_get(:@@custom_index_attributes).keys.each do |index_name|
20
- puts "Vacuuming custom index: #{index_name}"
20
+ puts "Vacuuming custom index: #{index_name} for model: #{model.name}"
21
21
  _vacuum_custom_index(model, index_name)
22
22
  end
23
23
  end
@@ -32,24 +32,27 @@ module Redcord::VacuumHelper
32
32
 
33
33
  sig { params(model: T.class_of(Redcord::Base), range_index_attr: Symbol).void }
34
34
  def self._vacuum_range_index_attribute(model, range_index_attr)
35
+ key_suffix = model.shard_by_attribute.nil? ? nil : '{*}'
35
36
  range_index_set_key = "#{model.model_key}:#{range_index_attr}"
36
37
  range_index_set_nil_key = "#{range_index_set_key}:"
37
38
 
38
39
  # Handle nil values for range index attributes, which are stored in a normal
39
40
  # set at Redcord:Model:range_index_attr:
40
- model.redis.scan_each_shard("#{range_index_set_nil_key}*") do |key|
41
+ model.redis.scan_each_shard("#{range_index_set_nil_key}#{key_suffix}") do |key|
41
42
  _remove_stale_ids_from_set(model, key)
42
43
  end
43
44
 
44
- model.redis.scan_each_shard("#{range_index_set_key}*") do |key|
45
+ model.redis.scan_each_shard("#{range_index_set_key}#{key_suffix}") do |key|
45
46
  _remove_stale_ids_from_sorted_set(model, key)
46
47
  end
47
48
  end
48
49
 
49
50
  sig { params(model: T.class_of(Redcord::Base), index_name: Symbol).void }
50
51
  def self._vacuum_custom_index(model, index_name)
52
+ key_suffix = model.shard_by_attribute.nil? ? nil : '{*}'
51
53
  custom_index_content_key = "#{model.model_key}:custom_index:#{index_name}_content"
52
- model.redis.scan_each_shard("#{custom_index_content_key}*") do |key|
54
+
55
+ model.redis.scan_each_shard("#{custom_index_content_key}#{key_suffix}") do |key|
53
56
  hash_tag = key.split(custom_index_content_key)[1] || ""
54
57
  _remove_stale_records_from_custom_index(model, hash_tag, index_name)
55
58
  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.1.0
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: connection_pool
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.2.3
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.2.3
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: sorbet
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -162,6 +176,7 @@ files:
162
176
  - lib/redcord/attribute.rb
163
177
  - lib/redcord/base.rb
164
178
  - lib/redcord/configurations.rb
179
+ - lib/redcord/connection_pool.rb
165
180
  - lib/redcord/logger.rb
166
181
  - lib/redcord/lua_script_reader.rb
167
182
  - lib/redcord/migration.rb
@@ -205,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
220
  - !ruby/object:Gem::Version
206
221
  version: '0'
207
222
  requirements: []
208
- rubygems_version: 3.0.8
223
+ rubygems_version: 3.1.6
209
224
  signing_key:
210
225
  specification_version: 4
211
226
  summary: A Ruby ORM like Active Record, but for Redis