redcord 0.0.2.alpha → 0.1.2

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: 679b0154429c95be05676a47608f0778c3d34e714d60eb5ec065863691fa9461
4
- data.tar.gz: 43847db0f9fd339c8a91db402b8b2da70ea4b3f874b6faf8a05d20f234cf0aea
3
+ metadata.gz: c0c8f566eb89b44bd73c4a444deb313af1004a342a703a0e9f504364aa96df40
4
+ data.tar.gz: e77507ef1ebb3a1d2a6ba9f0fc654d25355f5bc685b05e4bc07368957bdc7ec3
5
5
  SHA512:
6
- metadata.gz: b01c381b8e3436aaf0f4d2ebc8604bfd582c64663601a17044b0a29ca45c46e4e8ced22969ed5c53123af65808e1c4749cf262bec17e26468c1624c0023d2b9d
7
- data.tar.gz: cb4403b960c7f2e017143738edcbbfbe356e7e8c8e5b761d1f98482296541dcc2255b480d1285ce233d5add5c7e7e78fda29ab8f5fafd561f106a64e9e144460
6
+ metadata.gz: 27d9afec9f1a663516b65096439312bdc313aed672c6476bcf38d0ce98cd9178bebf3b5a46341d69ad6fc6f3f4268655956c6e5170af2f7da8489d68e343dee5
7
+ data.tar.gz: 1b011be75251942091acc209189f33d72ff289496a07f5700c140a06916f83ad310e1e804fc3bd60d77815f89b57fd6a66b9efe2d7fabd81d32dfca7f281b7d6
@@ -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
@@ -9,6 +9,7 @@ require 'redcord/relation'
9
9
  module Redcord
10
10
  # Raised by Model.find
11
11
  class RecordNotFound < StandardError; end
12
+ class InvalidAction < StandardError; end
12
13
  end
13
14
 
14
15
  module Redcord::Actions
@@ -24,29 +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, "Couldn't find #{name} with 'id'=#{id}"
42
- end
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
43
74
 
44
- coerce_and_set_id(args, id)
75
+ coerce_and_set_id(args, id)
76
+ end
45
77
  end
46
78
 
