familia 2.0.0.pre.pre → 2.0.0.pre3
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 +12 -5
- data/Gemfile +4 -3
- data/Gemfile.lock +24 -11
- data/bin/irb +1 -1
- data/docs/connection_pooling.md +98 -223
- data/familia.gemspec +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/core_ext.rb +2 -2
- data/lib/familia/features/expiration.rb +0 -1
- data/lib/familia/features/relatable_objects.rb +127 -0
- data/lib/familia/features.rb +7 -3
- data/lib/familia/horreum/class_methods.rb +18 -4
- data/lib/familia/secure_identifier.rb +129 -0
- data/lib/familia/utils.rb +7 -96
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +3 -1
- data/try/configuration/scenarios_try.rb +43 -31
- data/try/core/connection_try.rb +1 -1
- data/try/core/errors_try.rb +10 -10
- data/try/core/extensions_try.rb +56 -23
- data/try/core/familia_extended_try.rb +3 -3
- data/try/core/familia_try.rb +2 -6
- data/try/core/middleware_try.rb +34 -40
- data/try/{pooling/connection_pool_test_try.rb → core/pools_try.rb} +2 -2
- data/try/core/secure_identifier_try.rb +104 -0
- data/try/core/tools_try.rb +52 -36
- data/try/core/utils_try.rb +0 -98
- data/try/datatypes/boolean_try.rb +6 -7
- data/try/datatypes/datatype_base_try.rb +2 -2
- data/try/datatypes/hash_try.rb +0 -1
- data/try/datatypes/list_try.rb +0 -1
- data/try/datatypes/set_try.rb +0 -2
- data/try/datatypes/sorted_set_try.rb +1 -2
- data/try/datatypes/string_try.rb +1 -2
- data/try/edge_cases/empty_identifiers_try.rb +42 -35
- data/try/edge_cases/hash_symbolization_try.rb +5 -5
- data/try/edge_cases/json_serialization_try.rb +12 -13
- data/try/edge_cases/race_conditions_try.rb +46 -49
- data/try/edge_cases/reserved_keywords_try.rb +103 -49
- data/try/edge_cases/string_coercion_try.rb +2 -2
- data/try/edge_cases/ttl_side_effects_try.rb +44 -25
- data/try/features/expiration_try.rb +2 -2
- data/try/features/quantization_try.rb +2 -2
- data/try/features/relatable_objects_try.rb +221 -0
- data/try/features/safe_dump_advanced_try.rb +13 -14
- data/try/features/safe_dump_try.rb +8 -8
- data/try/helpers/test_helpers.rb +10 -12
- data/try/horreum/base_try.rb +9 -9
- data/try/horreum/class_methods_try.rb +34 -28
- data/try/horreum/commands_try.rb +69 -33
- data/try/horreum/initialization_try.rb +4 -4
- data/try/horreum/relations_try.rb +13 -14
- data/try/horreum/serialization_try.rb +3 -3
- data/try/horreum/settings_try.rb +25 -31
- data/try/integration/cross_component_try.rb +45 -35
- data/try/models/customer_safe_dump_try.rb +4 -4
- data/try/models/customer_try.rb +22 -25
- data/try/models/datatype_base_try.rb +2 -4
- data/try/models/familia_object_try.rb +3 -4
- data/try/performance/benchmarks_try.rb +47 -38
- data/try/prototypes/atomic_saves_v4.rb +3 -3
- metadata +18 -15
- data/try/core/refinements_try.rb +0 -39
- /data/try/{pooling → prototypes/pooling}/README.md +0 -0
- /data/try/{pooling/configurable_stress_test_try.rb → prototypes/pooling/configurable_stress_test.rb} +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/connection_pool_metrics.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{pooling → prototypes/pooling}/lib/visualize_stress_results.rb +0 -0
- /data/try/{pooling/pool_siege_try.rb → prototypes/pooling/pool_siege.rb} +0 -0
- /data/try/{pooling/run_stress_tests_try.rb → prototypes/pooling/run_stress_tests.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5d32d5a95de13ddda788d37390fb97e7a73c5ff1b80515bf55f2db16d18a211
|
4
|
+
data.tar.gz: b5a97154c4f4461de329d5190b239b0db135304ce2b36e59d226f8b37ade5614
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e51b2929601c71ed5fc33465fc39732fdae070c44427af3352de73e4f5b1ae14af6fc075a1179b5c0c197c6fe0e699c524e251ce22a33049f353f9d385495b09
|
7
|
+
data.tar.gz: 3be9e23886a0c327dbbe529a683675ca2a3dc0a3c3774a1eab447f49f7949b4b9c5a82248b6fea1a0ad546eb261872e6a54f3ef6c2ebcf6df51277c0b720aeb5
|
data/CLAUDE.md
CHANGED
@@ -5,10 +5,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
5
5
|
## Development Commands
|
6
6
|
|
7
7
|
### Testing
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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.
|
14
|
+
|
15
|
+
- **Run tests**: `bundle exec try` (uses tryouts testing framework)
|
16
|
+
- **Run specific test file, verbose**: `bundle exec try -v try/specific_test_try.rb`
|
17
|
+
- **Debug mode**: `FAMILIA_DEBUG=1 bundle exec try -D`
|
18
|
+
- **Trace mode**: `FAMILIA_TRACE=1 bundle exec try -D` (detailed Redis operation logging)
|
12
19
|
|
13
20
|
### Development Setup
|
14
21
|
- **Install dependencies**: `bundle install`
|
@@ -74,7 +81,7 @@ end
|
|
74
81
|
```
|
75
82
|
|
76
83
|
**Identifier Resolution**: Multiple strategies for object identification:
|
77
|
-
- Symbol: `
|
84
|
+
- Symbol: `identifier_field :email`
|
78
85
|
- Proc: `identifier ->(user) { "user:#{user.email}" }`
|
79
86
|
- Array: `identifier [:type, :email]`
|
80
87
|
|
data/Gemfile
CHANGED
@@ -6,16 +6,16 @@ gemspec
|
|
6
6
|
|
7
7
|
group :test do
|
8
8
|
if ENV['LOCAL_DEV']
|
9
|
-
gem 'tryouts', path: '
|
9
|
+
gem 'tryouts', path: '../tryouts'
|
10
|
+
gem 'uri-valkey', path: '..//uri-valkey/gems', glob: 'uri-valkey.gemspec'
|
10
11
|
else
|
11
|
-
gem 'tryouts', '~> 3.1.
|
12
|
+
gem 'tryouts', '~> 3.1.2', require: false
|
12
13
|
end
|
13
14
|
gem 'concurrent-ruby', '~> 1.3.5', require: false
|
14
15
|
gem 'ruby-prof'
|
15
16
|
gem 'stackprof'
|
16
17
|
end
|
17
18
|
|
18
|
-
|
19
19
|
group :development, :test do
|
20
20
|
# byebug only works with MRI
|
21
21
|
gem 'byebug', '~> 11.0', require: false if RUBY_ENGINE == 'ruby'
|
@@ -25,4 +25,5 @@ group :development, :test do
|
|
25
25
|
gem 'rubocop-performance', require: false
|
26
26
|
gem 'rubocop-thread_safety', require: false
|
27
27
|
gem 'yard', '~> 0.9', require: false
|
28
|
+
gem 'irb', '~> 1.15.2', require: false
|
28
29
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
familia (2.0.0.
|
4
|
+
familia (2.0.0.pre2)
|
5
5
|
benchmark
|
6
6
|
connection_pool
|
7
7
|
csv
|
8
8
|
logger
|
9
9
|
redis (>= 4.8.1, < 6.0)
|
10
10
|
stringio (~> 3.1.1)
|
11
|
-
uri-
|
11
|
+
uri-valkey (~> 1.4)
|
12
12
|
|
13
13
|
GEM
|
14
14
|
remote: https://rubygems.org/
|
@@ -21,8 +21,14 @@ GEM
|
|
21
21
|
concurrent-ruby (1.3.5)
|
22
22
|
connection_pool (2.5.3)
|
23
23
|
csv (3.3.5)
|
24
|
+
date (3.4.1)
|
24
25
|
diff-lcs (1.6.2)
|
25
|
-
|
26
|
+
erb (5.0.2)
|
27
|
+
io-console (0.8.1)
|
28
|
+
irb (1.15.2)
|
29
|
+
pp (>= 0.6.0)
|
30
|
+
rdoc (>= 4.0.0)
|
31
|
+
reline (>= 0.4.2)
|
26
32
|
json (2.13.0)
|
27
33
|
kramdown (2.5.1)
|
28
34
|
rexml (>= 3.3.9)
|
@@ -35,6 +41,9 @@ GEM
|
|
35
41
|
parser (3.3.8.0)
|
36
42
|
ast (~> 2.4.1)
|
37
43
|
racc
|
44
|
+
pp (0.6.2)
|
45
|
+
prettyprint
|
46
|
+
prettyprint (0.2.0)
|
38
47
|
prism (1.4.0)
|
39
48
|
pry (0.14.2)
|
40
49
|
coderay (~> 1.1)
|
@@ -42,13 +51,21 @@ GEM
|
|
42
51
|
pry-byebug (3.10.1)
|
43
52
|
byebug (~> 11.0)
|
44
53
|
pry (>= 0.13, < 0.15)
|
54
|
+
psych (5.2.6)
|
55
|
+
date
|
56
|
+
stringio
|
45
57
|
racc (1.8.1)
|
46
58
|
rainbow (3.1.1)
|
59
|
+
rdoc (6.14.2)
|
60
|
+
erb
|
61
|
+
psych (>= 4.0.0)
|
47
62
|
redis (5.4.1)
|
48
63
|
redis-client (>= 0.22.0)
|
49
64
|
redis-client (0.25.1)
|
50
65
|
connection_pool
|
51
66
|
regexp_parser (2.10.0)
|
67
|
+
reline (0.6.2)
|
68
|
+
io-console (~> 0.5)
|
52
69
|
rexml (3.4.1)
|
53
70
|
rspec (3.13.1)
|
54
71
|
rspec-core (~> 3.13.0)
|
@@ -89,19 +106,14 @@ GEM
|
|
89
106
|
base64
|
90
107
|
ruby-progressbar (1.13.0)
|
91
108
|
stackprof (0.2.27)
|
92
|
-
storable (0.10.0)
|
93
109
|
stringio (3.1.7)
|
94
|
-
|
95
|
-
drydock (< 1.0)
|
96
|
-
storable (~> 0.10)
|
97
|
-
tryouts (3.1.1)
|
110
|
+
tryouts (3.1.2)
|
98
111
|
minitest (~> 5.0)
|
99
112
|
rspec (~> 3.0)
|
100
|
-
sysinfo (>= 0.8, < 1.0)
|
101
113
|
unicode-display_width (3.1.4)
|
102
114
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
103
115
|
unicode-emoji (4.0.4)
|
104
|
-
uri-
|
116
|
+
uri-valkey (1.4.0)
|
105
117
|
yard (0.9.37)
|
106
118
|
|
107
119
|
PLATFORMS
|
@@ -112,6 +124,7 @@ DEPENDENCIES
|
|
112
124
|
byebug (~> 11.0)
|
113
125
|
concurrent-ruby (~> 1.3.5)
|
114
126
|
familia!
|
127
|
+
irb (~> 1.15.2)
|
115
128
|
kramdown
|
116
129
|
pry-byebug (~> 3.10.1)
|
117
130
|
rubocop
|
@@ -119,7 +132,7 @@ DEPENDENCIES
|
|
119
132
|
rubocop-thread_safety
|
120
133
|
ruby-prof
|
121
134
|
stackprof
|
122
|
-
tryouts (~> 3.1.
|
135
|
+
tryouts (~> 3.1.2)
|
123
136
|
yard (~> 0.9)
|
124
137
|
|
125
138
|
BUNDLED WITH
|
data/bin/irb
CHANGED
data/docs/connection_pooling.md
CHANGED
@@ -1,317 +1,192 @@
|
|
1
1
|
# Connection Pooling with Familia
|
2
2
|
|
3
|
-
Familia uses a
|
3
|
+
Familia uses a connection provider pattern for efficient connection pooling. This guide shows how to configure pools for optimal performance with multiple logical databases.
|
4
4
|
|
5
5
|
## Key Concepts
|
6
6
|
|
7
|
-
|
7
|
+
- **Connection Provider Contract**: Your provider MUST return connections already on the correct logical database. Familia will NOT issue SELECT commands.
|
8
|
+
- **URI-based Selection**: Familia passes normalized URIs (e.g., `redis://localhost:6379/2`) encoding the logical database.
|
9
|
+
- **One Pool Per Database**: Each unique logical database requires its own connection pool.
|
8
10
|
|
9
|
-
|
11
|
+
## Basic Setup
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
## Basic Connection Pool Setup
|
14
|
-
|
15
|
-
### Example 1: Simple Connection Pool
|
13
|
+
### Simple Connection Pool
|
16
14
|
|
17
15
|
```ruby
|
18
16
|
require 'connection_pool'
|
19
|
-
require 'familia'
|
20
17
|
|
21
18
|
class MyApp
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
db: parsed.db || 0 # Connection created with correct DB
|
35
|
-
)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Return a connection from the pool
|
39
|
-
@pools[pool_key].with { |conn| conn }
|
19
|
+
@pools = {}
|
20
|
+
|
21
|
+
Familia.connection_provider = lambda do |uri|
|
22
|
+
parsed = URI.parse(uri)
|
23
|
+
pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
|
24
|
+
|
25
|
+
@pools[pool_key] ||= ConnectionPool.new(size: 10, timeout: 5) do
|
26
|
+
Redis.new(
|
27
|
+
host: parsed.host,
|
28
|
+
port: parsed.port,
|
29
|
+
db: parsed.db || 0
|
30
|
+
)
|
40
31
|
end
|
32
|
+
|
33
|
+
@pools[pool_key].with { |conn| conn }
|
41
34
|
end
|
42
35
|
end
|
43
36
|
```
|
44
37
|
|
45
|
-
###
|
38
|
+
### Multi-Database Configuration
|
46
39
|
|
47
40
|
```ruby
|
48
41
|
class MyApp
|
49
|
-
# Different pool sizes based on expected traffic
|
50
42
|
POOL_CONFIGS = {
|
51
|
-
0 => { size: 20
|
52
|
-
1 => { size: 5
|
53
|
-
2 => { size: 10
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
ConnectionPool.new(**config) do
|
68
|
-
Redis.new(host: parsed.host, port: parsed.port, db: db)
|
69
|
-
end
|
43
|
+
0 => { size: 20 }, # Main database
|
44
|
+
1 => { size: 5 }, # Analytics
|
45
|
+
2 => { size: 10 } # Cache
|
46
|
+
}.freeze
|
47
|
+
|
48
|
+
@pools = {}
|
49
|
+
|
50
|
+
Familia.connection_provider = lambda do |uri|
|
51
|
+
parsed = URI.parse(uri)
|
52
|
+
db = parsed.db || 0
|
53
|
+
pool_key = "#{parsed.host}:#{parsed.port}/#{db}"
|
54
|
+
|
55
|
+
@pools[pool_key] ||= begin
|
56
|
+
config = POOL_CONFIGS[db] || { size: 5 }
|
57
|
+
ConnectionPool.new(timeout: 5, **config) do
|
58
|
+
Redis.new(host: parsed.host, port: parsed.port, db: db)
|
70
59
|
end
|
71
|
-
|
72
|
-
@pools[pool_key].with { |conn| conn }
|
73
60
|
end
|
61
|
+
|
62
|
+
@pools[pool_key].with { |conn| conn }
|
74
63
|
end
|
75
64
|
end
|
76
65
|
```
|
77
66
|
|
78
|
-
###
|
67
|
+
### Production Setup with Roda
|
79
68
|
|
80
69
|
```ruby
|
81
|
-
#
|
70
|
+
# config/familia.rb
|
71
|
+
class FamiliaPoolManager
|
72
|
+
include Singleton
|
82
73
|
|
83
|
-
|
84
|
-
|
85
|
-
|
74
|
+
def initialize
|
75
|
+
@pools = {}
|
76
|
+
end
|
86
77
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
$redis_pools[pool_key] ||= ConnectionPool.new(
|
94
|
-
size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i,
|
78
|
+
def get_connection(uri)
|
79
|
+
parsed = URI.parse(uri)
|
80
|
+
pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
|
81
|
+
|
82
|
+
@pools[pool_key] ||= ConnectionPool.new(
|
83
|
+
size: pool_size_for_environment,
|
95
84
|
timeout: 5
|
96
85
|
) do
|
97
86
|
Redis.new(
|
98
87
|
host: parsed.host,
|
99
88
|
port: parsed.port,
|
100
89
|
db: parsed.db || 0,
|
101
|
-
timeout: 1,
|
90
|
+
timeout: 1,
|
102
91
|
reconnect_attempts: 3
|
103
92
|
)
|
104
93
|
end
|
105
|
-
end
|
106
|
-
|
107
|
-
$redis_pools[pool_key].with { |conn| conn }
|
108
|
-
end
|
109
|
-
```
|
110
|
-
|
111
|
-
### Example 4: Using with Sidekiq
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
# Sidekiq has its own connection pool management
|
115
|
-
# This example shows how to share pools between Sidekiq and web processes
|
116
94
|
|
117
|
-
|
118
|
-
include Singleton
|
119
|
-
|
120
|
-
def initialize
|
121
|
-
@pools = {}
|
122
|
-
@mutex = Mutex.new
|
95
|
+
@pools[pool_key].with { |conn| conn }
|
123
96
|
end
|
124
|
-
|
125
|
-
def get_connection(uri)
|
126
|
-
parsed = URI.parse(uri)
|
127
|
-
pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
|
128
|
-
|
129
|
-
pool = @mutex.synchronize do
|
130
|
-
@pools[pool_key] ||= ConnectionPool.new(
|
131
|
-
size: determine_pool_size,
|
132
|
-
timeout: 5
|
133
|
-
) do
|
134
|
-
Redis.new(
|
135
|
-
host: parsed.host,
|
136
|
-
port: parsed.port,
|
137
|
-
db: parsed.db || 0
|
138
|
-
)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
pool.with { |conn| conn }
|
143
|
-
end
|
144
|
-
|
97
|
+
|
145
98
|
private
|
146
|
-
|
147
|
-
def
|
99
|
+
|
100
|
+
def pool_size_for_environment
|
148
101
|
if defined?(Sidekiq)
|
149
|
-
# Sidekiq workers need more connections
|
150
102
|
Sidekiq.options[:concurrency] + 2
|
151
103
|
else
|
152
|
-
|
153
|
-
ENV.fetch('RAILS_MAX_THREADS', 5).to_i
|
104
|
+
ENV.fetch('WEB_CONCURRENCY', 5).to_i + 2
|
154
105
|
end
|
155
106
|
end
|
156
107
|
end
|
157
108
|
|
158
|
-
# Configure
|
109
|
+
# Configure at application startup
|
159
110
|
Familia.connection_provider = lambda do |uri|
|
160
|
-
|
111
|
+
FamiliaPoolManager.instance.get_connection(uri)
|
112
|
+
end
|
113
|
+
|
114
|
+
# In your Roda app
|
115
|
+
class App < Roda
|
116
|
+
plugin :hooks
|
117
|
+
|
118
|
+
before do
|
119
|
+
# Familia pools are automatically used via connection_provider
|
120
|
+
end
|
161
121
|
end
|
162
122
|
```
|
163
123
|
|
164
124
|
## Model Configuration
|
165
125
|
|
166
|
-
|
126
|
+
Configure models to use different logical databases:
|
167
127
|
|
168
128
|
```ruby
|
169
|
-
# Models using different logical databases
|
170
129
|
class Customer < Familia::Horreum
|
171
130
|
self.logical_database = 0 # Main application data
|
172
|
-
field :name
|
173
|
-
field :email
|
131
|
+
field :name, :email
|
174
132
|
end
|
175
133
|
|
176
134
|
class Analytics < Familia::Horreum
|
177
|
-
self.logical_database = 1 # Analytics data
|
178
|
-
field :event_type
|
179
|
-
field :timestamp
|
135
|
+
self.logical_database = 1 # Analytics data
|
136
|
+
field :event_type, :timestamp
|
180
137
|
end
|
181
138
|
|
182
139
|
class Session < Familia::Horreum
|
183
140
|
self.logical_database = 2 # Session/cache data
|
184
141
|
feature :expiration
|
185
142
|
default_expiration 1.hour
|
186
|
-
field :user_id
|
187
|
-
field :data
|
188
|
-
end
|
189
|
-
|
190
|
-
# Models can share the same logical database
|
191
|
-
class Order < Familia::Horreum
|
192
|
-
self.logical_database = 0 # Shares DB with Customer
|
193
|
-
field :customer_id
|
194
|
-
field :total
|
143
|
+
field :user_id, :data
|
195
144
|
end
|
196
145
|
```
|
197
146
|
|
198
|
-
## Performance
|
199
|
-
|
200
|
-
### Avoiding SELECT Command Overhead
|
201
|
-
|
202
|
-
Without proper pooling configuration, each Redis operation might issue a SELECT command:
|
147
|
+
## Performance Benefits
|
203
148
|
|
149
|
+
Without connection pooling, each operation triggers database switches:
|
204
150
|
```
|
205
|
-
# Bad: Without connection provider
|
206
151
|
SET key value # Connection on DB 0
|
207
152
|
SELECT 2 # Switch to DB 2
|
208
153
|
SET key2 value2 # Now on DB 2
|
209
154
|
SELECT 0 # Switch back
|
210
|
-
GET key # Now on DB 0
|
211
155
|
```
|
212
156
|
|
213
|
-
With the
|
214
|
-
|
157
|
+
With proper pooling, connections stay on the correct database:
|
215
158
|
```
|
216
|
-
#
|
217
|
-
SET
|
218
|
-
SET key2 value2 # Different connection, already on correct DB
|
219
|
-
GET key # Original connection, still on correct DB
|
159
|
+
SET key value # Connection already on DB 0
|
160
|
+
SET key2 value2 # Different connection, already on DB 2
|
220
161
|
```
|
221
162
|
|
222
|
-
|
223
|
-
|
224
|
-
1. **Web Applications**: `pool_size = number_of_threads + buffer`
|
225
|
-
```ruby
|
226
|
-
size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i + 2
|
227
|
-
```
|
163
|
+
## Pool Sizing Guidelines
|
228
164
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
```
|
165
|
+
- **Web Applications**: `threads + 2`
|
166
|
+
- **Background Jobs**: `concurrency + 2`
|
167
|
+
- **High Traffic DBs**: Scale up based on usage patterns
|
233
168
|
|
234
|
-
|
235
|
-
```ruby
|
236
|
-
POOL_CONFIGS = {
|
237
|
-
0 => { size: 20 }, # High-traffic main DB
|
238
|
-
1 => { size: 5 }, # Low-traffic analytics
|
239
|
-
2 => { size: 15 } # Medium-traffic cache
|
240
|
-
}
|
241
|
-
```
|
242
|
-
|
243
|
-
## Testing Your Configuration
|
169
|
+
## Testing and Debugging
|
244
170
|
|
171
|
+
Enable debug mode to verify correct database selection:
|
245
172
|
```ruby
|
246
|
-
|
247
|
-
class ConnectionPoolTester
|
248
|
-
def self.test_pools
|
249
|
-
# Create test models using different databases
|
250
|
-
class TestModel0 < Familia::Horreum
|
251
|
-
self.logical_database = 0
|
252
|
-
field :value
|
253
|
-
end
|
254
|
-
|
255
|
-
class TestModel1 < Familia::Horreum
|
256
|
-
self.logical_database = 1
|
257
|
-
field :value
|
258
|
-
end
|
259
|
-
|
260
|
-
# Test concurrent access
|
261
|
-
threads = 10.times.map do |i|
|
262
|
-
Thread.new do
|
263
|
-
100.times do |j|
|
264
|
-
# This should use the pool for DB 0
|
265
|
-
TestModel0.create(value: "thread-#{i}-#{j}")
|
266
|
-
|
267
|
-
# This should use the pool for DB 1
|
268
|
-
TestModel1.create(value: "thread-#{i}-#{j}")
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
threads.each(&:join)
|
274
|
-
puts "Pool test completed successfully"
|
275
|
-
end
|
276
|
-
end
|
277
|
-
```
|
278
|
-
|
279
|
-
## Monitoring and Debugging
|
280
|
-
|
281
|
-
Enable debug mode to verify connection providers are returning connections on the correct database:
|
282
|
-
|
283
|
-
```ruby
|
284
|
-
Familia.debug = true # Logs warnings if provider returns wrong DB
|
173
|
+
Familia.debug = true
|
285
174
|
```
|
286
175
|
|
287
|
-
|
288
|
-
|
176
|
+
Test concurrent access:
|
289
177
|
```ruby
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
# Log pool statistics
|
295
|
-
Rails.logger.info "Pool stats: #{pool.size} size, #{pool.available} available"
|
296
|
-
|
297
|
-
pool.with { |conn| conn }
|
178
|
+
threads = 10.times.map do |i|
|
179
|
+
Thread.new do
|
180
|
+
100.times { |j| MyModel.create(value: "test-#{i}-#{j}") }
|
181
|
+
end
|
298
182
|
end
|
183
|
+
threads.each(&:join)
|
299
184
|
```
|
300
185
|
|
301
|
-
##
|
302
|
-
|
303
|
-
1. **Not returning DB-ready connections**: Your provider MUST return connections already on the correct database.
|
304
|
-
|
305
|
-
2. **Creating too many pools**: Ensure you're keying pools correctly to avoid creating duplicate pools for the same database.
|
306
|
-
|
307
|
-
3. **Forgetting thread safety**: Pool creation should be thread-safe in multi-threaded environments.
|
308
|
-
|
309
|
-
4. **Incorrect pool sizing**: Monitor your connection usage and adjust pool sizes accordingly.
|
310
|
-
|
311
|
-
## Summary
|
186
|
+
## Best Practices
|
312
187
|
|
313
|
-
-
|
314
|
-
-
|
315
|
-
-
|
316
|
-
-
|
317
|
-
-
|
188
|
+
- Return connections already on the correct database
|
189
|
+
- Use one pool per unique logical database
|
190
|
+
- Implement thread-safe pool creation
|
191
|
+
- Monitor pool usage and adjust sizes accordingly
|
192
|
+
- Use the `connection_pool` gem for production reliability
|
data/familia.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_dependency 'logger'
|
26
26
|
spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
|
27
27
|
spec.add_dependency 'stringio', '~> 3.1.1'
|
28
|
-
spec.add_dependency 'uri-
|
28
|
+
spec.add_dependency 'uri-valkey', '~> 1.4'
|
29
29
|
|
30
30
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
31
31
|
end
|
data/lib/familia/connection.rb
CHANGED
@@ -107,7 +107,7 @@ module Familia
|
|
107
107
|
# Provider MUST return connection already on the correct database
|
108
108
|
parsed_uri = normalize_uri(uri)
|
109
109
|
connection = connection_provider.call(parsed_uri.to_s)
|
110
|
-
|
110
|
+
|
111
111
|
# In debug mode, verify the provider honored the contract
|
112
112
|
if Familia.debug? && connection.respond_to?(:client)
|
113
113
|
current_db = connection.client.db
|
@@ -116,7 +116,7 @@ module Familia
|
|
116
116
|
Familia.warn "Connection provider returned connection on DB #{current_db}, expected #{expected_db}"
|
117
117
|
end
|
118
118
|
end
|
119
|
-
|
119
|
+
|
120
120
|
return connection
|
121
121
|
end
|
122
122
|
|
@@ -231,7 +231,7 @@ module Familia
|
|
231
231
|
# Provides explicit access to a Database connection.
|
232
232
|
#
|
233
233
|
# This method is useful when you need direct access to a connection
|
234
|
-
# for operations not covered by other methods. The connection is
|
234
|
+
# for operations not covered by other methods. The connection is
|
235
235
|
# properly managed and returned to the pool (if using connection_provider).
|
236
236
|
#
|
237
237
|
# @yield [Redis] A Database connection
|
data/lib/familia/core_ext.rb
CHANGED
@@ -12,7 +12,7 @@ class String
|
|
12
12
|
#
|
13
13
|
# @return [Float, nil] The time in seconds, or nil if the string is invalid
|
14
14
|
def in_seconds
|
15
|
-
q, u = scan(/([\d.]+)([
|
15
|
+
q, u = scan(/([\d.]+)([smhyd])?/).flatten
|
16
16
|
q &&= q.to_f and u ||= 's'
|
17
17
|
q&.in_seconds(u)
|
18
18
|
end
|
@@ -125,7 +125,7 @@ class Numeric
|
|
125
125
|
size = abs.to_f
|
126
126
|
unit = 0
|
127
127
|
|
128
|
-
while size
|
128
|
+
while size >= 1024 && unit < units.length - 1
|
129
129
|
size /= 1024
|
130
130
|
unit += 1
|
131
131
|
end
|