familia 2.0.0.pre5 → 2.0.0.pre6

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +8 -5
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +4 -3
  5. data/docs/wiki/API-Reference.md +95 -18
  6. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  7. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  8. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  9. data/docs/wiki/Feature-System-Guide.md +600 -0
  10. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  11. data/docs/wiki/Field-System-Guide.md +784 -0
  12. data/docs/wiki/Home.md +72 -15
  13. data/docs/wiki/Implementation-Guide.md +126 -33
  14. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  15. data/docs/wiki/RelatableObjects-Guide.md +563 -0
  16. data/docs/wiki/Security-Model.md +65 -25
  17. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  18. data/lib/familia/base.rb +1 -1
  19. data/lib/familia/data_type/types/counter.rb +38 -0
  20. data/lib/familia/data_type/types/hashkey.rb +18 -0
  21. data/lib/familia/data_type/types/lock.rb +43 -0
  22. data/lib/familia/data_type/types/string.rb +9 -2
  23. data/lib/familia/data_type.rb +2 -2
  24. data/lib/familia/encryption/encrypted_data.rb +137 -0
  25. data/lib/familia/encryption/manager.rb +21 -4
  26. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  27. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  28. data/lib/familia/encryption.rb +1 -1
  29. data/lib/familia/errors.rb +17 -3
  30. data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
  31. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  32. data/lib/familia/features/expiration.rb +1 -1
  33. data/lib/familia/features/quantization.rb +1 -1
  34. data/lib/familia/features/safe_dump.rb +1 -1
  35. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  36. data/lib/familia/features/transient_fields.rb +1 -1
  37. data/lib/familia/field_type.rb +5 -2
  38. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  39. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  40. data/lib/familia/horreum/core/serialization.rb +535 -0
  41. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  42. data/lib/familia/horreum/core.rb +21 -0
  43. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  44. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +44 -28
  45. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  46. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  47. data/lib/familia/horreum.rb +17 -17
  48. data/lib/familia/version.rb +1 -1
  49. data/lib/familia.rb +1 -1
  50. data/try/core/create_method_try.rb +240 -0
  51. data/try/core/database_consistency_try.rb +299 -0
  52. data/try/core/errors_try.rb +25 -4
  53. data/try/core/familia_try.rb +1 -1
  54. data/try/core/persistence_operations_try.rb +297 -0
  55. data/try/data_types/counter_try.rb +93 -0
  56. data/try/data_types/lock_try.rb +133 -0
  57. data/try/debugging/debug_aad_process.rb +82 -0
  58. data/try/debugging/debug_concealed_internal.rb +59 -0
  59. data/try/debugging/debug_concealed_reveal.rb +61 -0
  60. data/try/debugging/debug_context_aad.rb +68 -0
  61. data/try/debugging/debug_context_simple.rb +80 -0
  62. data/try/debugging/debug_cross_context.rb +62 -0
  63. data/try/debugging/debug_database_load.rb +64 -0
  64. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  65. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  66. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  67. data/try/debugging/debug_field_decrypt.rb +74 -0
  68. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  69. data/try/debugging/debug_load_path.rb +66 -0
  70. data/try/debugging/debug_method_definition.rb +46 -0
  71. data/try/debugging/debug_method_resolution.rb +41 -0
  72. data/try/debugging/debug_minimal.rb +24 -0
  73. data/try/debugging/debug_provider.rb +68 -0
  74. data/try/debugging/debug_secure_behavior.rb +73 -0
  75. data/try/debugging/debug_string_class.rb +46 -0
  76. data/try/debugging/debug_test.rb +46 -0
  77. data/try/debugging/debug_test_design.rb +80 -0
  78. data/try/encryption/encryption_core_try.rb +3 -3
  79. data/try/features/encrypted_fields_core_try.rb +19 -11
  80. data/try/features/encrypted_fields_integration_try.rb +66 -70
  81. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  82. data/try/features/encrypted_fields_security_try.rb +151 -144
  83. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  84. data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
  85. data/try/features/encryption_fields/context_isolation_try.rb +29 -8
  86. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  87. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  88. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  89. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  90. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  91. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  92. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  93. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  94. data/try/features/feature_dependencies_try.rb +3 -3
  95. data/try/features/transient_fields_core_try.rb +1 -1
  96. data/try/features/transient_fields_integration_try.rb +1 -1
  97. data/try/helpers/test_helpers.rb +25 -0
  98. data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
  99. data/try/horreum/initialization_try.rb +1 -1
  100. data/try/horreum/relations_try.rb +1 -1
  101. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  102. data/try/horreum/serialization_try.rb +39 -4
  103. data/try/models/customer_safe_dump_try.rb +1 -1
  104. data/try/models/customer_try.rb +1 -1
  105. metadata +51 -10
  106. data/TEST_COVERAGE.md +0 -40
  107. data/lib/familia/horreum/serialization.rb +0 -473
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5517961b53f3213a7d92f9bd07001291bcdae102f1ca5cf59570537020e4fc2f
4
- data.tar.gz: 44610f83cf5b65d1cde483c91b9a259816bb1040da2b1ff77867061be8aeaf36
3
+ metadata.gz: 93cb8ea627c19daff3566c7373aa45ff04adb6d37a3346c3b3049712c18838ea
4
+ data.tar.gz: 9d359e2067062a4c6afe91a76985362989a569611400f10f7155e505dd55e39b
5
5
  SHA512:
