redcord 0.0.1.alpha → 0.1.1

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/redcord.rb +30 -2
  3. data/lib/redcord.rbi +0 -16
  4. data/lib/redcord/actions.rb +171 -40
  5. data/lib/redcord/attribute.rb +126 -21
  6. data/lib/redcord/base.rb +15 -0
  7. data/lib/redcord/configurations.rb +4 -0
  8. data/lib/redcord/logger.rb +1 -1
  9. data/lib/redcord/lua_script_reader.rb +16 -5
  10. data/lib/redcord/migration.rb +2 -0
  11. data/lib/redcord/migration/index.rb +57 -0
  12. data/lib/redcord/migration/migrator.rb +1 -1
  13. data/lib/redcord/migration/ttl.rb +9 -4
  14. data/lib/redcord/migration/version.rb +3 -0
  15. data/lib/redcord/railtie.rb +18 -0
  16. data/lib/redcord/redis.rb +200 -0
  17. data/lib/redcord/redis_connection.rb +38 -29
  18. data/lib/redcord/relation.rb +214 -38
  19. data/lib/redcord/serializer.rb +147 -49
  20. data/lib/redcord/server_scripts/create_hash.erb.lua +81 -0
  21. data/lib/redcord/server_scripts/delete_hash.erb.lua +17 -8
  22. data/lib/redcord/server_scripts/find_by_attr.erb.lua +50 -16
  23. data/lib/redcord/server_scripts/find_by_attr_count.erb.lua +45 -14
  24. data/lib/redcord/server_scripts/shared/index_helper_methods.erb.lua +45 -16
  25. data/lib/redcord/server_scripts/shared/lua_helper_methods.erb.lua +20 -4
  26. data/lib/redcord/server_scripts/shared/query_helper_methods.erb.lua +86 -14
  27. data/lib/redcord/server_scripts/update_hash.erb.lua +40 -26
  28. data/lib/redcord/tasks/redis.rake +15 -0
  29. data/lib/redcord/tracer.rb +48 -0
  30. data/lib/redcord/vacuum_helper.rb +90 -0
  31. metadata +13 -11
  32. data/lib/redcord/prepared_redis.rb +0 -18
  33. data/lib/redcord/server_scripts.rb +0 -78
  34. data/lib/redcord/server_scripts/create_hash_returning_id.erb.lua +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abc776db5aa9171e1f8a478013a55179158d7c9dc97e65953c27c162ad4069c7
4
- data.tar.gz: 55d37148a8069725c09637f29392a6461993d93f6999bc76247e646f96a94528
3
+ metadata.gz: 525c939f4cee5b8adbb11b0530a2b2f44f6a26a8bb1a55e4dc837d725b3f71fb
4
+ data.tar.gz: f7e1747cc025310b20ab5e8d675fefe6d951ec60c5c09056454e11c8c366008e
5
5
  SHA512:
6
- metadata.gz: 1cfe27b4241554f39ce0f8f0dbce4492f308e3a1db5607f4dbb7153832f976b692987926a6cc9c7c051121f55819f9e9b9b10c02b82250986d3aa6fdf4964e8e
7
- data.tar.gz: fa4865ee7e9ad1b70b4c7037d196bb60ed2aed8ab774356ec07b06a9cefd1f964d05f4da1cdd7cd2e09a5366f7bfb8da06ab84e50e282cfc74cccbb16a185621
6
+ metadata.gz: 6e2ff5ff1008d0a9e59e02438010e78324ec86c3ce03c470f28175498c7b29fbd277f97509f59dead80e258609ed033c9d686119726553b206996c064d4c7827
7
+ data.tar.gz: bba7b07d1e6b222614667a271b25973f5d1693d2090cb533dcbbfaf092f6200cce75ce5a7cf76356277bc1d51ae5011405465caae64ad295b85faa63d016c191
@@ -1,11 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # typed: strict
2
- module Redcord
3
- end
4
4
 
5
5
  require 'sorbet-runtime'
6
6
 
