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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +12 -5
  3. data/Gemfile +4 -3
  4. data/Gemfile.lock +24 -11
  5. data/bin/irb +1 -1
  6. data/docs/connection_pooling.md +98 -223
  7. data/familia.gemspec +1 -1
  8. data/lib/familia/connection.rb +3 -3
  9. data/lib/familia/core_ext.rb +2 -2
  10. data/lib/familia/features/expiration.rb +0 -1
  11. data/lib/familia/features/relatable_objects.rb +127 -0
  12. data/lib/familia/features.rb +7 -3
  13. data/lib/familia/horreum/class_methods.rb +18 -4
  14. data/lib/familia/secure_identifier.rb +129 -0
  15. data/lib/familia/utils.rb +7 -96
  16. data/lib/familia/version.rb +1 -1
  17. data/lib/familia.rb +3 -1
  18. data/try/configuration/scenarios_try.rb +43 -31
  19. data/try/core/connection_try.rb +1 -1
  20. data/try/core/errors_try.rb +10 -10
  21. data/try/core/extensions_try.rb +56 -23
  22. data/try/core/familia_extended_try.rb +3 -3
  23. data/try/core/familia_try.rb +2 -6
  24. data/try/core/middleware_try.rb +34 -40
  25. data/try/{pooling/connection_pool_test_try.rb → core/pools_try.rb} +2 -2
  26. data/try/core/secure_identifier_try.rb +104 -0
  27. data/try/core/tools_try.rb +52 -36
  28. data/try/core/utils_try.rb +0 -98
  29. data/try/datatypes/boolean_try.rb +6 -7
  30. data/try/datatypes/datatype_base_try.rb +2 -2
  31. data/try/datatypes/hash_try.rb +0 -1
  32. data/try/datatypes/list_try.rb +0 -1
  33. data/try/datatypes/set_try.rb +0 -2
  34. data/try/datatypes/sorted_set_try.rb +1 -2
  35. data/try/datatypes/string_try.rb +1 -2
  36. data/try/edge_cases/empty_identifiers_try.rb +42 -35
  37. data/try/edge_cases/hash_symbolization_try.rb +5 -5
  38. data/try/edge_cases/json_serialization_try.rb +12 -13
  39. data/try/edge_cases/race_conditions_try.rb +46 -49
  40. data/try/edge_cases/reserved_keywords_try.rb +103 -49
  41. data/try/edge_cases/string_coercion_try.rb +2 -2
  42. data/try/edge_cases/ttl_side_effects_try.rb +44 -25
  43. data/try/features/expiration_try.rb +2 -2
  44. data/try/features/quantization_try.rb +2 -2
  45. data/try/features/relatable_objects_try.rb +221 -0
  46. data/try/features/safe_dump_advanced_try.rb +13 -14
  47. data/try/features/safe_dump_try.rb +8 -8
  48. data/try/helpers/test_helpers.rb +10 -12
  49. data/try/horreum/base_try.rb +9 -9
  50. data/try/horreum/class_methods_try.rb +34 -28
  51. data/try/horreum/commands_try.rb +69 -33
  52. data/try/horreum/initialization_try.rb +4 -4
  53. data/try/horreum/relations_try.rb +13 -14
  54. data/try/horreum/serialization_try.rb +3 -3
  55. data/try/horreum/settings_try.rb +25 -31
  56. data/try/integration/cross_component_try.rb +45 -35
  57. data/try/models/customer_safe_dump_try.rb +4 -4
  58. data/try/models/customer_try.rb +22 -25
  59. data/try/models/datatype_base_try.rb +2 -4
  60. data/try/models/familia_object_try.rb +3 -4
  61. data/try/performance/benchmarks_try.rb +47 -38
  62. data/try/prototypes/atomic_saves_v4.rb +3 -3
  63. metadata +18 -15
  64. data/try/core/refinements_try.rb +0 -39
  65. /data/try/{pooling → prototypes/pooling}/README.md +0 -0
  66. /data/try/{pooling/configurable_stress_test_try.rb → prototypes/pooling/configurable_stress_test.rb} +0 -0
  67. /data/try/{pooling → prototypes/pooling}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
  68. /data/try/{pooling → prototypes/pooling}/lib/connection_pool_metrics.rb +0 -0
  69. /data/try/{pooling → prototypes/pooling}/lib/connection_pool_stress_test.rb +0 -0
  70. /data/try/{pooling → prototypes/pooling}/lib/connection_pool_threading_models.rb +0 -0
  71. /data/try/{pooling → prototypes/pooling}/lib/visualize_stress_results.rb +0 -0
  72. /data/try/{pooling/pool_siege_try.rb → prototypes/pooling/pool_siege.rb} +0 -0
  73. /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: 92ee81bd30cdbcb84de0e72a26bde44a170efcd6578d81bdd47458aee37f52be