47
79
  sig { params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
48
80
  def find_by(args)
49
- where(args).to_a.first
81
+ Redcord::Base.trace(
82
+ 'redcord_actions_class_methods_find_by_args',
83
+ model_name: name,
84
+ ) do
85
+ where(args).to_a.first
86
+ end
50
87
  end
51
88
 
52
89
  sig { params(args: T::Hash[Symbol, T.untyped]).returns(Redcord::Relation) }
@@ -56,7 +93,18 @@ module Redcord::Actions
56
93
 
57
94
  sig { params(id: T.untyped).returns(T::Boolean) }
58
95
  def destroy(id)
59
- 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
60
108
  end
61
109
  end
62
110
 
@@ -88,46 +136,105 @@ module Redcord::Actions
88
136
 
89
137
  sig { void }
90
138
  def save!
91
- self.updated_at = Time.zone.now
92
- _id = id
93
- if _id.nil?
94
- self.created_at = T.must(self.updated_at)
95
- _id = redis.create_hash_returning_id(
96
- self.class.model_key,
97
- self.class.to_redis_hash(serialize),
98
- )
99
- send(:id=, _id)
100
- else
101
- redis.update_hash(
102
- self.class.model_key,
103
- _id,
104
- self.class.to_redis_hash(serialize),
105
- )
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
106
173
  end
107
174
  end
108
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
+
109
186
  sig { params(args: T::Hash[Symbol, T.untyped]).void }
110
- def update!(args = {})
111
- _id = id
112
- if _id.nil?
113
- _set_args!(args)
114
- save!
115
- else
116
- args[:updated_at] = Time.zone.now
117
- _set_args!(args)
118
- redis.update_hash(
119
- self.class.model_key,
120
- _id,
121
- self.class.to_redis_hash(args),
122
- )
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
123
215
  end
124
216
  end
125
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
+
126
228
  sig { returns(T::Boolean) }
127
229
  def destroy
128
- return false if id.nil?
230
+ Redcord::Base.trace(
231
+ 'redcord_actions_instance_methods_destroy',
232
+ model_name: self.class.name,
233
+ ) do
234
+ return false if id.nil?
129
235
 
130
- self.class.destroy(T.must(id))
236
+ self.class.destroy(T.must(id))
237
+ end
131
238
  end
132
239
 
133
240
  sig { returns(String) }
@@ -142,14 +249,14 @@ module Redcord::Actions
142
249
  end
143
250
  end
144
251
 
145
- sig { returns(T.nilable(Integer)) }
252
+ sig { returns(T.nilable(String)) }
146
253
  def id
147
254
  instance_variable_get(:@_id)
148
255
  end
149
256
 
150
257
  private
151
258
 
152
- sig { params(id: Integer).returns(Integer) }
259
+ sig { params(id: String).returns(String) }
153
260
  def id=(id)
154
261
  instance_variable_set(:@_id, id)
155
262
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # typed: strict
4
+ module Redcord
5
+ class InvalidAttribute < StandardError; end
6
+ end
4
7
 
5
8
  module Redcord::Attribute
6
9
  extend T::Sig
@@ -10,18 +13,33 @@ module Redcord::Attribute
10
13
  # type.
11
14
  RangeIndexType = T.type_alias {
12
15
  T.any(
13
- T.nilable(Float),
14
- T.nilable(Integer),
15
- T.nilable(Time),
16
+ Float,
17
+ Integer,
18
+ NilClass,
19
+ Numeric,
20
+ Time,
16
21
  )
17
22
  }
18
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
+
19
34
  sig { params(klass: T.class_of(T::Struct)).void }
20
35
  def self.included(klass)
21
36
  klass.extend(ClassMethods)
37
+ klass.include(InstanceMethods)
22
38
  klass.class_variable_set(:@@index_attributes, Set.new)
23
39
  klass.class_variable_set(:@@range_index_attributes, Set.new)
40
+ klass.class_variable_set(:@@custom_index_attributes, Hash.new { |h, k| h[k] = [] })
24
41
  klass.class_variable_set(:@@ttl, nil)
42
+ klass.class_variable_set(:@@shard_by_attribute, nil)
25
43
  end
26
44
 
27
45
  module ClassMethods
@@ -46,30 +64,81 @@ module Redcord::Attribute
46
64
  def index_attribute(attr, type)
47
65
  if should_range_index?(type)
48
66
  class_variable_get(:@@range_index_attributes) << attr
49
- sadd_proc_on_redis_connection('range_index_attrs', attr.to_s)
50
67
  else
51
68
  class_variable_get(:@@index_attributes) << attr
52
- sadd_proc_on_redis_connection('index_attrs', attr.to_s)
53
69
  end
54
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
55
89
 
56
90
  sig { params(duration: T.nilable(ActiveSupport::Duration)).void }
57
91
  def ttl(duration)
58
92
  class_variable_set(:@@ttl, duration)
59
93
  end
60
94
 
61
- private
95
+ def shard_by_attribute(attr=nil)
96
+ return class_variable_get(:@@shard_by_attribute) if attr.nil?
62
97
 
63
- sig { params(redis_key: String, item_to_add: String).void }
64
- def sadd_proc_on_redis_connection(redis_key, item_to_add)
65
- # TODO: Currently we're setting indexed attributes through procs that are
66
- # run when a RedisConnection is established. This should be replaced with
67
- # migrations
68
- Redcord::RedisConnection.procs_to_prepare << proc do |redis|
69
- redis.sadd("#{model_key}:#{redis_key}", item_to_add)
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}'"
70
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
115
+ end
116
+
117
+ class_variable_set(:@@shard_by_attribute, attr)
71
118
  end
72
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)
138
+ end
139
+
140
+ private
141
+
73
142
  sig { params(type: T.any(Class, T::Types::Base)).returns(T::Boolean) }
74
143
  def should_range_index?(type)
75
144
  # Change Ruby raw type to Sorbet type in order to call subtype_of?
@@ -77,6 +146,34 @@ module Redcord::Attribute
77
146
 
78
147
  type.subtype_of?(RangeIndexType)
79
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"
173
+ end
174
+
175
+ "{#{tag || default_tag}}"
176
+ end
80
177
  end
81
178
 
82
179
  mixes_in_class_methods(ClassMethods)