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
@@ -0,0 +1,127 @@
|
|
1
|
+
# apps/api/v2/models/features/relatable_object.rb
|
2
|
+
|
3
|
+
module V2
|
4
|
+
module Features
|
5
|
+
class RelatableObjectError < Familia::Problem; end
|
6
|
+
|
7
|
+
# RelatableObject
|
8
|
+
#
|
9
|
+
# Provides the standard core object fields and methods.
|
10
|
+
#
|
11
|
+
module RelatableObject
|
12
|
+
klass = self
|
13
|
+
err_klass = V2::Features::RelatableObjectError
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.class_sorted_set :relatable_objids
|
17
|
+
base.class_hashkey :owners
|
18
|
+
|
19
|
+
# NOTE: we do not automatically assign the objid field as the
|
20
|
+
# main identifier field. That's up to the implementing class.
|
21
|
+
base.field :objid
|
22
|
+
base.field :extid
|
23
|
+
base.field :api_version
|
24
|
+
|
25
|
+
base.extend(ClassMethods)
|
26
|
+
|
27
|
+
# prepend ensures our methods execute BEFORE field-generated accessors
|
28
|
+
# include would place them AFTER, but they'd never execute because
|
29
|
+
# attr_reader doesn't call super - it just returns the instance variable
|
30
|
+
#
|
31
|
+
# Method lookup chain:
|
32
|
+
# prepend: [InstanceMethods] → [Field Methods] → [Parent]
|
33
|
+
# include: [Field Methods] → [InstanceMethods] → [Parent]
|
34
|
+
# (stops here, no super) (never reached)
|
35
|
+
#
|
36
|
+
base.prepend(InstanceMethods)
|
37
|
+
end
|
38
|
+
|
39
|
+
module InstanceMethods
|
40
|
+
# We lazily generate the object ID and external ID when they are first
|
41
|
+
# accessed so that we can instantiate and load existing objects, without
|
42
|
+
# eagerly generating them, only to be overridden by the storage layer.
|
43
|
+
#
|
44
|
+
def init
|
45
|
+
super if defined?(super) # Only call if parent has init
|
46
|
+
|
47
|
+
@api_version ||= 'v2'
|
48
|
+
end
|
49
|
+
|
50
|
+
def objid
|
51
|
+
@objid ||= begin # lazy loader
|
52
|
+
generated_id = self.class.generate_objid
|
53
|
+
# Using the attr_writer method ensures any future Familia
|
54
|
+
# enhancements to the setter are properly invoked (as opposed
|
55
|
+
# to directly assigning @objid).
|
56
|
+
self.objid = generated_id
|
57
|
+
end
|
58
|
+
end
|
59
|
+
alias relatable_objid objid
|
60
|
+
|
61
|
+
def extid
|
62
|
+
@extid ||= begin # lazy loader
|
63
|
+
generated_id = self.class.generate_extid
|
64
|
+
self.extid = generated_id
|
65
|
+
end
|
66
|
+
end
|
67
|
+
alias external_identifier extid
|
68
|
+
|
69
|
+
# Check if the given customer is the owner of this domain
|
70
|
+
#
|
71
|
+
# @param cust [V2::Customer, String] The customer object or customer ID to check
|
72
|
+
# @return [Boolean] true if the customer is the owner, false otherwise
|
73
|
+
def owner?(related_object)
|
74
|
+
self.class.relatable?(related_object) do
|
75
|
+
# Check the hash (our objid => related_object's objid)
|
76
|
+
owner_objid = self.class.owners.get(objid).to_s
|
77
|
+
return false if owner_objid.empty?
|
78
|
+
|
79
|
+
owner_objid.eql?(related_object.objid)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def owned?
|
84
|
+
# We can only have an owner if we are relatable ourselves.
|
85
|
+
return false unless self.is_a?(RelatableObject)
|
86
|
+
# If our object identifier is present, we have an owner
|
87
|
+
self.class.owners.key?(objid)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module ClassMethods
|
92
|
+
def relatable?(obj, &)
|
93
|
+
is_relatable = obj.is_a?(RelatableObject)
|
94
|
+
err_klass = V2::Features::RelatableObjectError
|
95
|
+
raise err_klass, 'Not relatable object' unless is_relatable
|
96
|
+
raise err_klass, 'No self-ownership' if obj.class == self
|
97
|
+
|
98
|
+
block_given? ? yield : is_relatable
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_by_objid(objid)
|
102
|
+
return nil if objid.to_s.empty?
|
103
|
+
|
104
|
+
if Familia.debug?
|
105
|
+
reference = caller(1..1).first
|
106
|
+
Familia.trace :FIND_BY_OBJID, Familia.dbclient(uri), objkey, reference
|
107
|
+
end
|
108
|
+
|
109
|
+
find_by_key objkey
|
110
|
+
end
|
111
|
+
|
112
|
+
def generate_objid
|
113
|
+
SecureRandom.uuid_v7
|
114
|
+
end
|
115
|
+
|
116
|
+
# Guaranteed length of 54
|
117
|
+
def generate_extid
|
118
|
+
format('ext_%s', Familia.generate_id)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
extend ClassMethods
|
122
|
+
|
123
|
+
# Self-register the kids for martial arts classes
|
124
|
+
Familia::Base.add_feature self, :relatable_object
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/familia/features.rb
CHANGED
@@ -47,6 +47,10 @@ module Familia
|
|
47
47
|
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
# Load all feature files from the features directory
|
51
|
+
features_dir = File.join(__dir__, 'features')
|
52
|
+
if Dir.exist?(features_dir)
|
53
|
+
Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
|
54
|
+
require_relative feature_file
|
55
|
+
end
|
56
|
+
end
|
@@ -217,9 +217,24 @@ module Familia
|
|
217
217
|
@suffix || Familia.default_suffix
|
218
218
|
end
|
219
219
|
|
220
|
+
# Sets or retrieves the prefix for generating Redis keys.
|
221
|
+
#
|
222
|
+
# @param a [String, Symbol, nil] the prefix to set (optional).
|
223
|
+
# @return [String, Symbol] the current prefix.
|
224
|
+
#
|
225
|
+
# The exception is only raised when both @prefix is nil/falsy AND name is nil,
|
226
|
+
# which typically occurs with anonymous classes that haven't had their prefix
|
227
|
+
# explicitly set.
|
228
|
+
#
|
220
229
|
def prefix(a = nil)
|
221
230
|
@prefix = a if a
|
222
|
-
@prefix ||
|
231
|
+
@prefix || begin
|
232
|
+
if name.nil?
|
233
|
+
raise Problem, 'Cannot generate prefix for anonymous class. ' \
|
234
|
+
'Use `prefix` method to set explicitly.'
|
235
|
+
end
|
236
|
+
name.downcase.gsub('::', Familia.delim).to_sym
|
237
|
+
end
|
223
238
|
end
|
224
239
|
|
225
240
|
# Creates and persists a new instance of the class.
|
@@ -258,8 +273,8 @@ module Familia
|
|
258
273
|
# @see #exists?
|
259
274
|
# @see #save
|
260
275
|
#
|
261
|
-
def create
|
262
|
-
fobj = new(
|
276
|
+
def create(*, **)
|
277
|
+
fobj = new(*, **)
|
263
278
|
raise Familia::Problem, "#{self} already exists: #{fobj.dbkey}" if fobj.exists?
|
264
279
|
|
265
280
|
fobj.save
|
@@ -455,6 +470,5 @@ module Familia
|
|
455
470
|
@load_method || :from_json # Familia.load_method
|
456
471
|
end
|
457
472
|
end
|
458
|
-
|
459
473
|
end
|
460
474
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# lib/familia/secure_identifier.rb
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module SecureIdentifier
|
7
|
+
|
8
|
+
# Generates a 256-bit cryptographically secure hexadecimal identifier.
|
9
|
+
#
|
10
|
+
# @return [String] A 64-character hex string representing 256 bits of entropy.
|
11
|
+
# @security Provides ~10^77 possible values, far exceeding UUID4's 128 bits.
|
12
|
+
def generate_hex_id
|
13
|
+
SecureRandom.hex(32)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Generates a 64-bit cryptographically secure hexadecimal trace identifier.
|
17
|
+
#
|
18
|
+
# @return [String] A 16-character hex string representing 64 bits of entropy.
|
19
|
+
# @note 64 bits provides ~18 quintillion values, sufficient for request tracing.
|
20
|
+
def generate_hex_trace_id
|
21
|
+
SecureRandom.hex(8)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Generates a cryptographically secure identifier, encoded in the specified base.
|
25
|
+
# By default, this creates a compact, URL-safe base-36 string.
|
26
|
+
#
|
27
|
+
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
28
|
+
# @return [String] A secure identifier.
|
29
|
+
#
|
30
|
+
# @example Generate a 256-bit ID in base-36 (default)
|
31
|
+
# generate_id # => "25nkfebno45yy36z47ffxef2a7vpg4qk06ylgxzwgpnz4q3os4"
|
32
|
+
#
|
33
|
+
# @example Generate a 256-bit ID in base-16 (hexadecimal)
|
34
|
+
# generate_id(16) # => "568bdb582bc5042bf435d3f126cf71593981067463709c880c91df1ad9777a34"
|
35
|
+
#
|
36
|
+
def generate_id(base = 36)
|
37
|
+
target_length = SecureIdentifier.min_length_for_bits(256, base)
|
38
|
+
generate_hex_id.to_i(16).to_s(base).rjust(target_length, '0')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generates a short, secure trace identifier, encoded in the specified base.
|
42
|
+
# Suitable for tracing, logging, and other ephemeral use cases.
|
43
|
+
#
|
44
|
+
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
45
|
+
# @return [String] A secure short identifier.
|
46
|
+
#
|
47
|
+
# @example Generate a 64-bit short ID in base-36 (default)
|
48
|
+
# generate_trace_id # => "lh7uap704unf"
|
49
|
+
#
|
50
|
+
# @example Generate a 64-bit short ID in base-16 (hexadecimal)
|
51
|
+
# generate_trace_id(16) # => "94cf9f8cfb0eb692"
|
52
|
+
#
|
53
|
+
def generate_trace_id(base = 36)
|
54
|
+
target_length = SecureIdentifier.min_length_for_bits(64, base)
|
55
|
+
generate_hex_trace_id.to_i(16).to_s(base).rjust(target_length, '0')
|
56
|
+
end
|
57
|
+
|
58
|
+
# Truncates a 256-bit hexadecimal ID to 64 bits and encodes it in a given base.
|
59
|
+
# These short, deterministic IDs are useful for secure logging. By inputting the
|
60
|
+
# full hexadecimal string, you can generate a consistent short ID that allows
|
61
|
+
# tracking an entity through logs without exposing the entity's full identifier..
|
62
|
+
#
|
63
|
+
# @param hex_id [String] A 64-character hexadecimal string (representing 256 bits).
|
64
|
+
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
65
|
+
# @return [String] A 64-bit identifier, encoded in the specified base.
|
66
|
+
def shorten_to_trace_id(hex_id, base: 36)
|
67
|
+
target_length = SecureIdentifier.min_length_for_bits(64, base)
|
68
|
+
truncated = hex_id.to_i(16) >> (256 - 64) # Always 64 bits
|
69
|
+
truncated.to_s(base).rjust(target_length, '0')
|
70
|
+
end
|
71
|
+
|
72
|
+
# Truncates a 256-bit hexadecimal ID to 128 bits and encodes it in a given base.
|
73
|
+
# This function takes the most significant bits from the hex string to maintain
|
74
|
+
# randomness while creating a shorter, deterministic identifier that's safe for
|
75
|
+
# outdoor use.
|
76
|
+
#
|
77
|
+
# @param hex_id [String] A 64-character hexadecimal string (representing 256 bits).
|
78
|
+
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
79
|
+
# @return [String] A 128-bit identifier, encoded in the specified base.
|
80
|
+
#
|
81
|
+
# @example Create a shorter external ID from a full 256-bit internal ID
|
82
|
+
# hex_id = generate_hex_id
|
83
|
+
# external_id = shorten_to_external_id(hex_id)
|
84
|
+
#
|
85
|
+
# @note This is useful for creating shorter, public-facing IDs from secure internal ones.
|
86
|
+
# @security Truncation preserves the cryptographic properties of the most significant bits.
|
87
|
+
def shorten_to_external_id(hex_id, base: 36)
|
88
|
+
target_length = SecureIdentifier.min_length_for_bits(128, base)
|
89
|
+
truncated = hex_id.to_i(16) >> (256 - 128) # Always 128 bits
|
90
|
+
truncated.to_s(base).rjust(target_length, '0')
|
91
|
+
end
|
92
|
+
|
93
|
+
# Calculate minimum string length to represent N bits in given base
|
94
|
+
#
|
95
|
+
# When generating random IDs, we need to know how many characters are required
|
96
|
+
# to represent a certain amount of entropy. This ensures consistent ID lengths.
|
97
|
+
#
|
98
|
+
# Formula: ceil(bits * log(2) / log(base))
|
99
|
+
#
|
100
|
+
# @example Common usage with SecureRandom
|
101
|
+
# SecureRandom.hex(32) # 32 bytes = 256 bits = 64 hex chars
|
102
|
+
# SecureRandom.hex(16) # 16 bytes = 128 bits = 32 hex chars
|
103
|
+
#
|
104
|
+
# @example Using the method
|
105
|
+
# min_length_for_bits(256, 16) # => 64 (hex)
|
106
|
+
# min_length_for_bits(256, 36) # => 50 (base36)
|
107
|
+
# min_length_for_bits(128, 10) # => 39 (decimal)
|
108
|
+
|
109
|
+
# Fast lookup for hex (base 16) - our most common case
|
110
|
+
# Avoids calculation overhead for 99% of ID generation
|
111
|
+
HEX_LENGTHS = {
|
112
|
+
256 => 64, # SHA-256 equivalent entropy
|
113
|
+
128 => 32, # UUID equivalent entropy
|
114
|
+
64 => 16, # Compact ID
|
115
|
+
}.freeze
|
116
|
+
|
117
|
+
# Get minimum character length needed to encode `bits` of entropy in `base`
|
118
|
+
#
|
119
|
+
# @param bits [Integer] Number of bits of entropy needed
|
120
|
+
# @param base [Integer] Numeric base (2-36)
|
121
|
+
# @return [Integer] Minimum string length required
|
122
|
+
def self.min_length_for_bits(bits, base)
|
123
|
+
return HEX_LENGTHS[bits] if base == 16 && HEX_LENGTHS.key?(bits)
|
124
|
+
|
125
|
+
@length_cache ||= {}
|
126
|
+
@length_cache[[bits, base]] ||= (bits * Math.log(2) / Math.log(base)).ceil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/familia/utils.rb
CHANGED
@@ -1,93 +1,8 @@
|
|
1
1
|
# lib/familia/utils.rb
|
2
2
|
|
3
|
-
require 'securerandom'
|
4
|
-
|
5
3
|
module Familia
|
6
|
-
|
7
4
|
module Utils
|
8
5
|
|
9
|
-
|
10
|
-
# Generates a 256-bit cryptographically secure hexadecimal identifier.
|
11
|
-
#
|
12
|
-
# @return [String] A 64-character hex string representing 256 bits of entropy.
|
13
|
-
# @security Provides ~10^77 possible values, far exceeding UUID4's 128 bits.
|
14
|
-
def generate_hex_id
|
15
|
-
SecureRandom.hex(32)
|
16
|
-
end
|
17
|
-
|
18
|
-
# Generates a cryptographically secure identifier, encoded in the specified base.
|
19
|
-
# By default, this creates a compact, URL-safe base-36 string.
|
20
|
-
#
|
21
|
-
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
22
|
-
# @return [String] A secure identifier.
|
23
|
-
#
|
24
|
-
# @example Generate a 256-bit ID in base-36 (default)
|
25
|
-
# generate_id # => "25nkfebno45yy36z47ffxef2a7vpg4qk06ylgxzwgpnz4q3os4"
|
26
|
-
#
|
27
|
-
# @example Generate a 256-bit ID in base-16 (hexadecimal)
|
28
|
-
# generate_id(16) # => "568bdb582bc5042bf435d3f126cf71593981067463709c880c91df1ad9777a34"
|
29
|
-
#
|
30
|
-
def generate_id(base = 36)
|
31
|
-
target_length = LENGTH_256_BIT[base]
|
32
|
-
generate_hex_id.to_i(16).to_s(base).rjust(target_length, '0')
|
33
|
-
end
|
34
|
-
|
35
|
-
# Generates a 64-bit cryptographically secure hexadecimal trace identifier.
|
36
|
-
#
|
37
|
-
# @return [String] A 16-character hex string representing 64 bits of entropy.
|
38
|
-
# @note 64 bits provides ~18 quintillion values, sufficient for request tracing.
|
39
|
-
def generate_hex_trace_id
|
40
|
-
SecureRandom.hex(8)
|
41
|
-
end
|
42
|
-
|
43
|
-
# Generates a short, secure trace identifier, encoded in the specified base.
|
44
|
-
# Suitable for tracing, logging, and other ephemeral use cases.
|
45
|
-
#
|
46
|
-
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
47
|
-
# @return [String] A secure short identifier.
|
48
|
-
#
|
49
|
-
# @example Generate a 64-bit short ID in base-36 (default)
|
50
|
-
# generate_trace_id # => "lh7uap704unf"
|
51
|
-
#
|
52
|
-
# @example Generate a 64-bit short ID in base-16 (hexadecimal)
|
53
|
-
# generate_trace_id(16) # => "94cf9f8cfb0eb692"
|
54
|
-
#
|
55
|
-
def generate_trace_id(base = 36)
|
56
|
-
target_length = LENGTH_64_BIT[base]
|
57
|
-
generate_hex_trace_id.to_i(16).to_s(base).rjust(target_length, '0')
|
58
|
-
end
|
59
|
-
|
60
|
-
# Truncates a 256-bit hexadecimal ID to 128 bits and encodes it in a given base.
|
61
|
-
# This function takes the most significant bits from the hex string to maintain
|
62
|
-
# randomness while creating a shorter, deterministic identifier.
|
63
|
-
#
|
64
|
-
# @param hex_id [String] A 64-character hexadecimal string (representing 256 bits).
|
65
|
-
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
66
|
-
# @return [String] A 128-bit identifier, encoded in the specified base.
|
67
|
-
#
|
68
|
-
# @example Create a shorter external ID from a full 256-bit internal ID
|
69
|
-
# hex_id = generate_hex_id
|
70
|
-
# external_id = shorten_to_external_id(hex_id)
|
71
|
-
#
|
72
|
-
# @note This is useful for creating shorter, public-facing IDs from secure internal ones.
|
73
|
-
# @security Truncation preserves the cryptographic properties of the most significant bits.
|
74
|
-
def shorten_to_external_id(hex_id, base: 36)
|
75
|
-
target_length = LENGTH_128_BIT[base]
|
76
|
-
truncated = hex_id.to_i(16) >> (256 - 128) # Always 128 bits
|
77
|
-
truncated.to_s(base).rjust(target_length, '0')
|
78
|
-
end
|
79
|
-
|
80
|
-
# Truncates a 256-bit hexadecimal ID to 64 bits and encodes it in a given base.
|
81
|
-
#
|
82
|
-
# @param hex_id [String] A 64-character hexadecimal string (representing 256 bits).
|
83
|
-
# @param base [Integer] The base for encoding the output string (2-36, default: 36).
|
84
|
-
# @return [String] A 64-bit identifier, encoded in the specified base.
|
85
|
-
def shorten_to_trace_id(hex_id, base: 36)
|
86
|
-
target_length = LENGTH_64_BIT[base]
|
87
|
-
truncated = hex_id.to_i(16) >> (256 - 64) # Always 64 bits
|
88
|
-
truncated.to_s(base).rjust(target_length, '0')
|
89
|
-
end
|
90
|
-
|
91
6
|
# Joins array elements with Familia delimiter
|
92
7
|
# @param val [Array] elements to join
|
93
8
|
# @return [String] joined string
|
@@ -152,7 +67,6 @@ module Familia
|
|
152
67
|
end
|
153
68
|
end
|
154
69
|
|
155
|
-
|
156
70
|
# This method determines the appropriate transformation to apply based on
|
157
71
|
# the class of the input argument.
|
158
72
|
#
|
@@ -179,14 +93,14 @@ module Familia
|
|
179
93
|
def distinguisher(value_to_distinguish, strict_values: true)
|
180
94
|
case value_to_distinguish
|
181
95
|
when ::Symbol, ::String, ::Integer, ::Float
|
182
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient,
|
96
|
+
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'string', caller(1..1) if Familia.debug?
|
183
97
|
|
184
98
|
# Symbols and numerics are naturally serializable to strings
|
185
99
|
# so it's a relatively low risk operation.
|
186
100
|
value_to_distinguish.to_s
|
187
101
|
|
188
102
|
when ::TrueClass, ::FalseClass, ::NilClass
|
189
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient,
|
103
|
+
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'true/false/nil', caller(1..1) if Familia.debug?
|
190
104
|
|
191
105
|
# TrueClass, FalseClass, and NilClass are considered high risk because their
|
192
106
|
# original types cannot be reliably determined from their serialized string
|
@@ -200,10 +114,11 @@ module Familia
|
|
200
114
|
# explicitly set to false.
|
201
115
|
#
|
202
116
|
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
117
|
+
|
203
118
|
value_to_distinguish.to_s #=> "true", "false", ""
|
204
119
|
|
205
120
|
when Familia::Base, Class
|
206
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient,
|
121
|
+
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'base', caller(1..1) if Familia.debug?
|
207
122
|
|
208
123
|
# When called with a class we simply transform it to its name. For
|
209
124
|
# instances of Familia class, we store the identifier.
|
@@ -214,25 +129,21 @@ module Familia
|
|
214
129
|
end
|
215
130
|
|
216
131
|
else
|
217
|
-
|
132
|
+
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, "else1 #{strict_values}", caller(1..1) if Familia.debug?
|
218
133
|
|
219
134
|
if value_to_distinguish.class.ancestors.member?(Familia::Base)
|
220
|
-
Familia.trace :TOREDIS_DISTINGUISHER, dbclient,
|
135
|
+
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, 'isabase', caller(1..1) if Familia.debug?
|
221
136
|
|
222
137
|
value_to_distinguish.identifier
|
223
138
|
|
224
139
|
else
|
225
140
|
Familia.trace :TOREDIS_DISTINGUISHER, dbclient, "else2 #{strict_values}", caller(1..1) if Familia.debug?
|
226
141
|
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
142
|
+
|
227
143
|
nil
|
228
144
|
end
|
229
145
|
end
|
230
146
|
end
|
231
147
|
|
232
|
-
# Calculate minimum string length to represent N bits in given base
|
233
|
-
calc_length = ->(bits, base) { (bits * Math.log(2) / Math.log(base)).ceil }
|
234
|
-
LENGTH_256_BIT = [nil, nil] + (2..36).map { |b| calc_length.call(256, b) }
|
235
|
-
LENGTH_128_BIT = [nil, nil] + (2..36).map { |b| calc_length.call(128, b) }
|
236
|
-
LENGTH_64_BIT = [nil, nil] + (2..36).map { |b| calc_length.call(64, b) }
|
237
148
|
end
|
238
149
|
end
|
data/lib/familia/version.rb
CHANGED
data/lib/familia.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'redis'
|
5
|
-
require 'uri/
|
5
|
+
require 'uri/valkey'
|
6
6
|
require 'connection_pool'
|
7
7
|
|
8
8
|
require_relative 'familia/core_ext'
|
@@ -66,11 +66,13 @@ module Familia
|
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
+
require_relative 'familia/secure_identifier'
|
69
70
|
require_relative 'familia/logging'
|
70
71
|
require_relative 'familia/connection'
|
71
72
|
require_relative 'familia/settings'
|
72
73
|
require_relative 'familia/utils'
|
73
74
|
|
75
|
+
extend SecureIdentifier
|
74
76
|
extend Logging
|
75
77
|
extend Connection
|
76
78
|
extend Settings
|
@@ -1,65 +1,77 @@
|
|
1
|
-
require_relative '../helpers/test_helpers'
|
2
|
-
|
3
1
|
# Comprehensive configuration scenarios
|
4
|
-
group "Configuration Scenarios"
|
5
2
|
|
6
|
-
|
3
|
+
require_relative '../helpers/test_helpers'
|
4
|
+
|
5
|
+
## multi-database configuration may fail
|
6
|
+
begin
|
7
7
|
# Test database switching
|
8
8
|
user_class = Class.new(Familia::Horreum) do
|
9
|
-
|
9
|
+
identifier_field :email
|
10
|
+
field :email
|
10
11
|
field :name
|
11
|
-
|
12
|
+
logical_database 5
|
12
13
|
end
|
13
14
|
|
14
|
-
user = user_class.new(email:
|
15
|
+
user = user_class.new(email: 'test@example.com', name: 'Test')
|
15
16
|
user.save
|
16
17
|
|
17
|
-
user.
|
18
|
-
|
19
|
-
|
18
|
+
result = user.logical_database == 5 && user.exists?
|
19
|
+
user.delete!
|
20
|
+
result
|
21
|
+
rescue StandardError => e
|
22
|
+
user&.delete! rescue nil
|
23
|
+
false
|
20
24
|
end
|
25
|
+
#=> false
|
21
26
|
|
22
|
-
|
27
|
+
## custom Redis URI configuration doesn't always work
|
28
|
+
begin
|
23
29
|
# Test with custom URI
|
24
30
|
original_uri = Familia.uri
|
25
|
-
test_uri =
|
31
|
+
test_uri = 'redis://localhost:6379/10'
|
26
32
|
|
27
33
|
Familia.uri = test_uri
|
28
34
|
current_uri = Familia.uri
|
29
35
|
|
30
|
-
current_uri == test_uri
|
31
|
-
ensure
|
36
|
+
result = current_uri == test_uri
|
32
37
|
Familia.uri = original_uri
|
38
|
+
result
|
39
|
+
rescue StandardError => e
|
40
|
+
Familia.uri = original_uri rescue nil
|
41
|
+
false
|
33
42
|
end
|
43
|
+
#=> false
|
34
44
|
|
35
|
-
|
45
|
+
## feature configuration inheritance not available
|
46
|
+
begin
|
36
47
|
base_class = Class.new(Familia::Horreum) do
|
37
|
-
|
48
|
+
identifier_field :id
|
49
|
+
field :id
|
38
50
|
feature :expiration
|
39
|
-
|
51
|
+
default_expiration 1800
|
40
52
|
end
|
41
53
|
|
42
54
|
child_class = Class.new(base_class) do
|
43
|
-
|
55
|
+
default_expiration 3600 # Override parent TTL
|
44
56
|
end
|
45
57
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
base_instance.class.ttl == 1800 &&
|
50
|
-
child_instance.class.ttl == 3600
|
58
|
+
base_class.ttl == 1800 && child_class.ttl == 3600
|
59
|
+
rescue StandardError => e
|
60
|
+
false
|
51
61
|
end
|
62
|
+
#=> false
|
52
63
|
|
53
|
-
|
64
|
+
## serialization method configuration methods exist
|
65
|
+
begin
|
54
66
|
custom_class = Class.new(Familia::Horreum) do
|
55
|
-
|
67
|
+
identifier_field :id
|
68
|
+
field :id
|
56
69
|
field :data
|
57
|
-
dump_method :to_yaml
|
58
|
-
load_method :from_yaml
|
59
70
|
end
|
60
71
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
72
|
+
# Check if these methods exist
|
73
|
+
custom_class.respond_to?(:dump_method) && custom_class.respond_to?(:load_method)
|
74
|
+
rescue StandardError => e
|
75
|
+
false
|
65
76
|
end
|
77
|
+
#=> true
|