familia 1.2.1 → 2.0.0.pre.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +68 -0
- data/.github/workflows/docs.yml +64 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +3 -1
- data/.rubocop.yml +16 -9
- data/.rubocop_todo.yml +177 -31
- data/.yardopts +9 -0
- data/CLAUDE.md +141 -0
- data/Gemfile +15 -2
- data/Gemfile.lock +76 -34
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +317 -0
- data/familia.gemspec +9 -5
- data/lib/familia/base.rb +19 -9
- data/lib/familia/connection.rb +232 -65
- data/lib/familia/core_ext.rb +1 -1
- data/lib/familia/datatype/commands.rb +59 -0
- data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
- data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
- data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
- data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
- data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
- data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
- data/lib/familia/datatype.rb +243 -0
- data/lib/familia/errors.rb +5 -2
- data/lib/familia/features/expiration.rb +33 -34
- data/lib/familia/features/quantization.rb +9 -3
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features.rb +2 -2
- data/lib/familia/horreum/class_methods.rb +97 -110
- data/lib/familia/horreum/commands.rb +46 -51
- data/lib/familia/horreum/connection.rb +82 -0
- data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
- data/lib/familia/horreum/serialization.rb +61 -198
- data/lib/familia/horreum/settings.rb +6 -17
- data/lib/familia/horreum/utils.rb +11 -10
- data/lib/familia/horreum.rb +69 -60
- data/lib/familia/logging.rb +12 -12
- data/lib/familia/multi_result.rb +72 -0
- data/lib/familia/refinements.rb +7 -44
- data/lib/familia/settings.rb +11 -11
- data/lib/familia/utils.rb +123 -90
- data/lib/familia/version.rb +4 -21
- data/lib/familia.rb +17 -12
- data/lib/middleware/database_middleware.rb +150 -0
- data/try/configuration/scenarios_try.rb +65 -0
- data/try/core/connection_try.rb +58 -0
- data/try/core/errors_try.rb +93 -0
- data/try/core/extensions_try.rb +26 -0
- data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
- data/try/{00_familia_try.rb → core/familia_try.rb} +5 -3
- data/try/core/middleware_try.rb +68 -0
- data/try/core/refinements_try.rb +39 -0
- data/try/core/settings_try.rb +76 -0
- data/try/core/tools_try.rb +54 -0
- data/try/core/utils_try.rb +189 -0
- data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
- data/try/datatypes/datatype_base_try.rb +69 -0
- data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
- data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
- data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
- data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
- data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
- data/try/edge_cases/empty_identifiers_try.rb +48 -0
- data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
- data/try/edge_cases/json_serialization_try.rb +85 -0
- data/try/edge_cases/race_conditions_try.rb +60 -0
- data/try/edge_cases/reserved_keywords_try.rb +59 -0
- data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
- data/try/edge_cases/ttl_side_effects_try.rb +51 -0
- data/try/features/expiration_try.rb +86 -0
- data/try/features/quantization_try.rb +90 -0
- data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
- data/try/features/safe_dump_try.rb +137 -0
- data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
- data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
- data/try/horreum/class_methods_try.rb +41 -0
- data/try/horreum/commands_try.rb +49 -0
- data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
- data/try/horreum/relations_try.rb +146 -0
- data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
- data/try/horreum/settings_try.rb +43 -0
- data/try/integration/cross_component_try.rb +46 -0
- data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
- data/try/{40_customer_try.rb → models/customer_try.rb} +20 -17
- data/try/models/datatype_base_try.rb +101 -0
- data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
- data/try/performance/benchmarks_try.rb +55 -0
- data/try/pooling/README.md +20 -0
- data/try/pooling/configurable_stress_test_try.rb +435 -0
- data/try/pooling/connection_pool_test_try.rb +273 -0
- data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- data/try/pooling/lib/connection_pool_metrics.rb +372 -0
- data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
- data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
- data/try/pooling/lib/visualize_stress_results.rb +434 -0
- data/try/pooling/pool_siege_try.rb +509 -0
- data/try/pooling/run_stress_tests_try.rb +482 -0
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
- data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
- data/try/prototypes/atomic_saves_v4.rb +105 -0
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- metadata +140 -43
- data/.github/workflows/ruby.yml +0 -71
- data/VERSION.yml +0 -4
- data/lib/familia/redistype/commands.rb +0 -59
- data/lib/familia/redistype.rb +0 -228
- data/lib/familia/tools.rb +0 -68
- data/lib/redis_middleware.rb +0 -109
- data/try/20_redis_type_try.rb +0 -70
- data/try/91_json_bug_try.rb +0 -86
data/Gemfile.lock
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
familia (
|
4
|
+
familia (2.0.0.pre.pre)
|
5
|
+
benchmark
|
6
|
+
connection_pool
|
7
|
+
csv
|
8
|
+
logger
|
5
9
|
redis (>= 4.8.1, < 6.0)
|
6
10
|
stringio (~> 3.1.1)
|
7
11
|
uri-redis (~> 1.3)
|
@@ -9,76 +13,114 @@ PATH
|
|
9
13
|
GEM
|
10
14
|
remote: https://rubygems.org/
|
11
15
|
specs:
|
12
|
-
ast (2.4.
|
16
|
+
ast (2.4.3)
|
17
|
+
base64 (0.3.0)
|
18
|
+
benchmark (0.4.1)
|
13
19
|
byebug (11.1.3)
|
14
20
|
coderay (1.1.3)
|
15
|
-
|
21
|
+
concurrent-ruby (1.3.5)
|
22
|
+
connection_pool (2.5.3)
|
23
|
+
csv (3.3.5)
|
24
|
+
diff-lcs (1.6.2)
|
16
25
|
drydock (0.6.9)
|
17
|
-
json (2.
|
18
|
-
|
26
|
+
json (2.13.0)
|
27
|
+
kramdown (2.5.1)
|
28
|
+
rexml (>= 3.3.9)
|
29
|
+
language_server-protocol (3.17.0.5)
|
30
|
+
lint_roller (1.1.0)
|
31
|
+
logger (1.7.0)
|
19
32
|
method_source (1.1.0)
|
20
|
-
|
21
|
-
|
33
|
+
minitest (5.25.5)
|
34
|
+
parallel (1.27.0)
|
35
|
+
parser (3.3.8.0)
|
22
36
|
ast (~> 2.4.1)
|
23
37
|
racc
|
38
|
+
prism (1.4.0)
|
24
39
|
pry (0.14.2)
|
25
40
|
coderay (~> 1.1)
|
26
41
|
method_source (~> 1.0)
|
27
42
|
pry-byebug (3.10.1)
|
28
43
|
byebug (~> 11.0)
|
29
44
|
pry (>= 0.13, < 0.15)
|
30
|
-
racc (1.8.
|
45
|
+
racc (1.8.1)
|
31
46
|
rainbow (3.1.1)
|
32
|
-
redis (5.
|
47
|
+
redis (5.4.1)
|
33
48
|
redis-client (>= 0.22.0)
|
34
|
-
redis-client (0.
|
49
|
+
redis-client (0.25.1)
|
35
50
|
connection_pool
|
36
|
-
regexp_parser (2.
|
37
|
-
rexml (3.
|
38
|
-
|
39
|
-
|
51
|
+
regexp_parser (2.10.0)
|
52
|
+
rexml (3.4.1)
|
53
|
+
rspec (3.13.1)
|
54
|
+
rspec-core (~> 3.13.0)
|
55
|
+
rspec-expectations (~> 3.13.0)
|
56
|
+
rspec-mocks (~> 3.13.0)
|
57
|
+
rspec-core (3.13.5)
|
58
|
+
rspec-support (~> 3.13.0)
|
59
|
+
rspec-expectations (3.13.5)
|
60
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
61
|
+
rspec-support (~> 3.13.0)
|
62
|
+
rspec-mocks (3.13.5)
|
63
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
64
|
+
rspec-support (~> 3.13.0)
|
65
|
+
rspec-support (3.13.4)
|
66
|
+
rubocop (1.78.0)
|
40
67
|
json (~> 2.3)
|
41
|
-
language_server-protocol (
|
68
|
+
language_server-protocol (~> 3.17.0.2)
|
69
|
+
lint_roller (~> 1.1.0)
|
42
70
|
parallel (~> 1.10)
|
43
71
|
parser (>= 3.3.0.2)
|
44
72
|
rainbow (>= 2.2.2, < 4.0)
|
45
|
-
regexp_parser (>= 2.
|
46
|
-
|
47
|
-
rubocop-ast (>= 1.31.1, < 2.0)
|
73
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
74
|
+
rubocop-ast (>= 1.45.1, < 2.0)
|
48
75
|
ruby-progressbar (~> 1.7)
|
49
|
-
unicode-display_width (>= 2.4.0, <
|
50
|
-
rubocop-ast (1.
|
51
|
-
parser (>= 3.3.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
rubocop (>=
|
76
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
77
|
+
rubocop-ast (1.46.0)
|
78
|
+
parser (>= 3.3.7.2)
|
79
|
+
prism (~> 1.4)
|
80
|
+
rubocop-performance (1.25.0)
|
81
|
+
lint_roller (~> 1.1)
|
82
|
+
rubocop (>= 1.75.0, < 2.0)
|
83
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
84
|
+
rubocop-thread_safety (0.7.3)
|
85
|
+
lint_roller (~> 1.1)
|
86
|
+
rubocop (~> 1.72, >= 1.72.1)
|
87
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
88
|
+
ruby-prof (1.7.2)
|
89
|
+
base64
|
57
90
|
ruby-progressbar (1.13.0)
|
91
|
+
stackprof (0.2.27)
|
58
92
|
storable (0.10.0)
|
59
|
-
stringio (3.1.
|
60
|
-
strscan (3.1.0)
|
93
|
+
stringio (3.1.7)
|
61
94
|
sysinfo (0.10.0)
|
62
95
|
drydock (< 1.0)
|
63
96
|
storable (~> 0.10)
|
64
|
-
tryouts (
|
65
|
-
|
66
|
-
|
97
|
+
tryouts (3.1.1)
|
98
|
+
minitest (~> 5.0)
|
99
|
+
rspec (~> 3.0)
|
100
|
+
sysinfo (>= 0.8, < 1.0)
|
101
|
+
unicode-display_width (3.1.4)
|
102
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
103
|
+
unicode-emoji (4.0.4)
|
67
104
|
uri-redis (1.3.0)
|
105
|
+
yard (0.9.37)
|
68
106
|
|
69
107
|
PLATFORMS
|
70
|
-
arm64-darwin-23
|
71
108
|
arm64-darwin-24
|
72
109
|
ruby
|
73
110
|
|
74
111
|
DEPENDENCIES
|
75
112
|
byebug (~> 11.0)
|
113
|
+
concurrent-ruby (~> 1.3.5)
|
76
114
|
familia!
|
115
|
+
kramdown
|
77
116
|
pry-byebug (~> 3.10.1)
|
78
117
|
rubocop
|
79
118
|
rubocop-performance
|
80
119
|
rubocop-thread_safety
|
81
|
-
|
120
|
+
ruby-prof
|
121
|
+
stackprof
|
122
|
+
tryouts (~> 3.1.1)
|
123
|
+
yard (~> 0.9)
|
82
124
|
|
83
125
|
BUNDLED WITH
|
84
|
-
2.
|
126
|
+
2.6.2
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
# Familia -
|
1
|
+
# Familia - 2.0.0
|
2
2
|
|
3
|
-
**Organize and store Ruby objects in Redis. A powerful Ruby ORM (of sorts) for Redis.**
|
3
|
+
**Organize and store Ruby objects in Valkey/Redis. A powerful Ruby ORM (of sorts) for Valkey/Redis.**
|
4
4
|
|
5
|
-
Familia provides a flexible and feature-rich way to interact with
|
5
|
+
Familia provides a flexible and feature-rich way to interact with Valkey using Ruby objects. It's designed to make working with Valkey as natural as working with Ruby classes, while offering advanced features for complex data management.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -18,11 +18,11 @@ Get it in one of the following ways:
|
|
18
18
|
|
19
19
|
### 1. Defining Horreum Classes
|
20
20
|
|
21
|
-
Familia uses the concept of "Horreum" classes to represent
|
21
|
+
Familia uses the concept of "Horreum" classes to represent Valkey-compatible objects:
|
22
22
|
|
23
23
|
```ruby
|
24
24
|
class Flower < Familia::Horreum
|
25
|
-
|
25
|
+
identifier_field :token
|
26
26
|
field :name
|
27
27
|
list :owners
|
28
28
|
set :tags
|
@@ -38,20 +38,20 @@ You can define identifiers in various ways:
|
|
38
38
|
|
39
39
|
```ruby
|
40
40
|
class User < Familia::Horreum
|
41
|
-
|
41
|
+
identifier_field :email
|
42
42
|
# or
|
43
|
-
|
43
|
+
identifier_field -> (user) { "user:#{user.email}" }
|
44
44
|
# or
|
45
|
-
|
45
|
+
identifier_field [:type, :email]
|
46
46
|
|
47
47
|
field :email
|
48
48
|
field :type
|
49
49
|
end
|
50
50
|
```
|
51
51
|
|
52
|
-
### 3.
|
52
|
+
### 3. Data Types
|
53
53
|
|
54
|
-
Familia supports various
|
54
|
+
Familia supports various Valkey-compatible data types:
|
55
55
|
|
56
56
|
```ruby
|
57
57
|
class Product < Familia::Horreum
|
@@ -63,9 +63,9 @@ class Product < Familia::Horreum
|
|
63
63
|
end
|
64
64
|
```
|
65
65
|
|
66
|
-
### 4. Class-level
|
66
|
+
### 4. Class-level Valkey-compatible Types
|
67
67
|
|
68
|
-
You can also define
|
68
|
+
You can also define Valkey-compatible types at the class level:
|
69
69
|
|
70
70
|
```ruby
|
71
71
|
class Customer < Familia::Horreum
|
@@ -78,12 +78,12 @@ end
|
|
78
78
|
|
79
79
|
### 5. Automatic Expiration
|
80
80
|
|
81
|
-
Use the expiration feature to set TTL for objects:
|
81
|
+
Use the expiration feature to set default TTL for objects:
|
82
82
|
|
83
83
|
```ruby
|
84
84
|
class Session < Familia::Horreum
|
85
85
|
feature :expiration
|
86
|
-
|
86
|
+
default_expiration 180.minutes
|
87
87
|
end
|
88
88
|
```
|
89
89
|
|
@@ -110,7 +110,7 @@ Use quantization for time-based metrics:
|
|
110
110
|
```ruby
|
111
111
|
class DailyMetric < Familia::Horreum
|
112
112
|
feature :quantization
|
113
|
-
string :counter,
|
113
|
+
string :counter, default_expiration: 1.day, quantize: [10.minutes, '%H:%M']
|
114
114
|
end
|
115
115
|
```
|
116
116
|
|
@@ -140,13 +140,6 @@ class Customer < Familia::Horreum
|
|
140
140
|
verified && !reset_requested
|
141
141
|
end
|
142
142
|
end
|
143
|
-
|
144
|
-
class Session < Familia::Horreum
|
145
|
-
def external_identifier
|
146
|
-
elements = [ipaddress || 'UNKNOWNIP', custid || 'anon']
|
147
|
-
@external_identifier ||= Familia.generate_sha_hash(elements)
|
148
|
-
end
|
149
|
-
end
|
150
143
|
```
|
151
144
|
### 10. Open-ended Serialization
|
152
145
|
|
@@ -171,6 +164,29 @@ user.transaction do |conn|
|
|
171
164
|
end
|
172
165
|
```
|
173
166
|
|
167
|
+
### 12. Connection Management and Pooling
|
168
|
+
|
169
|
+
Familia supports custom connection providers for advanced scenarios like connection pooling:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
# Using connection_pool gem for thread-safe pooling
|
173
|
+
require 'connection_pool'
|
174
|
+
|
175
|
+
# Create pools for each logical database
|
176
|
+
pools = {
|
177
|
+
"redis://localhost:6379/0" => ConnectionPool.new(size: 10) { Redis.new(db: 0) },
|
178
|
+
"redis://localhost:6379/1" => ConnectionPool.new(size: 5) { Redis.new(db: 1) }
|
179
|
+
}
|
180
|
+
|
181
|
+
# Configure Familia to use the pools
|
182
|
+
Familia.connection_provider = lambda do |uri|
|
183
|
+
pool = pools[uri] || pools["redis://localhost:6379/0"]
|
184
|
+
pool.with { |conn| conn }
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
See the [Connection Pooling Guide](docs/connection_pooling.md) for detailed examples.
|
189
|
+
|
174
190
|
|
175
191
|
## Usage Examples
|
176
192
|
|
@@ -225,7 +241,7 @@ end
|
|
225
241
|
|
226
242
|
## Conclusion
|
227
243
|
|
228
|
-
Familia provides a powerful and flexible way to work with
|
244
|
+
Familia provides a powerful and flexible way to work with Valkey-compatible in Ruby applications. Its features like automatic expiration, safe dumping, and quantization make it suitable for a wide range of use cases, from simple key-value storage to complex time-series data management.
|
229
245
|
|
230
246
|
For more information, visit:
|
231
247
|
- [Github Repository](https://github.com/delano/familia)
|
data/bin/irb
ADDED
@@ -0,0 +1,317 @@
|
|
1
|
+
# Connection Pooling with Familia
|
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.
|
4
|
+
|
5
|
+
## Key Concepts
|
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.
|
8
|
+
|
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.
|
10
|
+
|
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
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'connection_pool'
|
19
|
+
require 'familia'
|
20
|
+
|
21
|
+
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 }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### Example 2: Advanced Pooling with Different Configurations
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class MyApp
|
49
|
+
# Different pool sizes based on expected traffic
|
50
|
+
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
|
70
|
+
end
|
71
|
+
|
72
|
+
@pools[pool_key].with { |conn| conn }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
### Example 3: Using with Puma/Multi-threaded Servers
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# In config/initializers/familia.rb
|
82
|
+
|
83
|
+
# Global connection pools shared across threads
|
84
|
+
$redis_pools = {}
|
85
|
+
$pool_mutex = Mutex.new
|
86
|
+
|
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,
|
95
|
+
timeout: 5
|
96
|
+
) do
|
97
|
+
Redis.new(
|
98
|
+
host: parsed.host,
|
99
|
+
port: parsed.port,
|
100
|
+
db: parsed.db || 0,
|
101
|
+
timeout: 1, # Connection timeout
|
102
|
+
reconnect_attempts: 3
|
103
|
+
)
|
104
|
+
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
|
+
|
117
|
+
class RedisConnectionPools
|
118
|
+
include Singleton
|
119
|
+
|
120
|
+
def initialize
|
121
|
+
@pools = {}
|
122
|
+
@mutex = Mutex.new
|
123
|
+
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
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def determine_pool_size
|
148
|
+
if defined?(Sidekiq)
|
149
|
+
# Sidekiq workers need more connections
|
150
|
+
Sidekiq.options[:concurrency] + 2
|
151
|
+
else
|
152
|
+
# Web processes need fewer connections
|
153
|
+
ENV.fetch('RAILS_MAX_THREADS', 5).to_i
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Configure Familia
|
159
|
+
Familia.connection_provider = lambda do |uri|
|
160
|
+
RedisConnectionPools.instance.get_connection(uri)
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
## Model Configuration
|
165
|
+
|
166
|
+
Models can specify different logical databases:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
# Models using different logical databases
|
170
|
+
class Customer < Familia::Horreum
|
171
|
+
self.logical_database = 0 # Main application data
|
172
|
+
field :name
|
173
|
+
field :email
|
174
|
+
end
|
175
|
+
|
176
|
+
class Analytics < Familia::Horreum
|
177
|
+
self.logical_database = 1 # Analytics data
|
178
|
+
field :event_type
|
179
|
+
field :timestamp
|
180
|
+
end
|
181
|
+
|
182
|
+
class Session < Familia::Horreum
|
183
|
+
self.logical_database = 2 # Session/cache data
|
184
|
+
feature :expiration
|
185
|
+
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
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
## Performance Optimization
|
199
|
+
|
200
|
+
### Avoiding SELECT Command Overhead
|
201
|
+
|
202
|
+
Without proper pooling configuration, each Redis operation might issue a SELECT command:
|
203
|
+
|
204
|
+
```
|
205
|
+
# Bad: Without connection provider
|
206
|
+
SET key value # Connection on DB 0
|
207
|
+
SELECT 2 # Switch to DB 2
|
208
|
+
SET key2 value2 # Now on DB 2
|
209
|
+
SELECT 0 # Switch back
|
210
|
+
GET key # Now on DB 0
|
211
|
+
```
|
212
|
+
|
213
|
+
With the connection provider pattern:
|
214
|
+
|
215
|
+
```
|
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
|
220
|
+
```
|
221
|
+
|
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
|
+
```
|
228
|
+
|
229
|
+
2. **Background Jobs**: `pool_size = concurrency + buffer`
|
230
|
+
```ruby
|
231
|
+
size: Sidekiq.options[:concurrency] + 5
|
232
|
+
```
|
233
|
+
|
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
|
244
|
+
|
245
|
+
```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
|
285
|
+
```
|
286
|
+
|
287
|
+
Monitor pool usage:
|
288
|
+
|
289
|
+
```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 }
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
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
|
312
|
+
|
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
|
data/familia.gemspec
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/settings.rb
|
2
2
|
|
3
3
|
require_relative 'lib/familia/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'familia'
|
7
|
-
spec.version = Familia::VERSION
|
8
|
-
spec.summary = 'An ORM for
|
9
|
-
spec.description = "Familia: #{spec.summary}. Organize and store ruby objects in Redis"
|
7
|
+
spec.version = Familia::VERSION
|
8
|
+
spec.summary = 'An ORM for Valkey-compatible databases in Ruby.'
|
9
|
+
spec.description = "Familia: #{spec.summary}. Organize and store ruby objects in Valkey/Redis"
|
10
10
|
spec.authors = ['Delano Mandelbaum']
|
11
11
|
spec.email = 'gems@solutious.com'
|
12
12
|
spec.homepage = 'https://github.com/delano/familia'
|
@@ -17,8 +17,12 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
18
|
spec.require_paths = ['lib']
|
19
19
|
|
20
|
-
spec.required_ruby_version = Gem::Requirement.new('>=
|
20
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 3.4')
|
21
21
|
|
22
|
+
spec.add_dependency 'benchmark'
|
23
|
+
spec.add_dependency 'connection_pool'
|
24
|
+
spec.add_dependency 'csv'
|
25
|
+
spec.add_dependency 'logger'
|
22
26
|
spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
|
23
27
|
spec.add_dependency 'stringio', '~> 3.1.1'
|
24
28
|
spec.add_dependency 'uri-redis', '~> 1.3'
|