7
+ module Redcord
8
+ extend T::Sig
9
+
10
+ @@configuration_blks = T.let(
11
+ [],
12
+ T::Array[T.proc.params(arg0: T.untyped).void],
13
+ )
14
+
15
+ sig {
16
+ params(
17
+ blk: T.proc.params(arg0: T.untyped).void,
18
+ ).void
19
+ }
20
+ def self.configure(&blk)
21
+ @@configuration_blks << blk
22
+ end
23
+
24
+ sig { void }
25
+ def self._after_initialize!
26
+ @@configuration_blks.each do |blk|
27
+ blk.call(Redcord::Base)
28
+ end
29
+
30
+ @@configuration_blks.clear
31
+ end
32
+ end
33
+
7
34
  require 'redcord/base'
8
35
  require 'redcord/migration'
9
36
  require 'redcord/migration/migrator'
10
37
  require 'redcord/migration/version'
11
38
  require 'redcord/railtie'
39
+ require 'redcord/vacuum_helper'
@@ -45,22 +45,6 @@ module Redcord::TTL::ClassMethods
45
45
  include Redcord::Serializer::ClassMethods
46
46
  end
47
47
 
48
- module Redcord::ServerScripts
49
- include Kernel
50
-
51
- sig do
52
- params(
53
- sha: String,
54
- keys: T::Array[T.untyped],
55
- argv: T::Array[T.untyped],
56
- ).returns(T.untyped)
57
- end
58
- def evalsha(sha, keys: [], argv:[]); end
59
-
60
- sig { returns(T::Hash[Symbol, String]) }
61
- def redcord_server_script_shas; end
62
- end
63
-
64
48
  module Redcord::Actions::ClassMethods
65
49
  include Kernel
66
50
  include Redcord::RedisConnection::ClassMethods
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # typed: strict
4
+
2
5
  require 'sorbet-coerce'
3
6
 
4
7
  require 'redcord/relation'
@@ -6,9 +9,7 @@ require 'redcord/relation'
6
9
  module Redcord
7
10
  # Raised by Model.find
8
11
  class RecordNotFound < StandardError; end
9
- # Raised by Model.where
10
- class AttributeNotIndexed < StandardError; end
11
- class WrongAttributeType < TypeError; end
12
+ class InvalidAction < StandardError; end
12
13
  end
13
14
 
14
15
  module Redcord::Actions
@@ -24,25 +25,65 @@ module Redcord::Actions
24
25
  module ClassMethods
25
26
  extend T::Sig
26
27
 