6
- metadata.gz: 9e02c91aa204b248bf6853637c21ffdeabc90d7b04661c5ddc2cb99260fc0ebb6cc2d4780aed0d45e6f30e64e49229e15201551073f5c804fa8c2a0add328c97
7
- data.tar.gz: ef296043a9b843fa27745d15b505fa11f2df4f5baeb505189d02013fb3f02cfccb49f9d3606912b101ed95ef6b2f3967ade5b8ff79e67c6e50ebda7a81ef0e15
6
+ metadata.gz: e020a48703858e929eeb11188029893656ef84ce98113e4c448d41e354edb4fe77df420aa4bab33b7775f0719ad5a693fc672c8159cd0e963be7750daa12d5b9
7
+ data.tar.gz: c3e66800df77ccd33ccd5a6027d031173642446210309e60bf83243f59585a0416ea99002063f0080828eb24d1aa1fa50f3013e7dbf408b2299f2fea617b2fa3
data/CLAUDE.md CHANGED
@@ -6,11 +6,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  ### Testing
8
8
 
9
- A couple rules when writing tests:
10
- 1) Every tryouts file has three sections: setup, testcases, teardown.
11
- 2) Every tryouts testcase also has three parts: description, code, expectations.
12
- 3) Tryouts tests are meant to double as documentation examples; keep that in mind when considering syntax choices.
13
- 4) There are multiple kinds of expectations: `#=>` is the default comparison, `#=:>` is a class comparison via `is_a?` or `kind_of?`, `#=!>` is an exception class which allows you to knowingly raise an exception without needing a begin/rescue.
9
+ Tryouts framework rules:
10
+ 1) **Structure**: 3 sections - setup (optional), testcases, teardown (optional)
11
+ 2) **Test cases**: Use `##` for descriptions, Ruby code, then `#=>` expectations
12
+ 3) **Variables**: Instance variables (`@var`) persist across all sections
13
+ 4) **Expectations**: `#=>` (value), `#==>` (boolean), `#=:>` (type), `#=!>` (exception)
14
+ 5) **Comments**: Use single `#` prefix, DO NOT label sections
15
+ 6) **Philosophy**: Plain realistic code, avoid mocks/test DSL
16
+ 7) **Result**: Last expression in each test case is the result
14
17
 
15
18
  - **Run tests**: `bundle exec try` (uses tryouts testing framework)
16
19
  - **Run specific test file, verbose**: `bundle exec try -v try/specific_test_try.rb`
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ group :test do
9
9
  gem 'tryouts', path: '../tryouts'
10
10
  gem 'uri-valkey', path: '..//uri-valkey/gems', glob: 'uri-valkey.gemspec'
11
11
  else
12
- gem 'tryouts', '~> 3.2.2', require: false
12
+ gem 'tryouts', '~> 3.3.2', require: false
13
13
  end
14
14
  gem 'concurrent-ruby', '~> 1.3.5', require: false
15
15
  gem 'ruby-prof'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (2.0.0.pre5)
