redcord 0.0.1.alpha → 0.1.1

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