familia 1.2.0 → 2.0.0.pre.pre

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 +15 -2
  11. data/Gemfile.lock +76 -34
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +317 -0
  15. data/familia.gemspec +9 -5
  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 +17 -12
  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} +5 -3
  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} +20 -17
  88. data/try/models/datatype_base_try.rb +101 -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 +140 -43
  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
@@ -0,0 +1,273 @@
1
+ # try/pooling/connection_pool_test_try.rb
2
+
3
+ # USAGE: FAMILIA_TRACE=1 FAMILIA_DEBUG=1 bundle exec tryouts try/pooling/connection_pool_test_try.rb
4
+
5
+ require 'bundler/setup'
6
+ require 'securerandom'
7
+ require 'thread'
8
+ require_relative '../helpers/test_helpers'
9
+
10
+ # Configure connection pooling via connection_provider
11
+ require 'connection_pool'
12
+
13
+ # Create pools for each logical database
14
+ @pools = {}
15
+
16
+ Familia.connection_provider = lambda do |uri|
17
+ @pools[uri] ||= ConnectionPool.new(size: 5, timeout: 2) do
18
+ parsed = URI.parse(uri)
19
+ Redis.new(
20
+ host: parsed.host,
21
+ port: parsed.port,
22
+ db: parsed.db || 0
23
+ )
24
+ end
25
+
26
+ @pools[uri].with { |conn| conn }
27
+ end
28
+
29
+ # Test model for connection pool testing
30
+ class PoolTestAccount < Familia::Horreum
31
+ identifier_field :account_id
32
+ field :account_id
33
+ field :balance
34
+ field :holder_name
35
+
36
+ def init
37
+ @account_id ||= SecureRandom.hex(6)
38
+ @balance = @balance.to_f if @balance
39
+ end
40
+
41
+ def balance
42
+ @balance&.to_f
43
+ end
44
+ end
45
+
46
+ class PoolTestSession < Familia::Horreum
47
+ identifier_field :session_id
48
+ field :session_id
49
+ field :user_id
50
+ field :created_at
51
+
52
+ def init
53
+ @session_id ||= SecureRandom.hex(8)
54
+ @created_at ||= Time.now.to_i
55
+ end
56
+ end
57
+
58
+ ## Clean up before tests
59
+ PoolTestAccount.dbclient.flushdb
60
+ #=> "OK"
61
+
62
+ ## Test 1: Connection provider configuration
63
+ Familia.connection_provider.is_a?(Proc)
64
+ #=> true
65
+
66
+ ## Test 2: Connection pool created automatically
67
+ @account1 = PoolTestAccount.new(balance: 1000, holder_name: "Alice")
68
+ @account1.save
69
+ #=> true
70
+
71
+ ## Test 3: Basic pool functionality
72
+ @account1.balance
73
+ #=> 1000.0
74
+
75
+ ## Test 4: Multiple logical databases with separate pools
76
+ # Account in DB 0 (default)
77
+ @account_db0 = PoolTestAccount.new(balance: 500, holder_name: "Bob")
78
+ @account_db0.save
79
+ #=> true
80
+
81
+ ## Test 5: Account in DB 1 via class configuration
82
+ class PoolTestAccountDB1 < Familia::Horreum
83
+ self.logical_database = 1
84
+ identifier_field :account_id
85
+ field :account_id
86
+ field :balance
87
+ field :holder_name
88
+
89
+ def init
90
+ @account_id ||= SecureRandom.hex(6)
91
+ @balance = @balance.to_f if @balance
92
+ end
93
+
94
+ def balance
95
+ @balance&.to_f
96
+ end
97
+ end
98
+
99
+ @account_db1 = PoolTestAccountDB1.new(balance: 750, holder_name: "Charlie")
100
+ @account_db1.save
101
+ #=> true
102
+
103
+ ## Test 6: Verify accounts are in different databases
104
+ @account_db0.balance
105
+ #=> 500.0
106
+
107
+ ## Test 7: Verify DB1 account works independently
108
+ @account_db1.balance
109
+ #=> 750.0
110
+
111
+ ## Test 8: Connection pool thread safety
112
+ @results = []
113
+ @mutex = Mutex.new
114
+
115
+ # Create multiple threads performing concurrent operations
116
+ threads = 5.times.map do |i|
117
+ Thread.new do
118
+ account = PoolTestAccount.new(balance: 1000, holder_name: "Thread#{i}")
119
+ result = account.save
120
+ @mutex.synchronize { @results << result }
121
+ end
122
+ end
123
+
124
+ threads.each(&:join)
125
+ @results.all?
126
+ #=> true
127
+
128
+ ## Test 9: Thread safety verification
129
+ @results.size
130
+ #=> 5
131
+
132
+ ## Test 10: Transaction support with connection pools
133
+ @account_a = PoolTestAccount.new(balance: 1000, holder_name: "AccountA")
134
+ @account_b = PoolTestAccount.new(balance: 500, holder_name: "AccountB")
135
+ [@account_a.save, @account_b.save]
136
+ #=> [true, true]
137
+
138
+ ## Test 11: Multi/EXEC transaction operations
139
+ @transfer_result = Familia.transaction do |conn|
140
+ # Test that transaction connection is available
141
+ conn.ping
142
+ end
143
+ # Transaction returns array with results
144
+ @transfer_result.first
145
+ #=> "PONG"
146
+
147
+ ## Test 12: Transaction block executes properly
148
+ # Simple verification that accounts maintain their values
149
+ [@account_a.balance, @account_b.balance]
150
+ #=> [1000.0, 500.0]
151
+
152
+ ## Test 13: with_connection method
153
+ @connection_test_result = Familia.with_connection do |conn|
154
+ conn.set("test_key_#{SecureRandom.hex(4)}", "test_value")
155
+ end
156
+ @connection_test_result
157
+ #=> "OK"
158
+
159
+ ## Test 14: Pipeline operations with connection pool
160
+ @pipeline_results = Familia.pipeline do |conn|
161
+ conn.ping
162
+ end
163
+ # Pipeline executes successfully
164
+ @pipeline_results.first
165
+ #=> "PONG"
166
+
167
+ ## Test 15: Multi/EXEC operations with connection pool
168
+ @multi_results = Familia.multi do |conn|
169
+ conn.ping
170
+ end
171
+ # Multi/EXEC executes successfully
172
+ @multi_results.first
173
+ #=> "PONG"
174
+
175
+ ## Test 16: Error handling in transactions
176
+ @error_account = PoolTestAccount.new(balance: 100, holder_name: "ErrorTest")
177
+ @error_account.save
178
+ #=> true
179
+
180
+ ## Test 17: Transaction error handling
181
+ begin
182
+ Familia.transaction do |conn|
183
+ conn.ping
184
+ raise "Simulated error"
185
+ end
186
+ false
187
+ rescue => e
188
+ # Error propagates correctly from transaction block
189
+ true
190
+ end
191
+ #=> true
192
+
193
+ ## Test 18: Verify account state after transaction error
194
+ @error_account.refresh!
195
+ @error_account.balance
196
+ #=> 100.0
197
+
198
+ ## Test 19: Multiple pools created for different databases
199
+ @pools.size >= 1
200
+ #=> true
201
+
202
+ ## Test 20: Connection pool timeout handling
203
+ timeout_threads = []
204
+ timeout_results = []
205
+ timeout_mutex = Mutex.new
206
+
207
+ # Start threads that hold connections briefly
208
+ 3.times do |i|
209
+ timeout_threads << Thread.new do
210
+ begin
211
+ result = Familia.with_connection do |conn|
212
+ sleep(0.1) # Brief hold
213
+ conn.ping
214
+ end
215
+ timeout_mutex.synchronize { timeout_results << result }
216
+ rescue => e
217
+ timeout_mutex.synchronize { timeout_results << e.class.name }
218
+ end
219
+ end
220
+ end
221
+
222
+ timeout_threads.each(&:join)
223
+ timeout_results.all? { |r| r == "PONG" }
224
+ #=> true
225
+
226
+ ## Test 21: Debug mode validation (if enabled)
227
+ # This test only runs if FAMILIA_DEBUG=1 is set
228
+ if ENV['FAMILIA_DEBUG']
229
+ Familia.debug = true
230
+ # Test that debug mode doesn't break normal operation
231
+ debug_account = PoolTestAccount.new(balance: 123, holder_name: "Debug")
232
+ debug_account.save
233
+ else
234
+ true # Skip debug test if not in debug mode
235
+ end
236
+ #=> true
237
+
238
+ ## Test 22: Backward compatibility - existing code works unchanged
239
+ @compat_result = PoolTestAccount.dbclient.ping
240
+ @compat_result
241
+ #=> "PONG"
242
+
243
+ ## Test 23: Field operations work unchanged
244
+ @compat_account = PoolTestAccount.new(balance: 9999, holder_name: "Compat")
245
+ @compat_account.save
246
+ #=> true
247
+
248
+ ## Test 24: Direct field access works
249
+ @compat_account.hget("balance").to_f
250
+ #=> 9999.0
251
+
252
+ ## Test 25: Connection provider receives correct URIs
253
+ @captured_uris = []
254
+ original_provider = Familia.connection_provider
255
+
256
+ # Temporarily wrap provider to capture URIs
257
+ Familia.connection_provider = lambda do |uri|
258
+ @captured_uris << uri
259
+ original_provider.call(uri)
260
+ end
261
+
262
+ # Trigger some operations to capture URIs
263
+ test_account = PoolTestAccount.new(balance: 555, holder_name: "URITest")
264
+ test_account.save
265
+
266
+ # Restore original provider
267
+ Familia.connection_provider = original_provider
268
+
269
+ # Verify URIs contain database information
270
+ @captured_uris.any? { |uri| uri.include?('redis://') }
271
+ #=> true
272
+
273
+ puts "Connection pool tests completed successfully!"
@@ -0,0 +1,192 @@
1
+ # try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb
2
+
3
+ ##
4
+ # Atomic Save V3 Proof of Concept - Connection Pool Integration
5
+ #
6
+ # This implementation explores atomic saves with Database connection pooling
7
+ # for thread safety in multi-threaded environments (like Puma).
8
+ #
9
+ # Key Goals:
10
+ # 1. **Connection Pool Integration**: Use ConnectionPool gem for thread safety
11
+ # 2. **Dual Approach Testing**: Compare proxy vs explicit connection passing
12
+ # 3. **Thread Safety Validation**: Prove pool handles concurrent operations
13
+ # 4. **Separate Transaction Boundaries**: Each atomic block gets own transaction
14
+ #
15
+ # Approaches Tested:
16
+ # - Proxy Approach: Familia.atomic { ... } (transparent, V2 style)
17
+ # - Explicit Approach: Familia.atomic { |conn| ... } (clear boundaries)
18
+
19
+ require 'connection_pool'
20
+ require 'json'
21
+
22
+ # Test models
23
+ class BankAccount < Familia::Horreum
24
+ identifier_field :account_number
25
+ field :account_number
26
+ field :balance
27
+ field :holder_name
28
+ field :metadata # Variable-sized JSON field for workload simulation
29
+
30
+ def init
31
+ @account_number ||= SecureRandom.hex(8)
32
+ @balance = @balance.to_f if @balance
33
+ @metadata = @metadata.is_a?(String) ? JSON.parse(@metadata) : @metadata rescue @metadata
34
+ end
35
+
36
+ def balance
37
+ @balance&.to_f
38
+ end
39
+
40
+ def withdraw(amount)
41
+ raise "Insufficient funds" if balance < amount
42
+ self.balance -= amount
43
+ end
44
+
45
+ def deposit(amount)
46
+ self.balance += amount
47
+ end
48
+
49
+ def metadata=(value)
50
+ @metadata = value.is_a?(Hash) || value.is_a?(Array) ? JSON.generate(value) : value
51
+ end
52
+
53
+ # Add method that accepts explicit connection
54
+ def save(using: nil)
55
+ if using
56
+ # Use provided connection explicitly
57
+ original_instance = @dbclient
58
+ @dbclient = using
59
+ begin
60
+ super()
61
+ ensure
62
+ @dbclient = original_instance
63
+ end
64
+ else
65
+ # Use normal save behavior
66
+ super()
67
+ end
68
+ end
69
+ end
70
+
71
+ class TransactionRecord < Familia::Horreum
72
+ identifier_field :transaction_id
73
+ field :transaction_id
74
+ field :from_account
75
+ field :to_account
76
+ field :amount
77
+ field :status
78
+ field :created_at
79
+
80
+ def initialize(from: nil, to: nil, amount: 0)
81
+ @transaction_id = SecureRandom.hex(8)
82
+ @from_account = from
83
+ @to_account = to
84
+ @amount = amount.to_f
85
+ @status = "pending"
86
+ @created_at = Time.now.to_i
87
+ end
88
+
89
+ def amount
90
+ @amount&.to_f
91
+ end
92
+
93
+ def created_at
94
+ @created_at&.to_i
95
+ end
96
+
97
+ def save(using: nil)
98
+ if using
99
+ original_instance = @dbclient
100
+ @dbclient = using
101
+ begin
102
+ super()
103
+ ensure
104
+ @dbclient = original_instance
105
+ end
106
+ else
107
+ super()
108
+ end
109
+ end
110
+ end
111
+
112
+ module Familia
113
+ # Connection pool for Database connections
114
+ @@connection_pool = ConnectionPool.new(size: 10, timeout: 5) do
115
+ Redis.new(url: Familia.uri.to_s)
116
+ end
117
+
118
+ class << self
119
+ def connection_pool
120
+ @@connection_pool
121
+ end
122
+
123
+ def current_transaction
124
+ Thread.current[:familia_current_transaction_v3]
125
+ end
126
+
127
+ def current_transaction=(transaction)
128
+ Thread.current[:familia_current_transaction_v3] = transaction
129
+ end
130
+
131
+ # Proxy approach - transparent like V2
132
+ def atomic(&block)
133
+ if current_transaction
134
+ # Nested atomic - create separate transaction
135
+ atomic_separate(&block)
136
+ else
137
+ # Use connection pool to get connection
138
+ # For this prototype, we'll use a simple approach that works with Redis
139
+ connection_pool.with do |conn|
140
+ begin
141
+ # Store the connection for use within the block
142
+ self.current_transaction = conn
143
+ result = yield
144
+ result
145
+ ensure
146
+ self.current_transaction = nil
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ # Explicit approach - connection passed to block
153
+ def atomic_explicit(&block)
154
+ connection_pool.with do |conn|
155
+ # For this prototype, pass the connection directly
156
+ yield(conn)
157
+ end
158
+ end
159
+
160
+ # Helper for separate nested transactions
161
+ def atomic_separate(&block)
162
+ connection_pool.with do |conn|
163
+ begin
164
+ old_transaction = current_transaction
165
+ # Use a separate connection for nested transactions
166
+ self.current_transaction = conn
167
+ result = yield
168
+ result
169
+ ensure
170
+ self.current_transaction = old_transaction
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Override dbclient method for proxy approach
177
+ module ConnectionPoolRedis
178
+ def dbclient
179
+ Familia.current_transaction || super
180
+ end
181
+ end
182
+
183
+ # Inject into Horreum for proxy approach
184
+ class Horreum
185
+ prepend ConnectionPoolRedis
186
+ end
187
+
188
+ # Inject into DataType for proxy approach
189
+ class DataType
190
+ prepend ConnectionPoolRedis
191
+ end
192
+ end