familia 1.2.1 → 2.0.0.pre2

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +4 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +16 -2
  11. data/Gemfile.lock +97 -36
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +192 -0
  15. data/familia.gemspec +10 -6
  16. data/lib/familia/base.rb +19 -9
  17. data/lib/familia/connection.rb +232 -65
  18. data/lib/familia/core_ext.rb +1 -1
  19. data/lib/familia/datatype/commands.rb +59 -0
  20. data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
  21. data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
  22. data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
  23. data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
  24. data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
  25. data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
  26. data/lib/familia/datatype.rb +243 -0
  27. data/lib/familia/errors.rb +5 -2
  28. data/lib/familia/features/expiration.rb +33 -34
  29. data/lib/familia/features/quantization.rb +9 -3
  30. data/lib/familia/features/safe_dump.rb +2 -3
  31. data/lib/familia/features.rb +2 -2
  32. data/lib/familia/horreum/class_methods.rb +97 -110
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +18 -13
  47. data/lib/middleware/database_middleware.rb +150 -0
  48. data/try/configuration/scenarios_try.rb +65 -0
  49. data/try/core/connection_try.rb +58 -0
  50. data/try/core/errors_try.rb +93 -0
  51. data/try/core/extensions_try.rb +26 -0
  52. data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
  53. data/try/{00_familia_try.rb → core/familia_try.rb} +7 -5
  54. data/try/core/middleware_try.rb +68 -0
  55. data/try/core/refinements_try.rb +39 -0
  56. data/try/core/settings_try.rb +76 -0
  57. data/try/core/tools_try.rb +54 -0
  58. data/try/core/utils_try.rb +189 -0
  59. data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
  60. data/try/datatypes/datatype_base_try.rb +69 -0
  61. data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
  62. data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
  63. data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
  64. data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
  65. data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
  66. data/try/edge_cases/empty_identifiers_try.rb +48 -0
  67. data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +21 -18
  88. data/try/models/datatype_base_try.rb +100 -0
  89. data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
  90. data/try/performance/benchmarks_try.rb +55 -0
  91. data/try/pooling/README.md +20 -0
  92. data/try/pooling/configurable_stress_test_try.rb +435 -0
  93. data/try/pooling/connection_pool_test_try.rb +273 -0
  94. data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  95. data/try/pooling/lib/connection_pool_metrics.rb +372 -0
  96. data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
  97. data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
  98. data/try/pooling/lib/visualize_stress_results.rb +434 -0
  99. data/try/pooling/pool_siege_try.rb +509 -0
  100. data/try/pooling/run_stress_tests_try.rb +482 -0
  101. data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
  102. data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
  103. data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
  104. data/try/prototypes/atomic_saves_v4.rb +105 -0
  105. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
  106. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  107. metadata +143 -46
  108. data/.github/workflows/ruby.yml +0 -71
  109. data/VERSION.yml +0 -4
  110. data/lib/familia/redistype/commands.rb +0 -59
  111. data/lib/familia/redistype.rb +0 -228
  112. data/lib/familia/tools.rb +0 -68
  113. data/lib/redis_middleware.rb +0 -109
  114. data/try/20_redis_type_try.rb +0 -70
  115. data/try/91_json_bug_try.rb +0 -86
@@ -1,12 +1,12 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/datatype/types/list.rb
2
2
 
3
3
  module Familia
4
- class List < RedisType
4
+ class List < DataType
5
5
 
6
6
  # Returns the number of elements in the list
7
7
  # @return [Integer] number of elements
8
8
  def element_count
9
- redis.llen rediskey
9
+ dbclient.llen dbkey
10
10
  end
11
11
  alias size element_count
12
12
 
@@ -16,8 +16,8 @@ module Familia
16
16
 
17
17
  def push *values
18
18
  echo :push, caller(1..1).first if Familia.debug
19
- values.flatten.compact.each { |v| redis.rpush rediskey, serialize_value(v) }
20
- redis.ltrim rediskey, -@opts[:maxlength], -1 if @opts[:maxlength]
19
+ values.flatten.compact.each { |v| dbclient.rpush dbkey, serialize_value(v) }
20
+ dbclient.ltrim dbkey, -@opts[:maxlength], -1 if @opts[:maxlength]
21
21
  update_expiration
22
22
  self
23
23
  end