28
+ sig { returns(Integer) }
29
+ def count
30
+ Redcord::Base.trace(
31
+ 'redcord_actions_class_methods_count',
32
+ model_name: name,
33
+ ) do
34
+ res = 0
35
+ redis.scan_each_shard("#{model_key}:id:*") { res += 1 }
36
+ res
37
+ end
38
+ end
39
+
27
40
  sig { params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
28
41
  def create!(args)
29
- args[:created_at] = args[:updated_at] = Time.zone.now
30
- instance = TypeCoerce[self].new.from(args)
31
- id = redis.create_hash_returning_id(model_key, to_redis_hash(args))
32
- instance.send(:id=, id)
33
- instance
42
+ Redcord::Base.trace(
43
+ 'redcord_actions_class_methods_create!',
44
+ model_name: name,
45
+ ) do
46
+ self.props.keys.each { |attr_key| args[attr_key] = nil unless args.key?(attr_key) }
47
+ args[:created_at] = args[:updated_at] = Time.zone.now
48
+ instance = TypeCoerce[self].new.from(args)
49
+ id = redis.create_hash_returning_id(
50
+ model_key,
51
+ to_redis_hash(args),
52
+ ttl: _script_arg_ttl,
53
+ index_attrs: _script_arg_index_attrs,
54
+ range_index_attrs: _script_arg_range_index_attrs,
55
+ custom_index_attrs: _script_arg_custom_index_attrs,
56
+ hash_tag: instance.hash_tag,
57
+ )
58
+ instance.send(:id=, id)
59
+ instance
60
+ end
34
61
  end
35
62
 
36
63
  sig { params(id: T.untyped).returns(T.untyped) }
37
64
  def find(id)
38
- instance_key = "#{model_key}:id:#{id}"
39
- args = redis.hgetall(instance_key)
40
- if args.empty?
41
- raise Redcord::RecordNotFound.new(
42
- "Couldn't find #{name} with 'id'=#{id}"
43
- )
65
+ Redcord::Base.trace(
66
+ 'redcord_actions_class_methods_find',
67
+ model_name: name,
68
+ ) do
69
+ instance_key = "#{model_key}:id:#{id}"
70
+ args = redis.hgetall(instance_key)
71
+ if args.empty?
72
+ raise Redcord::RecordNotFound, "Couldn't find #{name} with 'id'=#{id}"
73
+ end
74
+
75
+ coerce_and_set_id(args, id)
76
+ end
77
+ end
78
+
79
+ sig { params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
80
+ def find_by(args)
81
+ Redcord::Base.trace(
82
+ 'redcord_actions_class_methods_find_by_args',
83
+ model_name: name,
84
+ ) do
85
+ where(args).to_a.first
44
86
  end
45
- coerce_and_set_id(args, id)
46
87
  end
47
88
 
48
89
  sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
@@ -52,7 +93,18 @@ module Redcord::Actions
52
93
 
53
94
  sig { params(id: T.untyped).returns(T::Boolean) }
54
95
  def destroy(id)
55
- return redis.delete_hash(model_key, id) == 1
96
+ Redcord::Base.trace(
97
+ 'redcord_actions_class_methods_destroy',
98
+ model_name: name,
99
+ ) do
100
+ redis.delete_hash(
101
+ model_key,
102
+ id,
103
+ index_attrs: _script_arg_index_attrs,
104
+ range_index_attrs: _script_arg_range_index_attrs,
105
+ custom_index_attrs: _script_arg_custom_index_attrs,
106
+ ) == 1
107
+ end
56
108
  end
57
109
  end
58
110
 
@@ -65,45 +117,124 @@ module Redcord::Actions
65
117
  sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
66
118
  def created_at; end
67
119
 
68
- sig { abstract.params(time: ActiveSupport::TimeWithZone).returns(T.nilable(ActiveSupport::TimeWithZone)) }
120
+ sig {
121
+ abstract.params(
122
+ time: ActiveSupport::TimeWithZone,
123
+ ).returns(T.nilable(ActiveSupport::TimeWithZone))
124
+ }
69
125
  def created_at=(time); end
70
126
 
71
127
  sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
72
128
  def updated_at; end
73
129
 
74
- sig { abstract.params(time: ActiveSupport::TimeWithZone).returns(T.nilable(ActiveSupport::TimeWithZone)) }
130
+ sig {
131
+ abstract.params(
132
+ time: ActiveSupport::TimeWithZone,
133
+ ).returns(T.nilable(ActiveSupport::TimeWithZone))
134
+ }
75
135
  def updated_at=(time); end
76
136
 
77
137
  sig { void }
78
138
  def save!
79
- self.updated_at = Time.zone.now
80
- _id = id
81
- if _id.nil?
82
- self.created_at = T.must(self.updated_at)
83
- _id = redis.create_hash_returning_id(self.class.model_key, self.class.to_redis_hash(serialize))
84
- send(:id=, _id)
85
- else
86
- redis.update_hash(self.class.model_key, _id, self.class.to_redis_hash(serialize))
139
+ Redcord::Base.trace(
140
+ 'redcord_actions_instance_methods_save!',
141
+ model_name: self.class.name,
142
+ ) do
143
+ self.updated_at = Time.zone.now
144
+ _id = id
145
+ if _id.nil?
146
+ serialized_instance = serialize
147
+ self.class.props.keys.each do |attr_key|
148
+ serialized_instance[attr_key.to_s] = nil unless serialized_instance.key?(attr_key.to_s)
149
+ end
150
+ self.created_at = T.must(self.updated_at)
151
+ _id = redis.create_hash_returning_id(
152
+ self.class.model_key,
153
+ self.class.to_redis_hash(serialized_instance),
154
+ ttl: self.class._script_arg_ttl,
155
+ index_attrs: self.class._script_arg_index_attrs,
156
+ range_index_attrs: self.class._script_arg_range_index_attrs,
157
+ custom_index_attrs: self.class._script_arg_custom_index_attrs,
158
+ hash_tag: hash_tag,
159
+ )
160
+ send(:id=, _id)
161
+ else
162
+ redis.update_hash(
163
+ self.class.model_key,
164
+ _id,
165
+ self.class.to_redis_hash(serialize),
166
+ ttl: self.class._script_arg_ttl,
167
+ index_attrs: self.class._script_arg_index_attrs,
168
+ range_index_attrs: self.class._script_arg_range_index_attrs,
169
+ custom_index_attrs: self.class._script_arg_custom_index_attrs,
170
+ hash_tag: hash_tag,
171
+ )
172
+ end
87
173
  end
88
174
  end
89
175
 
176
+ sig { returns(T::Boolean) }
177
+ def save
178
+ save!
179
+
180
+ true
181
+ rescue Redis::CommandError
182
+ # TODO: break down Redis::CommandError by parsing the error message
183
+ false
184
+ end
185
+
90
186
  sig { params(args: T::Hash[Symbol, T.untyped]).void }
91
- def update!(args={})
92
- _id = id
93
- if _id.nil?
94
- _set_args!(args)
95
- save!
96
- else
97
- args[:updated_at] = Time.zone.now
98
- _set_args!(args)
99
- redis.update_hash(self.class.model_key, _id, self.class.to_redis_hash(args))
187
+ def update!(args)
188
+ Redcord::Base.trace(
189
+ 'redcord_actions_instance_methods_update!',
190
+ model_name: self.class.name,
191
+ ) do
192
+ shard_by_attr = self.class.shard_by_attribute
193
+ if args.keys.include?(shard_by_attr)
194
+ raise Redcord::InvalidAction, "Cannot update shard_by attribute #{shard_by_attr}"
195
+ end
196
+
197
+ _id = id
198
+ if _id.nil?
199
+ _set_args!(args)
200
+ save!
201
+ else
202
+ args[:updated_at] = Time.zone.now
203
+ _set_args!(args)
204
+ redis.update_hash(
205
+ self.class.model_key,
206
+ _id,
207
+ self.class.to_redis_hash(args),
208
+ ttl: self.class._script_arg_ttl,
209
+ index_attrs: self.class._script_arg_index_attrs,
210
+ range_index_attrs: self.class._script_arg_range_index_attrs,
211
+ custom_index_attrs: self.class._script_arg_custom_index_attrs,
212
+ hash_tag: hash_tag,
213
+ )
214
+ end
100
215
  end
101
216
  end
102
217
 
218
+ sig { params(args: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
219
+ def update(args)
220
+ update!(args)
221
+
222
+ true
223
+ rescue Redis::CommandError
224
+ # TODO: break down Redis::CommandError by parsing the error message
225
+ false
226
+ end
227
+
103
228
  sig { returns(T::Boolean) }
104
229
  def destroy
105
- return false if id.nil?
106
- self.class.destroy(T.must(id))
230
+ Redcord::Base.trace(
231
+ 'redcord_actions_instance_methods_destroy',
232
+ model_name: self.class.name,
233
+ ) do
234
+ return false if id.nil?
235
+
236
+ self.class.destroy(T.must(id))
237
+ end
107
238
  end
108
239
 
109
240
  sig { returns(String) }
@@ -118,14 +249,14 @@ module Redcord::Actions
118
249
  end
119
250
  end
120
251
 
121
- sig { returns(T.nilable(Integer)) }
252
+ sig { returns(T.nilable(String)) }
122
253
  def id
123
254
  instance_variable_get(:@_id)
124
255
  end
125
256
 
126
- private
257
+ private
127
258
 
128
- sig { params(id: Integer).returns(Integer) }
259
+ sig { params(id: String).returns(String) }
129
260
  def id=(id)
130
261
  instance_variable_set(:@_id, id)
131
262
  end
@@ -1,19 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # typed: strict
4
+ module Redcord
5
+ class InvalidAttribute < StandardError; end
6
+ end
7
+
2
8
  module Redcord::Attribute
3
9
  extend T::Sig
4
10
  extend T::Helpers
5
11
 
6
- # We implicitly determine what should be a range index on Redis based on Ruby type.
12
+ # We implicitly determine what should be a range index on Redis based on Ruby
13
+ # type.
7
14
  RangeIndexType = T.type_alias {
8
- T.any(T.nilable(Time), T.nilable(Float), T.nilable(Integer))
15
+ T.any(
16
+ Float,
17
+ Integer,
18
+ NilClass,
19
+ Numeric,
20
+ Time,
21
+ )
9
22
  }
10
23
 
24
+ # Implicitly determine what data type can be a used in custom index on Redis based on Ruby type.
25
+ # Custom index currently supports positive integers with up to 19 characters in decimal notation,
26
+ # will raise error in Lua if bigger numbers are used.
27
+ CustomIndexType = T.type_alias {
28
+ T.any(
29
+ Integer,
30
+ Time,
31
+ )
32
+ }
33
+
11
34
  sig { params(klass: T.class_of(T::Struct)).void }
12
35
  def self.included(klass)
13
36
  klass.extend(ClassMethods)
37
+ klass.include(InstanceMethods)
14
38
  klass.class_variable_set(:@@index_attributes, Set.new)
15
39
  klass.class_variable_set(:@@range_index_attributes, Set.new)
40
+ klass.class_variable_set(:@@custom_index_attributes, Hash.new { |h, k| h[k] = [] })
16
41
  klass.class_variable_set(:@@ttl, nil)
42
+ klass.class_variable_set(:@@shard_by_attribute, nil)
17
43
  end
18
44
 
19
45
  module ClassMethods
@@ -26,48 +52,127 @@ module Redcord::Attribute
26
52
  options: T::Hash[Symbol, T.untyped],
27
53
  ).void
28
54
  end
29
- def attribute(name, type, options={})
55
+ def attribute(name, type, options = {})
30
56
  # TODO: support uniq options
57
+ # TODO: validate types
31
58
  prop(name, type)
32
- if options[:index]
33
- index_attribute(name, type)
34
- end
59
+
60
+ index_attribute(name, type) if options[:index]
35
61
  end
36
-
37
62
 
38
- sig { params(attr: Symbol, type: T.any(Class,T::Types::Base)).void }
63
+ sig { params(attr: Symbol, type: T.any(Class, T::Types::Base)).void }
39
64
  def index_attribute(attr, type)
40
65
  if should_range_index?(type)
41
66
  class_variable_get(:@@range_index_attributes) << attr
42
- sadd_proc_on_redis_connection("range_index_attrs", attr.to_s)
43
67
  else
44
68
  class_variable_get(:@@index_attributes) << attr
45
- sadd_proc_on_redis_connection("index_attrs", attr.to_s)
46
69
  end
47
70
  end
71
+
72
+ sig { params(index_name: Symbol, attrs: T::Array[Symbol]).void }
73
+ def custom_index(index_name, attrs)
74
+ attrs.each do |attr|
75
+ type = props[attr][:type]
76
+ if !can_custom_index?(type)
77
+ raise(Redcord::WrongAttributeType, "Custom index doesn't support '#{type}' attributes.")
78
+ end
79
+ end
80
+ shard_by_attr = class_variable_get(:@@shard_by_attribute)
81
+ if shard_by_attr and shard_by_attr != attrs.first
82
+ raise(
83
+ Redcord::CustomIndexInvalidDesign,
84
+ "shard_by attribute '#{shard_by_attr}' must be placed first in '#{index_name}' index"
85
+ )
86
+ end
87
+ class_variable_get(:@@custom_index_attributes)[index_name] = attrs
88
+ end
48
89
 
49
90
  sig { params(duration: T.nilable(ActiveSupport::Duration)).void }
50
91
  def ttl(duration)
51
92
  class_variable_set(:@@ttl, duration)
52
93
  end
53
94
 
54
- private
55
- sig { params(redis_key: String, item_to_add: String).void }
56
- def sadd_proc_on_redis_connection(redis_key, item_to_add)
57
- # TODO: Currently we're setting indexed attributes through procs that are run
58
- # when a RedisConnection is established. This should be replaced with migrations
59
- Redcord::RedisConnection.procs_to_prepare << Proc.new do |redis|
60
- redis.sadd("#{model_key}:#{redis_key}", item_to_add)
95
+ def shard_by_attribute(attr=nil)
96
+ return class_variable_get(:@@shard_by_attribute) if attr.nil?
97
+
98
+ # attr must be an non-index attribute (index: false)
99
+ if class_variable_get(:@@index_attributes).include?(attr) ||
100
+ class_variable_get(:@@range_index_attributes).include?(attr)
101
+ raise Redcord::InvalidAttribute, "Cannot shard by an index attribute '#{attr}'"
102
+ end
103
+
104
+ class_variable_get(:@@custom_index_attributes).each do |index_name, attrs|
105
+ if attr != attrs.first
106
+ raise(
107
+ Redcord::CustomIndexInvalidDesign,
108
+ "shard_by attribute '#{attr}' must be placed first in '#{index_name}' index"
109
+ )
110
+ end
111
+
112
+ # Delete the shard_by_attribute since it would be a constant in the
113
+ # custom index set
114
+ attrs.shift
61
115
  end
116
+
117
+ class_variable_set(:@@shard_by_attribute, attr)
118
+ end
119
+
120
+ sig { returns(Integer) }
121
+ def _script_arg_ttl
122
+ class_variable_get(:@@ttl)&.to_i || -1
123
+ end
124
+
125
+ sig { returns(T::Array[Symbol]) }
126
+ def _script_arg_index_attrs
127
+ class_variable_get(:@@index_attributes).to_a
128
+ end
129
+
130
+ sig { returns(T::Array[Symbol]) }
131
+ def _script_arg_range_index_attrs
132
+ class_variable_get(:@@range_index_attributes).to_a
133
+ end
134
+
135
+ sig { returns(T::Hash[Symbol, T::Array]) }
136
+ def _script_arg_custom_index_attrs
137
+ class_variable_get(:@@custom_index_attributes)
62
138
  end
63
139
 
64
- sig { params(type: T.any(Class,T::Types::Base)).returns(T::Boolean) }
140
+ private
141
+
142
+ sig { params(type: T.any(Class, T::Types::Base)).returns(T::Boolean) }
65
143
  def should_range_index?(type)
66
144
  # Change Ruby raw type to Sorbet type in order to call subtype_of?
67
- if type.is_a?(Class)
68
- type = T::Types::Simple.new(type)
145
+ type = T::Types::Simple.new(type) if type.is_a?(Class)
146
+
147
+ type.subtype_of?(RangeIndexType)
148
+ end
149
+
150
+ sig { params(type: T.any(Class, T::Types::Base)).returns(T::Boolean) }
151
+ def can_custom_index?(type)
152
+ # Change Ruby raw type to Sorbet type in order to call subtype_of?
153
+ type = T::Types::Simple.new(type) if type.is_a?(Class)
154
+ type.subtype_of?(CustomIndexType)
155
+ end
156
+ end
157
+
158
+ module InstanceMethods
159
+ extend T::Sig
160
+
161
+ sig { returns(T.nilable(String)) }
162
+ def hash_tag
163
+ attr = self.class.class_variable_get(:@@shard_by_attribute)
164
+
165
+ return nil if attr.nil?
166
+
167
+ # A blank hash tag would cause MOVED error in cluster mode
168
+ tag = send(attr)
169
+ default_tag = '__redcord_hash_tag_null__'
170
+
171
+ if tag == default_tag
172
+ raise Redcord::InvalidAttribute, "#{attr}=#{default_tag} conflicts with default hash_tag value"
69
173
  end
70
- return type.subtype_of?(RangeIndexType)
174
+
175
+ "{#{tag || default_tag}}"
71
176
  end
72
177
  end
73
178