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.
- checksums.yaml +4 -4
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +600 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +72 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/expiration.rb +1 -1
- data/lib/familia/features/quantization.rb +1 -1
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +1 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +44 -28
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/encryption/encryption_core_try.rb +3 -3
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +29 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +25 -0
- data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +1 -1
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- metadata +51 -10
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/horreum/serialization.rb +0 -473
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93cb8ea627c19daff3566c7373aa45ff04adb6d37a3346c3b3049712c18838ea
|
4
|
+
data.tar.gz: 9d359e2067062a4c6afe91a76985362989a569611400f10f7155e505dd55e39b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
10
|
-
1)
|
11
|
-
2)
|
12
|
-
3)
|
13
|
-
4)
|
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.
|
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.
|
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.
|
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.
|
151
|
+
tryouts (~> 3.3.2)
|
151
152
|
yard (~> 0.9)
|
152
153
|
|
153
154
|
BUNDLED WITH
|
data/docs/wiki/API-Reference.md
CHANGED
@@ -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
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
- `additional_data` (String, nil) - Optional AAD for authentication
|
81
|
+
### encrypt_with
|
82
|
+
|
83
|
+
Encrypts plaintext with a specific algorithm.
|
73
84
|
|
74
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
###
|
140
|
+
### derivation_count / reset_derivation_count!
|
104
141
|
|
105
|
-
|
142
|
+
Monitors key derivation operations (for testing and debugging).
|
106
143
|
|
107
144
|
```ruby
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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.
|