@@ -29,20 +29,20 @@ module Familia
29
29
  alias add <<
30
30
 
31
31
  def unshift *values
32
- values.flatten.compact.each { |v| redis.lpush rediskey, serialize_value(v) }
32
+ values.flatten.compact.each { |v| dbclient.lpush dbkey, serialize_value(v) }
33
33
  # TODO: test maxlength
34
- redis.ltrim rediskey, 0, @opts[:maxlength] - 1 if @opts[:maxlength]
34
+ dbclient.ltrim dbkey, 0, @opts[:maxlength] - 1 if @opts[:maxlength]
35
35
  update_expiration
36
36
  self
37
37
  end
38
38
  alias prepend unshift
39
39
 
40
40
  def pop
41
- deserialize_value redis.rpop(rediskey)
41
+ deserialize_value dbclient.rpop(dbkey)
42
42
  end
43
43
 
44
44
  def shift
45
- deserialize_value redis.lpop(rediskey)
45
+ deserialize_value dbclient.lpop(dbkey)
46
46
  end
47
47
 
48
48
  def [](idx, count = nil)
@@ -65,7 +65,7 @@ module Familia
65
65
  # @param count [Integer] Number of elements to remove (0 means all)
66
66
  # @return [Integer] The number of removed elements
67
67
  def remove_element(value, count = 0)
68
- redis.lrem rediskey, count, serialize_value(value)
68
+ dbclient.lrem dbkey, count, serialize_value(value)
69
69
  end
70
70
  alias remove remove_element
71
71
 
@@ -75,7 +75,7 @@ module Familia
75
75
  end
76
76
 
77
77
  def rangeraw(sidx = 0, eidx = -1)
78
- redis.lrange(rediskey, sidx, eidx)
78
+ dbclient.lrange(dbkey, sidx, eidx)
79
79
  end
80
80
 
81
81
  def members(count = -1)
@@ -124,7 +124,7 @@ module Familia
124
124
  end
125
125
 
126
126
  def at(idx)
127
- deserialize_value redis.lindex(rediskey, idx)
127
+ deserialize_value dbclient.lindex(dbkey, idx)
128
128
  end
129
129
 
130
130
  def first
@@ -157,6 +157,6 @@ module Familia
157
157
  # end
158
158
  # end
159
159
 
160
- Familia::RedisType.register self, :list
160
+ Familia::DataType.register self, :list
161
161
  end
162
162
  end
@@ -1,11 +1,11 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/datatype/types/sorted_set.rb
2
2
 
3
3
  module Familia
4
- class SortedSet < RedisType
4
+ class SortedSet < DataType
5
5
  # Returns the number of elements in the sorted set
6
6
  # @return [Integer] number of elements
7
7
  def element_count
8
- redis.zcard rediskey
8
+ dbclient.zcard dbkey
9
9
  end
10
10
  alias size element_count
11
11
 
@@ -46,32 +46,32 @@ module Familia
46
46
  end
47
47
 
48
48
  def add(score, val)
49
- ret = redis.zadd rediskey, score, serialize_value(val)
49
+ ret = dbclient.zadd dbkey, score, serialize_value(val)
50
50
  update_expiration
51
51
  ret
52
52
  end
53
53
 
54
54
  def score(val)
55
- ret = redis.zscore rediskey, serialize_value(val, strict_values: false)
55
+ ret = dbclient.zscore dbkey, serialize_value(val, strict_values: false)
56
56
  ret&.to_f
57
57
  end
58
58
  alias [] score
59
59
 
60
60
  def member?(val)
61
- Familia.trace :MEMBER, redis, "#{val}<#{val.class}>", caller(1..1) if Familia.debug?
61
+ Familia.trace :MEMBER, dbclient, "#{val}<#{val.class}>", caller(1..1) if Familia.debug?
62
62
  !rank(val).nil?
63
63
  end
64
64
  alias include? member?
65
65
 
66
66
  # rank of member +v+ when ordered lowest to highest (starts at 0)
67
67
  def rank(v)
68
- ret = redis.zrank rediskey, serialize_value(v, strict_values: false)
68
+ ret = dbclient.zrank dbkey, serialize_value(v, strict_values: false)
69
69
  ret&.to_i
70
70
  end
71
71
 
72
72
  # rank of member +v+ when ordered highest to lowest (starts at 0)
73
73
  def revrank(v)
