familia 1.2.1 → 2.0.0.pre2
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 +16 -2
- data/Gemfile.lock +97 -36
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +192 -0
- data/familia.gemspec +10 -6
- 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 +18 -13
- 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} +7 -5
- 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} +21 -18
- data/try/models/datatype_base_try.rb +100 -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 +143 -46
- 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,84 +1,145 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
familia (
|
4
|
+
familia (2.0.0.pre2)
|
5
|
+
benchmark
|
6
|
+
connection_pool
|
7
|
+
csv
|
8
|
+
logger
|
5
9
|
redis (>= 4.8.1, < 6.0)
|
6
10
|
stringio (~> 3.1.1)
|
7
|
-
uri-
|
11
|
+
uri-valkey (~> 1.4)
|
8
12
|
|
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
|
+
date (3.4.1)
|
25
|
+
diff-lcs (1.6.2)
|
16
26
|
drydock (0.6.9)
|
17
|
-
|
18
|
-
|
27
|
+
erb (5.0.2)
|
28
|
+
io-console (0.8.1)
|
29
|
+
irb (1.15.2)
|
30
|
+
pp (>= 0.6.0)
|
31
|
+
rdoc (>= 4.0.0)
|
32
|
+
reline (>= 0.4.2)
|
33
|
+
json (2.13.0)
|
34
|
+
kramdown (2.5.1)
|
35
|
+
rexml (>= 3.3.9)
|
36
|
+
language_server-protocol (3.17.0.5)
|
37
|
+
lint_roller (1.1.0)
|
38
|
+
logger (1.7.0)
|
19
39
|
method_source (1.1.0)
|
20
|
-
|
21
|
-
|
40
|
+
minitest (5.25.5)
|
41
|
+
parallel (1.27.0)
|
42
|
+
parser (3.3.8.0)
|
22
43
|
ast (~> 2.4.1)
|
23
44
|
racc
|
45
|
+
pp (0.6.2)
|
46
|
+
prettyprint
|
47
|
+
prettyprint (0.2.0)
|
48
|
+
prism (1.4.0)
|
24
49
|
pry (0.14.2)
|
25
50
|
coderay (~> 1.1)
|
26
51
|
method_source (~> 1.0)
|
27
52
|
pry-byebug (3.10.1)
|
28
53
|
byebug (~> 11.0)
|
29
54
|
pry (>= 0.13, < 0.15)
|
30
|
-
|
55
|
+
psych (5.2.6)
|
56
|
+
date
|
57
|
+
stringio
|
58
|
+
racc (1.8.1)
|
31
59
|
rainbow (3.1.1)
|
32
|
-
|
60
|
+
rdoc (6.14.2)
|
61
|
+
erb
|
62
|
+
psych (>= 4.0.0)
|
63
|
+
redis (5.4.1)
|
33
64
|
redis-client (>= 0.22.0)
|
34
|
-
redis-client (0.
|
65
|
+
redis-client (0.25.1)
|
35
66
|
connection_pool
|
36
|
-
regexp_parser (2.
|
37
|
-
|
38
|
-
|
39
|
-
|
67
|
+
regexp_parser (2.10.0)
|
68
|
+
reline (0.6.2)
|
69
|
+
io-console (~> 0.5)
|
70
|
+
rexml (3.4.1)
|
71
|
+
rspec (3.13.1)
|
72
|
+
rspec-core (~> 3.13.0)
|
73
|
+
rspec-expectations (~> 3.13.0)
|
74
|
+
rspec-mocks (~> 3.13.0)
|
75
|
+
rspec-core (3.13.5)
|
76
|
+
rspec-support (~> 3.13.0)
|
77
|
+
rspec-expectations (3.13.5)
|
78
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
79
|
+
rspec-support (~> 3.13.0)
|
80
|
+
rspec-mocks (3.13.5)
|
81
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
82
|
+
rspec-support (~> 3.13.0)
|
83
|
+
rspec-support (3.13.4)
|
84
|
+
rubocop (1.78.0)
|
40
85
|
json (~> 2.3)
|
41
|
-
language_server-protocol (
|
86
|
+
language_server-protocol (~> 3.17.0.2)
|
87
|
+
lint_roller (~> 1.1.0)
|
42
88
|
parallel (~> 1.10)
|
43
89
|
parser (>= 3.3.0.2)
|
44
90
|
rainbow (>= 2.2.2, < 4.0)
|
45
|
-
regexp_parser (>= 2.
|
46
|
-
|
47
|
-
rubocop-ast (>= 1.31.1, < 2.0)
|
91
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
92
|
+
rubocop-ast (>= 1.45.1, < 2.0)
|
48
93
|
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 (>=
|
94
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
95
|
+
rubocop-ast (1.46.0)
|
96
|
+
parser (>= 3.3.7.2)
|
97
|
+
prism (~> 1.4)
|
98
|
+
rubocop-performance (1.25.0)
|
99
|
+
lint_roller (~> 1.1)
|
100
|
+
rubocop (>= 1.75.0, < 2.0)
|
101
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
102
|
+
rubocop-thread_safety (0.7.3)
|
103
|
+
lint_roller (~> 1.1)
|
104
|
+
rubocop (~> 1.72, >= 1.72.1)
|
105
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
106
|
+
ruby-prof (1.7.2)
|
107
|
+
base64
|
57
108
|
ruby-progressbar (1.13.0)
|
109
|
+
stackprof (0.2.27)
|
58
110
|
storable (0.10.0)
|
59
|
-
stringio (3.1.
|
60
|
-
strscan (3.1.0)
|
111
|
+
stringio (3.1.7)
|
61
112
|
sysinfo (0.10.0)
|
62
113
|
drydock (< 1.0)
|
63
114
|
storable (~> 0.10)
|
64
|
-
tryouts (
|
65
|
-
|
66
|
-
|
67
|
-
|
115
|
+
tryouts (3.1.1)
|
116
|
+
minitest (~> 5.0)
|
117
|
+
rspec (~> 3.0)
|
118
|
+
sysinfo (>= 0.8, < 1.0)
|
119
|
+
unicode-display_width (3.1.4)
|
120
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
121
|
+
unicode-emoji (4.0.4)
|
122
|
+
uri-valkey (1.4.0)
|
123
|
+
yard (0.9.37)
|
68
124
|
|
69
125
|
PLATFORMS
|
70
|
-
arm64-darwin-23
|
71
126
|
arm64-darwin-24
|
72
127
|
ruby
|
73
128
|
|
74
129
|
DEPENDENCIES
|
75
130
|
byebug (~> 11.0)
|
131
|
+
concurrent-ruby (~> 1.3.5)
|
76
132
|
familia!
|
133
|
+
irb (~> 1.15.2)
|
134
|
+
kramdown
|
77
135
|
pry-byebug (~> 3.10.1)
|
78
136
|
rubocop
|
79
137
|
rubocop-performance
|
80
138
|
rubocop-thread_safety
|
81
|
-
|
139
|
+
ruby-prof
|
140
|
+
stackprof
|
141
|
+
tryouts (~> 3.1.1)
|
142
|
+
yard (~> 0.9)
|
82
143
|
|
83
144
|
BUNDLED WITH
|
84
|
-
2.
|
145
|
+
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,192 @@
|
|
1
|
+
# Connection Pooling with Familia
|
2
|
+
|
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
|
+
|
5
|
+
## Key Concepts
|
6
|
+
|
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.
|
10
|
+
|
11
|
+
## Basic Setup
|
12
|
+
|
13
|
+
### Simple Connection Pool
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require 'connection_pool'
|
17
|
+
|
18
|
+
class MyApp
|
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
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
@pools[pool_key].with { |conn| conn }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
### Multi-Database Configuration
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class MyApp
|
42
|
+
POOL_CONFIGS = {
|
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)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
@pools[pool_key].with { |conn| conn }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
### Production Setup with Roda
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# config/familia.rb
|
71
|
+
class FamiliaPoolManager
|
72
|
+
include Singleton
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
@pools = {}
|
76
|
+
end
|
77
|
+
|
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,
|
84
|
+
timeout: 5
|
85
|
+
) do
|
86
|
+
Redis.new(
|
87
|
+
host: parsed.host,
|
88
|
+
port: parsed.port,
|
89
|
+
db: parsed.db || 0,
|
90
|
+
timeout: 1,
|
91
|
+
reconnect_attempts: 3
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
@pools[pool_key].with { |conn| conn }
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def pool_size_for_environment
|
101
|
+
if defined?(Sidekiq)
|
102
|
+
Sidekiq.options[:concurrency] + 2
|
103
|
+
else
|
104
|
+
ENV.fetch('WEB_CONCURRENCY', 5).to_i + 2
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Configure at application startup
|
110
|
+
Familia.connection_provider = lambda do |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
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
## Model Configuration
|
125
|
+
|
126
|
+
Configure models to use different logical databases:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Customer < Familia::Horreum
|
130
|
+
self.logical_database = 0 # Main application data
|
131
|
+
field :name, :email
|
132
|
+
end
|
133
|
+
|
134
|
+
class Analytics < Familia::Horreum
|
135
|
+
self.logical_database = 1 # Analytics data
|
136
|
+
field :event_type, :timestamp
|
137
|
+
end
|
138
|
+
|
139
|
+
class Session < Familia::Horreum
|
140
|
+
self.logical_database = 2 # Session/cache data
|
141
|
+
feature :expiration
|
142
|
+
default_expiration 1.hour
|
143
|
+
field :user_id, :data
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
## Performance Benefits
|
148
|
+
|
149
|
+
Without connection pooling, each operation triggers database switches:
|
150
|
+
```
|
151
|
+
SET key value # Connection on DB 0
|
152
|
+
SELECT 2 # Switch to DB 2
|
153
|
+
SET key2 value2 # Now on DB 2
|
154
|
+
SELECT 0 # Switch back
|
155
|
+
```
|
156
|
+
|
157
|
+
With proper pooling, connections stay on the correct database:
|
158
|
+
```
|
159
|
+
SET key value # Connection already on DB 0
|
160
|
+
SET key2 value2 # Different connection, already on DB 2
|
161
|
+
```
|
162
|
+
|
163
|
+
## Pool Sizing Guidelines
|
164
|
+
|
165
|
+
- **Web Applications**: `threads + 2`
|
166
|
+
- **Background Jobs**: `concurrency + 2`
|
167
|
+
- **High Traffic DBs**: Scale up based on usage patterns
|
168
|
+
|
169
|
+
## Testing and Debugging
|
170
|
+
|
171
|
+
Enable debug mode to verify correct database selection:
|
172
|
+
```ruby
|
173
|
+
Familia.debug = true
|
174
|
+
```
|
175
|
+
|
176
|
+
Test concurrent access:
|
177
|
+
```ruby
|
178
|
+
threads = 10.times.map do |i|
|
179
|
+
Thread.new do
|
180
|
+
100.times { |j| MyModel.create(value: "test-#{i}-#{j}") }
|
181
|
+
end
|
182
|
+
end
|
183
|
+
threads.each(&:join)
|
184
|
+
```
|
185
|
+
|
186
|
+
## Best Practices
|
187
|
+
|
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
@@ -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,11 +17,15 @@ 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
|
-
spec.add_dependency 'uri-
|
28
|
+
spec.add_dependency 'uri-valkey', '~> 1.4'
|
25
29
|
|
26
30
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
27
31
|
end
|
data/lib/familia/base.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/base.rb
|
2
2
|
|
3
3
|
#
|
4
4
|
module Familia
|
5
|
-
# A common module for Familia::
|
5
|
+
# A common module for Familia::DataType and Familia::Horreum to include.
|
6
6
|
#
|
7
7
|
# This allows us to use a single comparison to check if a class is a
|
8
8
|
# Familia class. e.g.
|
@@ -11,13 +11,23 @@ module Familia
|
|
11
11
|
# klass.ancestors.member?(Familia::Base) # => true
|
12
12
|
#
|
13
13
|
# @see Familia::Horreum
|
14
|
-
# @see Familia::
|
14
|
+
# @see Familia::DataType
|
15
15
|
#
|
16
16
|
module Base
|
17
17
|
@features = nil
|
18
18
|
@dump_method = :to_json
|
19
19
|
@load_method = :from_json
|
20
20
|
|
21
|
+
# Returns a string representation of the object. Implementing classes
|
22
|
+
# are welcome to override this method to provide a more meaningful
|
23
|
+
# representation. Using this as a default via super is recommended.
|
24
|
+
#
|
25
|
+
# @return [String] A string representation of the object. Never nil.
|
26
|
+
#
|
27
|
+
def to_s
|
28
|
+
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
29
|
+
end
|
30
|
+
|
21
31
|
class << self
|
22
32
|
attr_reader :features
|
23
33
|
attr_accessor :dump_method, :load_method
|
@@ -34,23 +44,23 @@ module Familia
|
|
34
44
|
# with the :expiration feature's implementation.
|
35
45
|
#
|
36
46
|
# This is a no-op implementation that gets overridden by features like
|
37
|
-
# :expiration. It accepts an optional
|
47
|
+
# :expiration. It accepts an optional default_expiration parameter to maintain interface
|
38
48
|
# compatibility with the overriding implementations.
|
39
49
|
#
|
40
|
-
# @param
|
50
|
+
# @param default_expiration [Integer, nil] Time To Live in seconds (ignored in base implementation)
|
41
51
|
# @return [nil] Always returns nil
|
42
52
|
#
|
43
53
|
# @note This is a no-op implementation. Classes that need expiration
|
44
54
|
# functionality should include the :expiration feature.
|
45
55
|
#
|
46
|
-
def update_expiration(
|
47
|
-
Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{
|
56
|
+
def update_expiration(default_expiration: nil)
|
57
|
+
Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{dbkey} (caller: #{caller(1..1)})"
|
48
58
|
nil
|
49
59
|
end
|
50
60
|
|
51
61
|
def generate_id
|
52
|
-
@
|
53
|
-
@
|
62
|
+
@identifier ||= Familia.generate_id
|
63
|
+
@identifier
|
54
64
|
end
|
55
65
|
|
56
66
|
def uuid
|