4
- data.tar.gz: 4127847dbc955df3e6a7c7a3dd616dd3438da046f8703a59c734399697cf8903
3
+ metadata.gz: a5d32d5a95de13ddda788d37390fb97e7a73c5ff1b80515bf55f2db16d18a211
4
+ data.tar.gz: b5a97154c4f4461de329d5190b239b0db135304ce2b36e59d226f8b37ade5614
5
5
  SHA512:
6
- metadata.gz: 87f8ff4b5423771ce6c1c93640cd3e83ea1e9beb023c130896878bed64fd61341ac18b365641bd7c30310fafdcf1b31569ca726dbaedf5c7ec23d308805b3305
7
- data.tar.gz: d76036d625a604014380a67e5765263c2ea51bb608da09344a57fb4cf8386ae724499bb27e352c7d7133d15ae16a052dcb27ea262943cb509662666c72983d13
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
- - **Run tests**: `bundle exec tryouts` (uses tryouts testing framework)
9
- - **Run specific test file**: `bundle exec tryouts try/specific_test_try.rb`
10
- - **Debug mode**: `FAMILIA_DEBUG=1 bundle exec tryouts`
11
- - **Trace mode**: `FAMILIA_TRACE=1 bundle exec tryouts` (detailed Redis operation logging)
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: `identifier :email`
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: '../../d/tryouts'
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.1', require: false
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.pre.pre)
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-redis (~> 1.3)
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
- drydock (0.6.9)
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
- sysinfo (0.10.0)
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-redis (1.3.0)
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.1)
135
+ tryouts (~> 3.1.2)
123
136
  yard (~> 0.9)
124
137
 
125
138
  BUNDLED WITH
data/bin/irb CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/bin/bash
2
2
 
3
- irb -ruri/redis -Ilib -r familia -Itry -r helpers/test_helpers
3
+ irb -Ilib -r familia -Itry -r helpers/test_helpers
@@ -1,317 +1,192 @@
1
1
  # Connection Pooling with Familia
2
2
 
3
- Familia uses a flexible connection provider pattern that allows you to implement connection pooling in your application. This guide shows how to configure connection pools for optimal performance with multiple logical databases.
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
- 1. **Connection Provider Contract**: When you provide a `connection_provider`, it MUST return connections already on the correct logical database. Familia will NOT issue SELECT commands after receiving a connection from the provider.
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
- 2. **URI-based Selection**: Familia passes normalized URIs (e.g., `redis://localhost:6379/2`) to your provider, encoding the logical database in the URI.
11
+ ## Basic Setup
10
12
 