74
- ret = redis.zrevrank rediskey, serialize_value(v, strict_values: false)
74
+ ret = dbclient.zrevrank dbkey, serialize_value(v, strict_values: false)
75
75
  ret&.to_i
76
76
  end
77
77
 
@@ -140,12 +140,12 @@ module Familia
140
140
  def rangeraw(sidx, eidx, opts = {})
141
141
  # NOTE: :withscores (no underscore) is the correct naming for the
142
142
  # redis-4.x gem. We pass :withscores through explicitly b/c
143
- # redis.zrange et al only accept that one optional argument.
143
+ # dbclient.zrange et al only accept that one optional argument.
144
144
  # Passing `opts`` through leads to an ArgumentError:
145
145
  #
146
146
  # sorted_sets.rb:374:in `zrevrange': wrong number of arguments (given 4, expected 3) (ArgumentError)
147
147
  #
148
- redis.zrange(rediskey, sidx, eidx, **opts)
148
+ dbclient.zrange(dbkey, sidx, eidx, **opts)
149
149
  end
150
150
 
151
151
  def revrange(sidx, eidx, opts = {})
@@ -155,7 +155,7 @@ module Familia
155
155
  end
156
156
 
157
157
  def revrangeraw(sidx, eidx, opts = {})
158
- redis.zrevrange(rediskey, sidx, eidx, **opts)
158
+ dbclient.zrevrange(dbkey, sidx, eidx, **opts)
159
159
  end
160
160
 
161
161
  # e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]
@@ -167,7 +167,7 @@ module Familia
167
167
 
168
168
  def rangebyscoreraw(sscore, escore, opts = {})
169
169
  echo :rangebyscoreraw, caller(1..1).first if Familia.debug
170
- redis.zrangebyscore(rediskey, sscore, escore, **opts)
170
+ dbclient.zrangebyscore(dbkey, sscore, escore, **opts)
171
171
  end
172
172
 
173
173
  # e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]
@@ -180,19 +180,19 @@ module Familia
180
180
  def revrangebyscoreraw(sscore, escore, opts = {})
181
181
  echo :revrangebyscoreraw, caller(1..1).first if Familia.debug
182
182
  opts[:with_scores] = true if opts[:withscores]
183
- redis.zrevrangebyscore(rediskey, sscore, escore, opts)
183
+ dbclient.zrevrangebyscore(dbkey, sscore, escore, opts)
184
184
  end
185
185
 
186
186
  def remrangebyrank(srank, erank)
187
- redis.zremrangebyrank rediskey, srank, erank
187
+ dbclient.zremrangebyrank dbkey, srank, erank
188
188
  end
189
189
 
190
190
  def remrangebyscore(sscore, escore)
191
- redis.zremrangebyscore rediskey, sscore, escore
191
+ dbclient.zremrangebyscore dbkey, sscore, escore
192
192
  end
193
193
 
194
194
  def increment(val, by = 1)
195
- redis.zincrby(rediskey, by, val).to_i
195
+ dbclient.zincrby(dbkey, by, val).to_i
196
196
  end
197
197
  alias incr increment
198
198
  alias incrby increment
@@ -207,13 +207,13 @@ module Familia
207
207
  # @param value The value to remove from the sorted set
208
208
  # @return [Integer] The number of members that were removed (0 or 1)
209
209
  def remove_element(value)
210
- Familia.trace :REMOVE_ELEMENT, redis, "#{value}<#{value.class}>", caller(1..1) if Familia.debug?
210
+ Familia.trace :REMOVE_ELEMENT, dbclient, "#{value}<#{value.class}>", caller(1..1) if Familia.debug?
211
211
  # We use `strict_values: false` here to allow for the deletion of values
212
212
  # that are in the sorted set. If it's a horreum object, the value is
213
213
  # the identifier and not a serialized version of the object. So either
214
214
  # the value exists in the sorted set or it doesn't -- we don't need to
215
215
  # raise an error if it's not found.
216
- redis.zrem rediskey, serialize_value(value, strict_values: false)
216
+ dbclient.zrem dbkey, serialize_value(value, strict_values: false)
217
217
  end
218
218
  alias remove remove_element # deprecated
219
219
 
@@ -231,7 +231,7 @@ module Familia
231
231
  at(-1)
232
232
  end
233
233
 