4
+ familia (2.0.0.pre6)
5
5
  benchmark
6
6
  connection_pool
7
7
  csv
@@ -113,7 +113,8 @@ GEM
113
113
  ruby-progressbar (1.13.0)
114
114
  stackprof (0.2.27)
115
115
  stringio (3.1.7)
116
- tryouts (3.2.2)
116
+ tryouts (3.3.2)
117
+ concurrent-ruby (~> 1.0)
117
118
  irb
118
119
  minitest (~> 5.0)
119
120
  pastel (~> 0.8)
@@ -147,7 +148,7 @@ DEPENDENCIES
147
148
  rubocop-thread_safety
148
149
  ruby-prof
149
150
  stackprof
150
- tryouts (~> 3.2.2)
151
+ tryouts (~> 3.3.2)
151
152
  yard (~> 0.9)
152
153
 
153
154
  BUNDLED WITH
@@ -55,9 +55,21 @@ vault.secret_data(passphrase_value: "user_passphrase")
55
55
 
56
56
  ## Familia::Encryption Module
57
57
 
58
+ ### manager
59
+
60
+ Creates a manager instance with optional algorithm selection.
61
+
62
+ ```ruby
63
+ # Use best available provider
64
+ mgr = Familia::Encryption.manager
65
+
66
+ # Use specific algorithm
67
+ mgr = Familia::Encryption.manager(algorithm: 'xchacha20poly1305')
68
+ ```
69
+
58
70
  ### encrypt
59
71
 
60
- Encrypts plaintext with context-specific key.
72
+ Encrypts plaintext using the default provider.
61
73
 
62
74
  ```ruby
63
75
  Familia::Encryption.encrypt(plaintext,
@@ -66,16 +78,20 @@ Familia::Encryption.encrypt(plaintext,
66
78
  )
67
79
  ```
68
80
 
69
- **Parameters:**
70
- - `plaintext` (String) - Data to encrypt
71
- - `context` (String) - Key derivation context
72
- - `additional_data` (String, nil) - Optional AAD for authentication
81
+ ### encrypt_with
82
+
83
+ Encrypts plaintext with a specific algorithm.
73
84
 
74
- **Returns:** JSON string with encrypted data structure
85
+ ```ruby
86
+ Familia::Encryption.encrypt_with('aes-256-gcm', plaintext,
87
+ context: "User:favorite_snack:user123",
88
+ additional_data: nil
89
+ )
90
+ ```
75
91
 
76
92
  ### decrypt
77
93
 
78
- Decrypts ciphertext with context-specific key.
94
+ Decrypts ciphertext (auto-detects algorithm from JSON).
79
95
 
80
96
  ```ruby
81
97
  Familia::Encryption.decrypt(encrypted_json,
@@ -84,12 +100,33 @@ Familia::Encryption.decrypt(encrypted_json,
84
100
  )
85
101
  ```
86
102
 
87
- **Parameters:**
88
- - `encrypted_json` (String) - JSON-encoded encrypted data
89
- - `context` (String) - Key derivation context
90
- - `additional_data` (String, nil) - Optional AAD for verification
103
+ ### status
91
104
 
92
- **Returns:** Decrypted plaintext string
105
+ Returns current encryption configuration and available providers.
106
+
107
+ ```ruby
108
+ Familia::Encryption.status
109
+ # => {
110
+ # default_algorithm: "xchacha20poly1305",
111
+ # available_algorithms: ["xchacha20poly1305", "aes-256-gcm"],
112
+ # preferred_available: "Familia::Encryption::Providers::XChaCha20Poly1305Provider",
113
+ # using_hardware: false,
114
+ # key_versions: [:v1],
115
+ # current_version: :v1
116
+ # }
117
+ ```
118
+
119
+ ### benchmark
120
+
121
+ Benchmarks available providers.
122
+
123
+ ```ruby
124
+ Familia::Encryption.benchmark(iterations: 1000)
125
+ # => {
126
+ # "xchacha20poly1305" => { time: 0.45, ops_per_sec: 4444, priority: 100 },
127
+ # "aes-256-gcm" => { time: 0.52, ops_per_sec: 3846, priority: 50 }
128
+ # }
129
+ ```
93
130
 