11
- 3. **One Pool Per Database**: Since Familia models can use different logical databases, you typically need one connection pool per unique database.
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
- def self.setup_familia_pools
23
- @pools = {}
24
-
25
- Familia.connection_provider = lambda do |uri|
26
- parsed = URI.parse(uri)
27
- pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
28
-
29
- # Create a pool for each unique database
30
- @pools[pool_key] ||= ConnectionPool.new(size: 10, timeout: 5) do
31
- Redis.new(
32
- host: parsed.host,
33
- port: parsed.port,
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
- ### Example 2: Advanced Pooling with Different Configurations
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, timeout: 5 }, # High-traffic main database
52
- 1 => { size: 5, timeout: 5 }, # Low-traffic analytics database
53
- 2 => { size: 10, timeout: 5 }, # Medium-traffic cache database
54
- 3 => { size: 5, timeout: 5 } # Session database
55
- }
56
-
57
- def self.setup_familia_pools
58
- @pools = {}
59
-
60
- Familia.connection_provider = lambda do |uri|
61
- parsed = URI.parse(uri)
62
- db = parsed.db || 0
63
- pool_key = "#{parsed.host}:#{parsed.port}/#{db}"
64
-
65
- @pools[pool_key] ||= begin
66
- config = POOL_CONFIGS[db] || { size: 5, timeout: 5 }
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
- ### Example 3: Using with Puma/Multi-threaded Servers
67
+ ### Production Setup with Roda
79
68
 
80
69
  ```ruby
81
- # In config/initializers/familia.rb
70
+ # config/familia.rb
71
+ class FamiliaPoolManager
72
+ include Singleton
82
73
 
83
- # Global connection pools shared across threads
84
- $redis_pools = {}
85
- $pool_mutex = Mutex.new
74
+ def initialize
75
+ @pools = {}
76
+ end
86
77
 
87
- Familia.connection_provider = lambda do |uri|
88
- parsed = URI.parse(uri)
89
- pool_key = parsed.to_s
90
-
91
- # Thread-safe pool creation
92
- $pool_mutex.synchronize do
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, # Connection timeout
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
- class RedisConnectionPools
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 determine_pool_size
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
- # Web processes need fewer connections
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 Familia
109
+ # Configure at application startup
159
110
  Familia.connection_provider = lambda do |uri|
160
- RedisConnectionPools.instance.get_connection(uri)
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
- Models can specify different logical databases:
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 Optimization
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 connection provider pattern:
214
-
157
+ With proper pooling, connections stay on the correct database:
215
158
  ```
216
- # Good: With connection provider
217
- SET key value # Connection already on correct DB
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
- ### Pool Sizing Guidelines
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
- 2. **Background Jobs**: `pool_size = concurrency + buffer`
230
- ```ruby
231
- size: Sidekiq.options[:concurrency] + 5
232
- ```
165
+ - **Web Applications**: `threads + 2`
166
+ - **Background Jobs**: `concurrency + 2`
167
+ - **High Traffic DBs**: Scale up based on usage patterns
233
168
 
234
- 3. **Mixed Workloads**: Size based on the logical database's usage pattern
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
- # Test helper to verify pools are working correctly
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
- Monitor pool usage:
288
-
176
+ Test concurrent access:
289
177
  ```ruby
290
- # Add monitoring to your connection provider
291
- Familia.connection_provider = lambda do |uri|
292
- pool = get_pool_for(uri)
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
- ## Common Pitfalls
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
- - Familia delegates connection management to your application via `connection_provider`
314
- - Providers must return connections already on the correct logical database
315
- - Use the `connection_pool` gem for robust pooling
316
- - Create one pool per unique logical database
317
- - Monitor and tune pool sizes based on your workload
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-redis', '~> 1.3'
28
+ spec.add_dependency 'uri-valkey', '~> 1.4'
29
29
 
30
30
  spec.metadata['rubygems_mfa_required'] = 'true'
31
31
  end
@@ -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
@@ -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.]+)([smh])?/).flatten
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 > 1024 && unit < units.length - 1
128
+ while size >= 1024 && unit < units.length - 1
129
129
  size /= 1024
130
130
  unit += 1
131
131
  end
@@ -92,7 +92,6 @@ module Familia::Features
92
92
  # a bool.
93
93
  expire(default_expiration)
94
94
  end
95
-
96
95
  extend ClassMethods
97
96
 
98
97
  Familia::Base.add_feature self, :expiration