234
- Familia::RedisType.register self, :sorted_set
235
- Familia::RedisType.register self, :zset
234
+ Familia::DataType.register self, :sorted_set
235
+ Familia::DataType.register self, :zset
236
236
  end
237
237
  end
@@ -1,7 +1,7 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/datatype/types/string.rb
2
2
 
3
3
  module Familia
4
- class String < RedisType
4
+ class String < DataType
5
5
  def init; end
6
6
 
7
7
  # Returns the number of elements in the list
@@ -17,22 +17,23 @@ module Familia
17
17
 
18
18
  def value
19
19
  echo :value, caller(0..0) if Familia.debug
20
- redis.setnx rediskey, @opts[:default] if @opts[:default]
21
- deserialize_value redis.get(rediskey)
20
+ dbclient.setnx dbkey, @opts[:default] if @opts[:default]
21
+ deserialize_value dbclient.get(dbkey)
22
22
  end
23
23
  alias content value
24
24
  alias get value
25
25
 
26
26
  def to_s
27
- value.to_s # value can return nil which to_s should not
27
+ return super if value.to_s.empty?
28
+ value.to_s
28
29
  end
29
30
 
30
31
  def to_i
31
- value.to_i
32
+ value&.to_i || 0
32
33
  end
33
34
 
34
35
  def value=(val)
35
- ret = redis.set(rediskey, serialize_value(val))
36
+ ret = dbclient.set(dbkey, serialize_value(val))
36
37
  update_expiration
37
38
  ret
38
39
  end
@@ -40,68 +41,68 @@ module Familia
40
41
  alias set value=
41
42
 
42
43
  def setnx(val)
43
- ret = redis.setnx(rediskey, serialize_value(val))
44
+ ret = dbclient.setnx(dbkey, serialize_value(val))
44
45
  update_expiration
45
46
  ret
46
47
  end
47
48
 
48
49
  def increment
49
- ret = redis.incr(rediskey)
50
+ ret = dbclient.incr(dbkey)
50
51
  update_expiration
51
52
  ret
52
53
  end
53
54
  alias incr increment
54
55
 
55
56
  def incrementby(val)
56
- ret = redis.incrby(rediskey, val.to_i)
57
+ ret = dbclient.incrby(dbkey, val.to_i)
57
58
  update_expiration
58
59
  ret
59
60
  end
60
61
  alias incrby incrementby
61
62
 
62
63
  def decrement
63
- ret = redis.decr rediskey
64
+ ret = dbclient.decr dbkey
64
65
  update_expiration
65
66
  ret
66
67
  end
67
68
  alias decr decrement
68
69
 
69
70
  def decrementby(val)
70
- ret = redis.decrby rediskey, val.to_i
71
+ ret = dbclient.decrby dbkey, val.to_i
71
72
  update_expiration
72
73
  ret
73
74
  end
74
75
  alias decrby decrementby
75
76
 
76
77
  def append(val)
77
- ret = redis.append rediskey, val
78
+ ret = dbclient.append dbkey, val
78
79
  update_expiration
79
80
  ret
80
81
  end
81
82
  alias << append
82
83
 
83
84
  def getbit(offset)
84
- redis.getbit rediskey, offset
85
+ dbclient.getbit dbkey, offset
85
86
  end
86
87
 
87
88
  def setbit(offset, val)
88
- ret = redis.setbit rediskey, offset, val
89
+ ret = dbclient.setbit dbkey, offset, val
89
90
  update_expiration
90
91
  ret
91
92
  end
92
93
 
93
94
  def getrange(spoint, epoint)
94
- redis.getrange rediskey, spoint, epoint
95
+ dbclient.getrange dbkey, spoint, epoint
95
96
  end
96
97
 
97
98
  def setrange(offset, val)
98
- ret = redis.setrange rediskey, offset, val
99
+ ret = dbclient.setrange dbkey, offset, val
99
100
  update_expiration
100
101
  ret
101
102
  end
102
103
 
103
104
  def getset(val)
104
- ret = redis.getset rediskey, val
105
+ ret = dbclient.getset dbkey, val
105
106
  update_expiration
106
107
  ret
107
108
  end
@@ -110,8 +111,8 @@ module Familia
110
111
  value.nil?
111
112
  end
112
113
 