94
131
  ### validate_configuration!
95
132
 
@@ -100,17 +137,57 @@ Familia::Encryption.validate_configuration!
100
137
  # Raises Familia::EncryptionError if configuration invalid
101
138
  ```
102
139
 
103
- ### with_key_cache
140
+ ### derivation_count / reset_derivation_count!
104
141
 
105
- Provides request-scoped key caching.
142
+ Monitors key derivation operations (for testing and debugging).
106
143
 
107
144
  ```ruby
108
- Familia::Encryption.with_key_cache do
109
- # Operations here share derived keys
110
- users.each { |u| u.decrypt_fields }
111
- end
145
+ # Check how many key derivations have occurred
146
+ count = Familia::Encryption.derivation_count.value
147
+ # => 42
148
+
149
+ # Reset counter
150
+ Familia::Encryption.reset_derivation_count!
112
151
  ```
113
152
 
153
+ ## Familia::Encryption::Manager
154
+
155
+ Low-level manager class for direct provider control.
156
+
157
+ ### initialize
158
+
159
+ ```ruby
160
+ # Use default provider
161
+ manager = Familia::Encryption::Manager.new
162
+
163
+ # Use specific algorithm
164
+ manager = Familia::Encryption::Manager.new(algorithm: 'aes-256-gcm')
165
+ ```
166
+
167
+ ### encrypt / decrypt
168
+
169
+ Same interface as module-level methods but tied to specific provider.
170
+
171
+ ## Familia::Encryption::Registry
172
+
173
+ Provider management system.
174
+
175
+ ### setup!
176
+
177
+ Registers all available providers.
178
+
179
+ ### get
180
+
181
+ Returns provider instance by algorithm name.
182
+
183
+ ### default_provider
184
+
185
+ Returns highest-priority available provider instance.
186
+
187
+ ### available_algorithms
188
+
189
+ Returns array of available algorithm names.
190
+
114
191
  ## Configuration
115
192
 
116
193
  ### Familia.configure
@@ -0,0 +1,437 @@
1
+ # Connection Pooling Guide
2
+
3
+ ## Overview
4
+
5
+ Familia provides robust connection pooling through a provider pattern that enables efficient Redis/Valkey connection management with support for multiple logical databases, thread safety, and optimal performance.
6
+
7
+ ## Core Concepts
8
+
9
+ ### Connection Provider Contract
10
+
11
+ Your connection provider **MUST** follow these rules:
12
+
13
+ 1. **Database Selection**: Return connections already on the correct logical database
14
+ 2. **No SELECT Commands**: Familia will NOT issue `SELECT` commands
15
+ 3. **URI-based Selection**: Accept normalized URIs (e.g., `redis://localhost:6379/2`)
16
+ 4. **Thread Safety**: Handle concurrent access safely
17
+
18
+ ### Connection Priority System
19
+
20
+ Familia uses a three-tier connection resolution system:
21
+
22
+ 1. **Thread-local connections** (middleware pattern)
23
+ 2. **Connection provider** (if configured)
24
+ 3. **Fallback behavior** (legacy direct connections, if allowed)
25
+
26
+ ```ruby
27
+ # Priority 1: Thread-local (set by middleware)
28
+ Thread.current[:familia_connection] = redis_client
29
+
30
+ # Priority 2: Connection provider
31
+ Familia.connection_provider = ->(uri) { pool.checkout(uri) }
32
+
33
+ # Priority 3: Fallback (can be disabled)
34
+ Familia.connection_required = true # Disable fallback
35
+ ```
36
+
37
+ ## Basic Setup
38
+
39
+ ### Simple Connection Pool
40
+
41
+ ```ruby
42
+ require 'connection_pool'
43
+
44
+ class ConnectionManager
45
+ @pools = {}
46
+
47
+ # Configure provider at application startup
48
+ def self.setup!
49
+ Familia.connection_provider = lambda do |uri|
50
+ parsed = URI.parse(uri)
51
+ pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
52
+
53
+ @pools[pool_key] ||= ConnectionPool.new(size: 10, timeout: 5) do
54
+ Redis.new(
55
+ host: parsed.host,
56
+ port: parsed.port,
57
+ db: parsed.db || 0 # CRITICAL: Set DB on connection creation
58
+ )
59
+ end
60
+
61
+ @pools[pool_key].with { |conn| conn }
62
+ end
63
+ end
64
+ end
65
+
66
+ # Initialize at app startup
67
+ ConnectionManager.setup!
68
+ ```
69
+
70
+ ### Multi-Database Configuration
71
+
72
+ ```ruby
73
+ class DatabasePoolManager
74
+ POOL_CONFIGS = {
75
+ 0 => { size: 20, timeout: 5 }, # Main application data
76
+ 1 => { size: 5, timeout: 3 }, # Analytics/reporting
77
+ 2 => { size: 10, timeout: 2 }, # Session/cache data
78
+ 3 => { size: 15, timeout: 5 } # Background jobs
79
+ }.freeze
80
+
81
+ @pools = {}
82
+
83
+ def self.setup!
84
+ Familia.connection_provider = lambda do |uri|
85
+ parsed = URI.parse(uri)
86
+ db = parsed.db || 0
87
+ server = "#{parsed.host}:#{parsed.port}"
88
+ pool_key = "#{server}/#{db}"
89
+
90
+ @pools[pool_key] ||= begin
91
+ config = POOL_CONFIGS[db] || { size: 5, timeout: 5 }
92
+
93
+ ConnectionPool.new(**config) do
94
+ Redis.new(
95
+ host: parsed.host,
96
+ port: parsed.port,
97
+ db: db,
98
+ timeout: 1,
99
+ reconnect_attempts: 3,
100
+ inherit_socket: false
101
+ )
102
+ end
103
+ end
104
+
105
+ @pools[pool_key].with { |conn| conn }
106
+ end
107
+ end
108
+ end
109
+ ```
110
+
111
+ ## Advanced Patterns
112
+
113
+ ### Rails/Sidekiq Integration
114
+
115
+ ```ruby
116
+ # config/initializers/familia_pools.rb
117
+ class FamiliaPoolManager
118
+ include Singleton
119
+
120
+ def initialize
121
+ @pools = {}
122
+ setup_connection_provider
123
+ end
124
+
125
+ private
126
+
127
+ def setup_connection_provider
128
+ Familia.connection_provider = lambda do |uri|
129
+ get_connection(uri)
130
+ end
131
+ end
132
+
133
+ def get_connection(uri)
134
+ parsed = URI.parse(uri)
135
+ pool_key = connection_key(parsed)
136
+
137
+ @pools[pool_key] ||= create_pool(parsed)
138
+ @pools[pool_key].with { |conn| conn }
139
+ end
140
+
141
+ def connection_key(parsed_uri)
142
+ "#{parsed_uri.host}:#{parsed_uri.port}/#{parsed_uri.db || 0}"
143
+ end
144
+
145
+ def create_pool(parsed_uri)
146
+ db = parsed_uri.db || 0
147
+
148
+ ConnectionPool.new(
149
+ size: pool_size_for_database(db),
150
+ timeout: 5
151
+ ) do
152
+ Redis.new(
153
+ host: parsed_uri.host,
154
+ port: parsed_uri.port,
155
+ db: db,
156
+ timeout: redis_timeout,
157
+ reconnect_attempts: 3
158
+ )
159
+ end
160
+ end
161
+
162
+ def pool_size_for_database(db)
163
+ case db
164
+ when 0 then sidekiq_concurrency + web_concurrency + 2 # Main DB
165
+ when 1 then 5 # Analytics
166
+ when 2 then web_concurrency + 2 # Sessions
167
+ else 5 # Default
168
+ end
169
+ end
170
+
171
+ def sidekiq_concurrency
172
+ defined?(Sidekiq) ? Sidekiq.options[:concurrency] : 0
173
+ end
174
+
175
+ def web_concurrency
176
+ ENV.fetch('WEB_CONCURRENCY', 5).to_i
177
+ end
178
+
179
+ def redis_timeout
180
+ Rails.env.production? ? 1 : 5
181
+ end
182
+ end
183
+
184
+ # Initialize the pool manager
185
+ FamiliaPoolManager.instance
186
+ ```
187
+
188
+ ### Request-Scoped Connections (Middleware)
189
+
190
+ ```ruby
191
+ # Middleware for per-request connection management
192
+ class FamiliaConnectionMiddleware
193
+ def initialize(app)
194
+ @app = app
195
+ end
196
+
197
+ def call(env)
198
+ # Provide a single connection for the entire request
199
+ ConnectionPool.with do |conn|
200
+ Thread.current[:familia_connection] = conn
201
+ @app.call(env)
202
+ end
203
+ ensure
204
+ Thread.current[:familia_connection] = nil
205
+ end
206
+ end
207
+
208
+ # In your Rack/Rails app
209
+ use FamiliaConnectionMiddleware
210
+ ```
211
+
212
+ ## Model Database Configuration
213
+
214
+ Configure models to use specific logical databases:
215
+
216
+ ```ruby
217
+ class Customer < Familia::Horreum
218
+ self.logical_database = 0 # Primary application data
219
+ field :name, :email, :status
220
+ end
221
+
222
+ class AnalyticsEvent < Familia::Horreum
223
+ self.logical_database = 1 # Separate analytics database
224
+ field :event_type, :user_id, :timestamp, :properties
225
+ end
226
+
227
+ class SessionData < Familia::Horreum
228
+ self.logical_database = 2 # Fast cache database
229
+ feature :expiration
230
+ default_expiration 1.hour
231
+ field :user_id, :data, :csrf_token
232
+ end
233
+
234
+ class BackgroundJob < Familia::Horreum
235
+ self.logical_database = 3 # Job queue database
236
+ field :job_type, :payload, :status, :retry_count
237
+ end
238
+ ```
239
+
240
+ ## Performance Benefits
241
+
242
+ ### Without Connection Pooling
243
+
244
+ Each operation may trigger database switches:
245
+
246
+ ```ruby
247
+ # These operations might use different connections, causing SELECT commands:
248
+ customer = Customer.find(123) # SELECT 0, then query
249
+ session = SessionData.find(456) # SELECT 2, then query
250
+ analytics = AnalyticsEvent.find(789) # SELECT 1, then query
251
+ ```
252
+
253
+ ### With Connection Pooling
254
+
255
+ Connections stay on the correct database:
256
+
257
+ ```ruby
258
+ # Each model uses its dedicated connection pool:
259
+ customer = Customer.find(123) # Connection already on DB 0
260
+ session = SessionData.find(456) # Different connection, already on DB 2
261
+ analytics = AnalyticsEvent.find(789) # Different connection, already on DB 1
262
+ ```
263
+
264
+ ## Pool Sizing Guidelines
265
+
266
+ ### Web Applications
267
+ - **Formula**: `(threads_per_process * processes) + buffer`
268
+ - **Puma**: `(threads * workers) + 2`
269
+ - **Unicorn**: `processes + 2`
270
+
271
+ ### Background Jobs
272
+ - **Sidekiq**: `concurrency + 2`
273
+ - **DelayedJob**: `worker_processes + 2`
274
+
275
+ ### Database-Specific Sizing
276
+ ```ruby
277
+ def pool_size_for_database(db)
278
+ base_size = web_concurrency + sidekiq_concurrency
279
+
280
+ case db
281
+ when 0 then base_size + 5 # Main DB: highest usage
282
+ when 1 then 3 # Analytics: batch operations
283
+ when 2 then base_size + 2 # Sessions: per-request access
284
+ when 3 then sidekiq_concurrency # Jobs: worker access only
285
+ else 5 # Default for new DBs
286
+ end
287
+ end
288
+ ```
289
+
290
+ ## Monitoring and Debugging
291
+
292
+ ### Enable Debug Mode
293
+
294
+ ```ruby
295
+ Familia.debug = true
296
+ # Shows database selection and connection provider usage
297
+ ```
298
+
299
+ ### Pool Usage Monitoring
300
+
301
+ ```ruby
302
+ class PoolMonitor
303
+ def self.stats
304
+ FamiliaPoolManager.instance.instance_variable_get(:@pools).map do |key, pool|
305
+ {
306
+ database: key,
307
+ size: pool.size,
308
+ available: pool.available,
309
+ checked_out: pool.size - pool.available
310
+ }
311
+ end
312
+ end
313
+
314
+ def self.health_check
315
+ stats.each do |stat|
316
+ utilization = (stat[:checked_out] / stat[:size].to_f) * 100
317
+ puts "DB #{stat[:database]}: #{utilization.round(1)}% utilized"
318
+ warn "High utilization!" if utilization > 80
319
+ end
320
+ end
321
+ end
322
+ ```
323
+
324
+ ### Connection Testing
325
+
326
+ ```ruby
327
+ # Test concurrent access patterns
328
+ def test_concurrent_access
329
+ threads = 20.times.map do |i|
330
+ Thread.new do
331
+ 50.times do |j|
332
+ Customer.create(name: "test-#{i}-#{j}")
333
+ SessionData.create(user_id: i, data: "session-#{j}")
334
+ end
335
+ end
336
+ end
337
+
338
+ threads.each(&:join)
339
+ puts "Concurrent test completed"
340
+ end
341
+ ```
342
+
343
+ ## Troubleshooting
344
+
345
+ ### Common Issues
346
+
347
+ **1. Wrong Database Connections**
348
+ ```ruby
349
+ # Problem: Provider not setting DB correctly
350
+ Redis.new(host: 'localhost', port: 6379) # Missing db: parameter
351
+
352
+ # Solution: Always specify database
353
+ Redis.new(host: 'localhost', port: 6379, db: parsed_uri.db || 0)
354
+ ```
355
+
356
+ **2. Pool Exhaustion**
357
+ ```ruby
358
+ # Monitor pool usage
359
+ ConnectionPool.stats # If available
360
+ # Increase pool size or reduce hold time
361
+ ```
362
+
363
+ **3. Connection Leaks**
364
+ ```ruby
365
+ # Always use .with for pool connections
366
+ pool.with do |conn|
367
+ # Use connection
368
+ end
369
+
370
+ # Never checkout without returning
371
+ conn = pool.checkout # ❌ Can leak
372
+ ```
373
+
374
+ ### Error Handling
375
+
376
+ ```ruby
377
+ Familia.connection_provider = lambda do |uri|
378
+ begin
379
+ get_pooled_connection(uri)
380
+ rescue Redis::ConnectionError => e
381
+ # Log error, potentially retry or fall back
382
+ Familia.logger.error "Connection failed: #{e.message}"
383
+ raise Familia::ConnectionError, "Pool connection failed"
384
+ end
385
+ end
386
+ ```
387
+
388
+ ## Best Practices
389
+
390
+ 1. **Return Pre-Selected Connections**: Provider must return connections on the correct DB
391
+ 2. **One Pool Per Database**: Each logical DB needs its own pool
392
+ 3. **Thread Safety**: Use thread-safe pool creation and access
393
+ 4. **Monitor Usage**: Track pool utilization and adjust sizes
394
+ 5. **Proper Sizing**: Account for all concurrent access patterns
395
+ 6. **Error Handling**: Gracefully handle connection failures
396
+ 7. **Connection Validation**: Verify connections are healthy before use
397
+
398
+ ## Integration Examples
399
+
400
+ ### Roda Application
401
+
402
+ ```ruby
403
+ class App < Roda
404
+ plugin :hooks
405
+
406
+ before do
407
+ # Connection pooling handled automatically via provider
408
+ # Each request gets appropriate connections per database
409
+ end
410
+
411
+ route do |r|
412
+ r.get 'customers', Integer do |id|
413
+ customer = Customer.find(id) # Uses DB 0 pool
414
+ session = SessionData.find(r.env) # Uses DB 2 pool
415
+
416
+ render_json(customer: customer, session: session)
417
+ end
418
+ end
419
+ end
420
+ ```
421
+
422
+ ### Background Jobs
423
+
424
+ ```ruby
425
+ class ProcessCustomerJob
426
+ include Sidekiq::Worker
427
+
428
+ def perform(customer_id)
429
+ # Each of these uses appropriate database pool
430
+ customer = Customer.find(customer_id) # DB 0
431
+ SessionData.expire_for_user(customer_id) # DB 2
432
+ AnalyticsEvent.track('job.completed', user: customer_id) # DB 1
433
+ end
434
+ end
435
+ ```
436
+
437
+ This connection pooling system provides the foundation for scalable, performant Familia applications with proper resource management across multiple logical databases.