113
- Familia::RedisType.register self, :string
114
- Familia::RedisType.register self, :counter
115
- Familia::RedisType.register self, :lock
114
+ Familia::DataType.register self, :string
115
+ Familia::DataType.register self, :counter
116
+ Familia::DataType.register self, :lock
116
117
  end
117
118
  end
@@ -1,12 +1,12 @@
1
- # frozen_string_literal: true
1
+ # lib/familia/datatype/types/unsorted_set.rb
2
2
 
3
3
  module Familia
4
- class Set < RedisType
4
+ class Set < DataType
5
5
 
6
6
  # Returns the number of elements in the unsorted set
7
7
  # @return [Integer] number of elements
8
8
  def element_count
9
- redis.scard rediskey
9
+ dbclient.scard dbkey
10
10
  end
11
11
  alias size element_count
12
12
 
@@ -15,7 +15,7 @@ module Familia
15
15
  end
16
16
 
17
17
  def add *values
18
- values.flatten.compact.each { |v| redis.sadd? rediskey, serialize_value(v) }
18
+ values.flatten.compact.each { |v| dbclient.sadd? dbkey, serialize_value(v) }
19
19
  update_expiration
20
20
  self
21
21
  end
@@ -33,7 +33,7 @@ module Familia
33
33
  alias to_a members
34
34
 
35
35
  def membersraw
36
- redis.smembers(rediskey)
36
+ dbclient.smembers(dbkey)
37
37
  end
38
38
 
39
39
  def each(&blk)
@@ -69,7 +69,7 @@ module Familia
69
69
  end
70
70
 
71
71
  def member?(val)
72
- redis.sismember rediskey, serialize_value(val)
72
+ dbclient.sismember dbkey, serialize_value(val)
73
73
  end
74
74
  alias include? member?
75
75
 
@@ -77,7 +77,7 @@ module Familia
77
77
  # @param value The value to remove from the set
78
78
  # @return [Integer] The number of members that were removed (0 or 1)
79
79
  def remove_element(value)
80
- redis.srem rediskey, serialize_value(value)
80
+ dbclient.srem dbkey, serialize_value(value)
81
81
  end
82
82
  alias remove remove_element # deprecated
83
83
 
@@ -86,11 +86,11 @@ module Familia
86
86
  end
87
87
 
88
88
  def pop
89
- redis.spop rediskey
89
+ dbclient.spop dbkey
90
90
  end
91
91
 
92
92
  def move(dstkey, val)
93
- redis.smove rediskey, dstkey, val
93
+ dbclient.smove dbkey, dstkey, val
94
94
  end
95
95
 
96
96
  def random
@@ -98,7 +98,7 @@ module Familia
98
98
  end
99
99
 
100
100
  def randomraw
101
- redis.srandmember(rediskey)
101
+ dbclient.srandmember(dbkey)
102
102
  end
103
103
 
104
104
  ## Make the value stored at KEY identical to the given list
@@ -122,6 +122,6 @@ module Familia
122
122
  # end
123
123
  # end
124
124
 
125
- Familia::RedisType.register self, :set
125
+ Familia::DataType.register self, :set
126
126
  end
127
127
  end
@@ -0,0 +1,243 @@
1
+ # lib/familia/datatype.rb
2
+
3
+ require_relative 'datatype/commands'
4
+ require_relative 'datatype/serialization'
5
+
6
+ module Familia
7
+
8
+ # DataType - Base class for Database data type wrappers
9
+ #
10
+ # This class provides common functionality for various Database data types
11
+ # such as String, List, Set, SortedSet, and HashKey.
12
+ #
13
+ # @abstract Subclass and implement Database data type specific methods
14
+ class DataType
15
+ include Familia::Base
16
+ extend Familia::Features
17
+
18
+ @registered_types = {}
19
+ @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix]
20
+ @logical_database = nil
21
+
22
+ feature :expiration
23
+ feature :quantization
24
+
25
+ class << self
26
+ attr_reader :registered_types, :valid_options, :has_relations
27
+ attr_accessor :parent
28
+ attr_writer :logical_database, :uri
29
+ end
30
+
31
+ module ClassMethods
32
+ # To be called inside every class that inherits DataType
33
+ # +methname+ is the term used for the class and instance methods
34
+ # that are created for the given +klass+ (e.g. set, list, etc)
35
+ def register(klass, methname)
36
+ Familia.ld "[#{self}] Registering #{klass} as #{methname.inspect}"
37
+
38
+ @registered_types[methname] = klass
39
+ end
40
+
41
+ def logical_database(val = nil)
42
+ @logical_database = val unless val.nil?
43
+ @logical_database || parent&.logical_database
44
+ end
45
+
46
+ def uri(val = nil)
47
+ @uri = val unless val.nil?
48
+ @uri || (parent ? parent.uri : Familia.uri)
49
+ end
50
+
51
+ def inherited(obj)
52
+ Familia.trace :DATATYPE, nil, "#{obj} is my kinda type", caller(1..1) if Familia.debug?
53
+ obj.logical_database = logical_database
54
+ obj.default_expiration = default_expiration # method added via Features::Expiration
55
+ obj.uri = uri
56
+ obj.parent = self
57
+ super(obj)
58
+ end
59
+
60
+ def valid_keys_only(opts)
61
+ opts.select { |k, _| DataType.valid_options.include? k }
62
+ end
63
+
64
+ def has_relations?
65
+ @has_relations ||= false
66
+ end
67
+ end
68
+ extend ClassMethods
69
+
70
+ attr_reader :keystring, :parent, :opts
71
+ attr_writer :dump_method, :load_method
72
+
73
+ # +keystring+: If parent is set, this will be used as the suffix
74
+ # for dbkey. Otherwise this becomes the value of the key.
75
+ # If this is an Array, the elements will be joined.
76
+ #
77
+ # Options:
78
+ #
79
+ # :class => A class that responds to Familia.load_method and
80
+ # Familia.dump_method. These will be used when loading and
81
+ # saving data from/to the database to unmarshal/marshal the class.
82
+ #
83
+ # :parent => The Familia object that this datatype object belongs
84
+ # to. This can be a class that includes Familia or an instance.
85
+ #
86
+ # :default_expiration => the time to live in seconds. When not nil, this will
87
+ # set the default expiration for this dbkey whenever #save is called.
88
+ # You can also call it explicitly via #update_expiration.
89
+ #
90
+ # :default => the default value (String-only)
91
+ #
92
+ # :logical_database => the logical database index to use (ignored if :dbclient is used).
93
+ #
94
+ # :dbclient => an instance of database client.
95
+ #
96
+ # :dbkey => a hardcoded key to use instead of the deriving the from
97
+ # the name and parent (e.g. a derived key: customer:custid:secret_counter).
98
+ #
99
+ # :suffix => the suffix to use for the key (e.g. 'scores' in customer:custid:scores).
100
+ # :prefix => the prefix to use for the key (e.g. 'customer' in customer:custid:scores).
101
+ #
102
+ # Connection precendence: uses the database connection of the parent or the
103
+ # value of opts[:dbclient] or Familia.dbclient (in that order).
104
+ def initialize(keystring, opts = {})
105
+ #Familia.ld " [initializing] #{self.class} #{opts}"
106
+ @keystring = keystring
107
+ @keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)
108
+
109
+ # Remove all keys from the opts that are not in the allowed list
110
+ @opts = opts || {}
111
+ @opts = DataType.valid_keys_only(@opts)
112
+
113
+ # Apply the options to instance method setters of the same name
114
+ @opts.each do |k, v|
115
+ # Bewarde logging :parent instance here implicitly calls #to_s which for
116
+ # some classes could include the identifier which could still be nil at
117
+ # this point. This would result in a Familia::Problem being raised. So
118
+ # to be on the safe-side here until we have a better understanding of
119
+ # the issue, we'll just log the class name for each key-value pair.
120
+ Familia.ld " [setting] #{k} #{v.class}"
121
+ send(:"#{k}=", v) if respond_to? :"#{k}="
122
+ end
123
+
124
+ init if respond_to? :init
125
+ end
126
+
127
+ def dbclient
128
+ return Fiber[:familia_transaction] if Fiber[:familia_transaction]
129
+ return @dbclient if @dbclient
130
+
131
+ parent? ? parent.dbclient : Familia.dbclient(opts[:logical_database])
132
+ end
133
+
134
+ # Produces the full dbkey for this object.
135
+ #
136
+ # @return [String] The full dbkey.
137
+ #
138
+ # This method determines the appropriate dbkey based on the context of the DataType object:
139
+ #
140
+ # 1. If a hardcoded key is set in the options, it returns that key.
141
+ # 2. For instance-level DataType objects, it uses the parent instance's dbkey method.
142
+ # 3. For class-level DataType objects, it uses the parent class's dbkey method.
143
+ # 4. For standalone DataType objects, it uses the keystring as the full dbkey.
144
+ #
145
+ # For class-level DataType objects (parent_class? == true):
146
+ # - The suffix is optional and used to differentiate between different types of objects.
147
+ # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
148
+ # - If a nil suffix is explicitly passed, it won't appear in the resulting dbkey.
149
+ # - Passing nil as the suffix is how class-level DataType objects are created without
150
+ # the global default 'object' suffix.
151
+ #
152
+ # @example Instance-level DataType
153
+ # user_instance.some_datatype.dbkey # => "user:123:some_datatype"
154
+ #
155
+ # @example Class-level DataType
156
+ # User.some_datatype.dbkey # => "user:some_datatype"
157
+ #
158
+ # @example Standalone DataType
159
+ # DataType.new("mykey").dbkey # => "mykey"
160
+ #
161
+ # @example Class-level DataType with explicit nil suffix
162
+ # User.dbkey("123", nil) # => "user:123"
163
+ #
164
+ def dbkey
165
+ # Return the hardcoded key if it's set. This is useful for
166
+ # support legacy keys that aren't derived in the same way.
167
+ return opts[:dbkey] if opts[:dbkey]
168
+
169
+ if parent_instance?
170
+ # This is an instance-level datatype object so the parent instance's
171
+ # dbkey method is defined in Familia::Horreum::InstanceMethods.
172
+ parent.dbkey(keystring)
173
+ elsif parent_class?
174
+ # This is a class-level datatype object so the parent class' dbkey
175
+ # method is defined in Familia::Horreum::ClassMethods.
176
+ parent.dbkey(keystring, nil)
177
+ else
178
+ # This is a standalone DataType object where it's keystring
179
+ # is the full database key (dbkey).
180
+ keystring
181
+ end
182
+ end
183
+
184
+ def class?
185
+ !@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
186
+ end
187
+
188
+ def parent_instance?
189
+ parent.is_a?(Familia::Horreum)
190
+ end
191
+
192
+ def parent_class?
193
+ parent.is_a?(Class) && parent <= Familia::Horreum
194
+ end
195
+
196
+ def parent?
197
+ parent_class? || parent_instance?
198
+ end
199
+
200
+ def parent
201
+ @opts[:parent]
202
+ end
203
+
204
+ def logical_database
205
+ @opts[:logical_database] || self.class.logical_database
206
+ end
207
+
208
+ def uri
209
+ # If a specific URI is set in opts, use it
210
+ return @opts[:uri] if @opts[:uri]
211
+
212
+ # If parent has a DB set, create a URI with that DB
213
+ if parent? && parent.respond_to?(:logical_database) && parent.logical_database
214
+ base_uri = self.class.uri || Familia.uri
215
+ if base_uri
216
+ uri_with_db = base_uri.dup
217
+ uri_with_db.db = parent.logical_database
218
+ return uri_with_db
219
+ end
220
+ end
221
+
222
+ # Otherwise fall back to class URI
223
+ self.class.uri
224
+ end
225
+
226
+ def dump_method
227
+ @dump_method || self.class.dump_method
228
+ end
229
+
230
+ def load_method
231
+ @load_method || self.class.load_method
232
+ end
233
+
234
+ include Commands
235
+ include Serialization
236
+ end
237
+
238
+ require_relative 'datatype/types/list'
239
+ require_relative 'datatype/types/unsorted_set'
240
+ require_relative 'datatype/types/sorted_set'
241
+ require_relative 'datatype/types/hashkey'
242
+ require_relative 'datatype/types/string'
243
+ end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
1
+ # lib/familia/errors.rb
2
+ #
3
3
  module Familia
4
4
  class Problem < RuntimeError; end
5
5
  class NoIdentifier < Problem; end
@@ -31,6 +31,9 @@ module Familia
31
31
  end
32
32
  end
33
33
 
34
+ # Set Familia.connection_provider or use middleware to provide connections.
35
+ class NoConnectionAvailable < Problem; end
36
+
34
37
  # Raised when attempting to refresh an object whose key doesn't exist in Redis
35
38
  class KeyNotFoundError < Problem
36
39
  attr_reader :key