familia 2.0.0.pre17 → 2.0.0.pre18
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/CHANGELOG.rst +60 -0
- data/CLAUDE.md +9 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
- data/lib/familia/data_type/serialization.rb +5 -5
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/horreum/database_commands.rb +1 -1
- data/lib/familia/horreum/definition.rb +6 -37
- data/lib/familia/horreum/management.rb +17 -12
- data/lib/familia/horreum/persistence.rb +1 -1
- data/lib/familia/horreum/serialization.rb +91 -73
- data/lib/familia/horreum.rb +10 -6
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +271 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +6 -1
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +1 -1
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +1 -1
- data/try/integration/cross_component_try.rb +1 -1
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +1 -1
- data/try/{core → integration}/persistence_operations_try.rb +1 -1
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +1 -1
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
- data/try/{horreum → unit/horreum}/base_try.rb +3 -3
- data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +134 -125
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -0,0 +1,180 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Benchmark: Field deserialization strategies in find_by_dbkey
|
4
|
+
#
|
5
|
+
# Compares different approaches to deserializing Redis hash values
|
6
|
+
# when loading Horreum objects from the database.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# $ ruby try/support/benchmarks/deserialization_benchmark.rb
|
11
|
+
# ======================================================================
|
12
|
+
# RESULTS SUMMARY (baseline: direct assignment)
|
13
|
+
# ======================================================================
|
14
|
+
#
|
15
|
+
# Baseline (no deserialization): 0.151312s
|
16
|
+
#
|
17
|
+
# Deserialization strategies:
|
18
|
+
# 1. Selective (only JSON-like strings): 0.174016s (1.15x baseline, +15.0% overhead) 🏆 FASTEST
|
19
|
+
# 2. Bulk JSON round-trip (parse/dump): 0.183926s (1.22x baseline, +21.6% overhead)
|
20
|
+
# 3. Cached instance + transform: 0.320223s (2.12x baseline, +111.6% overhead)
|
21
|
+
# 4. Current (field-by-field transform_values): 0.49568s (3.28x baseline, +227.6% overhead)
|
22
|
+
|
23
|
+
require_relative '../../../lib/familia'
|
24
|
+
require 'benchmark'
|
25
|
+
require 'json'
|
26
|
+
|
27
|
+
# Setup Redis connection
|
28
|
+
Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
|
29
|
+
|
30
|
+
# Sample model with various field types
|
31
|
+
class BenchmarkUser < Familia::Horreum
|
32
|
+
identifier_field :user_id
|
33
|
+
|
34
|
+
field :user_id
|
35
|
+
field :name
|
36
|
+
field :email
|
37
|
+
field :age
|
38
|
+
field :active
|
39
|
+
field :metadata # Will store JSON hash
|
40
|
+
field :tags # Will store JSON array
|
41
|
+
field :created_at # Will store timestamp
|
42
|
+
field :score # Will store float
|
43
|
+
field :simple_string
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create sample data with realistic values
|
47
|
+
sample_data = {
|
48
|
+
'user_id' => 'user_12345',
|
49
|
+
'name' => 'John Doe',
|
50
|
+
'email' => 'john.doe@example.com',
|
51
|
+
'age' => '35',
|
52
|
+
'active' => 'true',
|
53
|
+
'metadata' => '{"role":"admin","department":"engineering","level":5}',
|
54
|
+
'tags' => '["ruby","redis","performance","optimization"]',
|
55
|
+
'created_at' => Time.now.to_i.to_s,
|
56
|
+
'score' => '98.7',
|
57
|
+
'simple_string' => 'Just a plain string value',
|
58
|
+
}
|
59
|
+
|
60
|
+
# Persist sample data to Redis
|
61
|
+
user = BenchmarkUser.new(**sample_data)
|
62
|
+
user.save
|
63
|
+
|
64
|
+
# Get the raw hash data directly from Redis (what find_by_dbkey gets)
|
65
|
+
raw_hash = BenchmarkUser.dbclient.hgetall(user.dbkey)
|
66
|
+
|
67
|
+
puts 'Benchmarking deserialization strategies'
|
68
|
+
puts "Sample data fields: #{raw_hash.keys.size}"
|
69
|
+
puts "Raw hash: #{raw_hash.inspect}"
|
70
|
+
puts "\n"
|
71
|
+
|
72
|
+
# Strategy 1: Current field-by-field with transform_values
|
73
|
+
def strategy_current(fields, klass)
|
74
|
+
deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
|
75
|
+
klass.new(**deserialized)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Strategy 2: Bulk JSON round-trip
|
79
|
+
def strategy_bulk_json(fields, klass)
|
80
|
+
parsed = JSON.parse(JSON.dump(fields))
|
81
|
+
klass.new(**parsed)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Strategy 3: Direct assignment without deserialization
|
85
|
+
def strategy_direct(fields, klass)
|
86
|
+
klass.new(**fields)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Strategy 4: Selective deserialization (only JSON-looking strings)
|
90
|
+
def strategy_selective(fields, klass)
|
91
|
+
deserialized = fields.transform_values do |value|
|
92
|
+
if value.to_s.start_with?('{', '[')
|
93
|
+
begin
|
94
|
+
JSON.parse(value, symbolize_names: true)
|
95
|
+
rescue JSON::ParserError
|
96
|
+
value
|
97
|
+
end
|
98
|
+
else
|
99
|
+
value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
klass.new(**deserialized)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Strategy 5: Cached instance + transform
|
106
|
+
def strategy_cached_instance(fields, klass)
|
107
|
+
instance = klass.new
|
108
|
+
deserialized = fields.transform_values { |value| instance.deserialize_value(value) }
|
109
|
+
klass.new(**deserialized)
|
110
|
+
end
|
111
|
+
|
112
|
+
iterations = 10_000
|
113
|
+
|
114
|
+
puts "Running #{iterations} iterations per strategy...\n\n"
|
115
|
+
|
116
|
+
strategies = {
|
117
|
+
'Current (field-by-field transform_values)' => :strategy_current,
|
118
|
+
'Bulk JSON round-trip (parse/dump)' => :strategy_bulk_json,
|
119
|
+
'Direct (no deserialization)' => :strategy_direct,
|
120
|
+
'Selective (only parse JSON-like strings)' => :strategy_selective,
|
121
|
+
'Cached instance + transform' => :strategy_cached_instance,
|
122
|
+
}
|
123
|
+
|
124
|
+
results = {}
|
125
|
+
|
126
|
+
strategies.each do |name, method_name|
|
127
|
+
time = Benchmark.measure do
|
128
|
+
iterations.times do
|
129
|
+
send(method_name, raw_hash, BenchmarkUser)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
results[name] = time.real
|
133
|
+
puts "#{name}: #{time.real.round(6)} seconds (#{(iterations / time.real).round(0)} ops/sec)"
|
134
|
+
end
|
135
|
+
|
136
|
+
puts "\n" + ('=' * 70)
|
137
|
+
puts 'RESULTS SUMMARY (baseline: direct assignment)'
|
138
|
+
puts '=' * 70
|
139
|
+
|
140
|
+
# Use direct assignment as baseline (obviously fastest but incorrect)
|
141
|
+
baseline = results['Direct (no deserialization)']
|
142
|
+
|
143
|
+
# Sort deserialization strategies only (exclude baseline)
|
144
|
+
deserialization_strategies = results.reject { |name, _| name == 'Direct (no deserialization)' }
|
145
|
+
sorted = deserialization_strategies.sort_by { |_, time| time }
|
146
|
+
|
147
|
+
puts "
|
148
|
+
Baseline (no deserialization): #{baseline.round(6)}s"
|
149
|
+
puts "
|
150
|
+
Deserialization strategies:
|
151
|
+
"
|
152
|
+
|
153
|
+
sorted.each_with_index do |(name, time), index|
|
154
|
+
overhead = ((time / baseline - 1) * 100).round(1)
|
155
|
+
vs_baseline = (time / baseline).round(2)
|
156
|
+
marker = index == 0 ? '🏆 FASTEST' : ''
|
157
|
+
puts "#{index + 1}. #{name}: #{time.round(6)}s (#{vs_baseline}x baseline, +#{overhead}% overhead) #{marker}"
|
158
|
+
end
|
159
|
+
|
160
|
+
puts "
|
161
|
+
" + ('=' * 70)
|
162
|
+
puts 'RECOMMENDATIONS'
|
163
|
+
puts '=' * 70
|
164
|
+
|
165
|
+
puts 'Best strategy depends on your data:'
|
166
|
+
puts ' • Mostly simple strings → Direct or Selective'
|
167
|
+
puts ' • Mixed types with JSON → Current (field-by-field)'
|
168
|
+
puts ' • Heavy JSON payloads → Consider lazy deserialization'
|
169
|
+
|
170
|
+
# Cleanup
|
171
|
+
user.destroy!
|
172
|
+
|
173
|
+
__END__
|
174
|
+
|
175
|
+
# Example output expectations:
|
176
|
+
#
|
177
|
+
# Current approach should be moderately fast
|
178
|
+
# Bulk JSON round-trip should be slower (extra serialization step)
|
179
|
+
# Direct assignment should be fastest but incorrect for complex types
|
180
|
+
# Selective should be fast for simple data, slower for JSON-heavy data
|
@@ -0,0 +1,237 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Correctness Test: Field deserialization strategies
|
4
|
+
#
|
5
|
+
# Verifies that different deserialization approaches produce identical
|
6
|
+
# results for all field types (strings, numbers, JSON, nested structures).
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
# $ try/support/benchmarks/deserialization_correctness_test.rb
|
10
|
+
#
|
11
|
+
|
12
|
+
require_relative '../../../lib/familia'
|
13
|
+
require 'json'
|
14
|
+
|
15
|
+
# Setup Redis connection
|
16
|
+
Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
|
17
|
+
|
18
|
+
# Sample model with various field types
|
19
|
+
class CorrectnessTestUser < Familia::Horreum
|
20
|
+
identifier_field :user_id
|
21
|
+
|
22
|
+
field :user_id
|
23
|
+
field :name
|
24
|
+
field :email
|
25
|
+
field :age
|
26
|
+
field :active
|
27
|
+
field :metadata # Will store JSON hash
|
28
|
+
field :tags # Will store JSON array
|
29
|
+
field :created_at # Will store timestamp
|
30
|
+
field :score # Will store float
|
31
|
+
field :simple_string
|
32
|
+
field :nested_data # Will store deeply nested JSON
|
33
|
+
field :empty_string
|
34
|
+
field :nil_value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create sample data with comprehensive test cases
|
38
|
+
sample_data = {
|
39
|
+
'user_id' => 'user_12345',
|
40
|
+
'name' => 'John Doe',
|
41
|
+
'email' => 'john.doe@example.com',
|
42
|
+
'age' => '35',
|
43
|
+
'active' => 'true',
|
44
|
+
'metadata' => '{"role":"admin","department":"engineering","level":5}',
|
45
|
+
'tags' => '["ruby","redis","performance","optimization"]',
|
46
|
+
'created_at' => Time.now.to_i.to_s,
|
47
|
+
'score' => '98.7',
|
48
|
+
'simple_string' => 'Just a plain string value',
|
49
|
+
'nested_data' => '{"user":{"profile":{"settings":{"theme":"dark","notifications":true}}}}',
|
50
|
+
'empty_string' => '',
|
51
|
+
'nil_value' => nil,
|
52
|
+
}
|
53
|
+
|
54
|
+
# Persist sample data to Redis
|
55
|
+
user = CorrectnessTestUser.new(**sample_data)
|
56
|
+
user.save
|
57
|
+
|
58
|
+
# Get the raw hash data directly from Redis
|
59
|
+
raw_hash = CorrectnessTestUser.dbclient.hgetall(user.dbkey)
|
60
|
+
|
61
|
+
puts 'Correctness Test: Deserialization Strategies'
|
62
|
+
puts '=' * 70
|
63
|
+
puts "\nTesting with #{raw_hash.keys.size} fields"
|
64
|
+
puts "\n"
|
65
|
+
|
66
|
+
# Strategy 1: Current field-by-field (reference implementation)
|
67
|
+
def strategy_current(fields, klass)
|
68
|
+
deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
|
69
|
+
klass.new(**deserialized)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Strategy 2: Bulk JSON round-trip
|
73
|
+
def strategy_bulk_json(fields, klass)
|
74
|
+
parsed = JSON.parse(JSON.dump(fields))
|
75
|
+
klass.new(**parsed)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Strategy 3: Selective deserialization (only JSON-looking strings)
|
79
|
+
def strategy_selective(fields, klass)
|
80
|
+
deserialized = fields.transform_values do |value|
|
81
|
+
if value.to_s.start_with?('{', '[')
|
82
|
+
begin
|
83
|
+
JSON.parse(value, symbolize_names: true)
|
84
|
+
rescue JSON::ParserError
|
85
|
+
value
|
86
|
+
end
|
87
|
+
else
|
88
|
+
value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
klass.new(**deserialized)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create objects using each strategy
|
95
|
+
current_obj = strategy_current(raw_hash, CorrectnessTestUser)
|
96
|
+
bulk_obj = strategy_bulk_json(raw_hash, CorrectnessTestUser)
|
97
|
+
selective_obj = strategy_selective(raw_hash, CorrectnessTestUser)
|
98
|
+
|
99
|
+
# Test helper to compare field values
|
100
|
+
def compare_values(field, current_val, test_val, strategy_name)
|
101
|
+
current_class = current_val.class
|
102
|
+
test_class = test_val.class
|
103
|
+
|
104
|
+
if current_val == test_val && current_class == test_class
|
105
|
+
{ status: :pass, field: field, strategy: strategy_name }
|
106
|
+
else
|
107
|
+
{
|
108
|
+
status: :fail,
|
109
|
+
field: field,
|
110
|
+
strategy: strategy_name,
|
111
|
+
current: { value: current_val.inspect, class: current_class },
|
112
|
+
test: { value: test_val.inspect, class: test_class },
|
113
|
+
}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Run correctness tests
|
118
|
+
results = []
|
119
|
+
fields_to_test = CorrectnessTestUser.fields
|
120
|
+
|
121
|
+
puts "Testing #{fields_to_test.size} fields across strategies...\n\n"
|
122
|
+
|
123
|
+
# Test Bulk JSON strategy
|
124
|
+
puts 'Strategy: Bulk JSON round-trip'
|
125
|
+
puts '-' * 70
|
126
|
+
fields_to_test.each do |field|
|
127
|
+
current_val = current_obj.send(field)
|
128
|
+
test_val = bulk_obj.send(field)
|
129
|
+
result = compare_values(field, current_val, test_val, 'Bulk JSON')
|
130
|
+
results << result
|
131
|
+
|
132
|
+
if result[:status] == :pass
|
133
|
+
puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
|
134
|
+
else
|
135
|
+
puts " ✗ #{field}: MISMATCH"
|
136
|
+
puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
|
137
|
+
puts " Bulk: #{result[:test][:value]} (#{result[:test][:class]})"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
puts "\n"
|
142
|
+
|
143
|
+
# Test Selective strategy
|
144
|
+
puts 'Strategy: Selective (only JSON-like strings)'
|
145
|
+
puts '-' * 70
|
146
|
+
fields_to_test.each do |field|
|
147
|
+
current_val = current_obj.send(field)
|
148
|
+
test_val = selective_obj.send(field)
|
149
|
+
result = compare_values(field, current_val, test_val, 'Selective')
|
150
|
+
results << result
|
151
|
+
|
152
|
+
if result[:status] == :pass
|
153
|
+
puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
|
154
|
+
else
|
155
|
+
puts " ✗ #{field}: MISMATCH"
|
156
|
+
puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
|
157
|
+
puts " Selective: #{result[:test][:value]} (#{result[:test][:class]})"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
puts "\n" + ('=' * 70)
|
162
|
+
puts 'SUMMARY'
|
163
|
+
puts '=' * 70
|
164
|
+
|
165
|
+
# Group results by strategy
|
166
|
+
by_strategy = results.group_by { |r| r[:strategy] }
|
167
|
+
|
168
|
+
by_strategy.each do |strategy, strategy_results|
|
169
|
+
passed = strategy_results.count { |r| r[:status] == :pass }
|
170
|
+
failed = strategy_results.count { |r| r[:status] == :fail }
|
171
|
+
total = strategy_results.size
|
172
|
+
|
173
|
+
status_icon = failed == 0 ? '✓' : '✗'
|
174
|
+
puts "\n#{status_icon} #{strategy}: #{passed}/#{total} passed"
|
175
|
+
|
176
|
+
next unless failed > 0
|
177
|
+
|
178
|
+
puts ' Failed fields:'
|
179
|
+
strategy_results.select { |r| r[:status] == :fail }.each do |result|
|
180
|
+
puts " - #{result[:field]}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Overall assessment
|
185
|
+
all_passed = results.all? { |r| r[:status] == :pass }
|
186
|
+
|
187
|
+
puts "\n" + ('=' * 70)
|
188
|
+
puts 'VERDICT'
|
189
|
+
puts '=' * 70
|
190
|
+
|
191
|
+
if all_passed
|
192
|
+
puts '✓ All strategies produce identical results to current implementation'
|
193
|
+
puts ' Safe to use for optimization'
|
194
|
+
else
|
195
|
+
puts '✗ Some strategies produce different results'
|
196
|
+
puts ' Review failures before implementing'
|
197
|
+
end
|
198
|
+
|
199
|
+
puts "\n" + ('=' * 70)
|
200
|
+
puts 'RECOMMENDATIONS'
|
201
|
+
puts '=' * 70
|
202
|
+
|
203
|
+
# Analyze which strategies passed
|
204
|
+
bulk_passed = by_strategy['Bulk JSON'].all? { |r| r[:status] == :pass }
|
205
|
+
selective_passed = by_strategy['Selective'].all? { |r| r[:status] == :pass }
|
206
|
+
|
207
|
+
if bulk_passed && selective_passed
|
208
|
+
puts '✓ Both Bulk JSON and Selective strategies are correct'
|
209
|
+
puts ' → Use Selective for best performance (+15-18% overhead)'
|
210
|
+
puts ' → Use Bulk JSON for simplicity (+20% overhead)'
|
211
|
+
elsif bulk_passed
|
212
|
+
puts '✓ Bulk JSON strategy is correct'
|
213
|
+
puts ' → Safe to implement (+20% overhead vs baseline)'
|
214
|
+
elsif selective_passed
|
215
|
+
puts '✓ Selective strategy is correct'
|
216
|
+
puts ' → Safe to implement (+15-18% overhead vs baseline)'
|
217
|
+
else
|
218
|
+
puts '✗ No alternative strategies passed all tests'
|
219
|
+
puts ' → Stick with current implementation'
|
220
|
+
puts ' → Or fix identified issues in failing strategies'
|
221
|
+
end
|
222
|
+
|
223
|
+
# Cleanup
|
224
|
+
user.destroy!
|
225
|
+
|
226
|
+
__END__
|
227
|
+
|
228
|
+
# Expected output:
|
229
|
+
#
|
230
|
+
# All strategies should pass if they correctly handle:
|
231
|
+
# - Simple strings (no parsing needed)
|
232
|
+
# - JSON objects (hashes)
|
233
|
+
# - JSON arrays
|
234
|
+
# - Numbers as strings
|
235
|
+
# - Empty strings
|
236
|
+
# - Nil values
|
237
|
+
# - Nested JSON structures
|
@@ -6,12 +6,21 @@
|
|
6
6
|
|
7
7
|
require 'digest'
|
8
8
|
|
9
|
-
require_relative '
|
9
|
+
require_relative '../../../lib/familia'
|
10
10
|
|
11
11
|
Familia.enable_database_logging = true
|
12
12
|
Familia.enable_database_counter = true
|
13
13
|
Familia.uri = 'redis://127.0.0.1:2525'
|
14
14
|
|
15
|
+
def generate_random_email
|
16
|
+
# Generate a random username
|
17
|
+
username = (0...8).map { ('a'..'z').to_a[rand(26)] }.join
|
18
|
+
# Define a domain
|
19
|
+
domain = "example.com"
|
20
|
+
# Combine to form an email address
|
21
|
+
"#{username}@#{domain}"
|
22
|
+
end
|
23
|
+
|
15
24
|
class Bone < Familia::Horreum
|
16
25
|
using Familia::Refinements::TimeLiterals
|
17
26
|
|
@@ -194,7 +203,7 @@ end
|
|
194
203
|
#
|
195
204
|
# NOTE: This will do nothing unless RedactedString is already requried
|
196
205
|
unless defined?(RedactedString)
|
197
|
-
require_relative '
|
206
|
+
require_relative '../../../lib/familia/features/transient_fields/redacted_string'
|
198
207
|
end
|
199
208
|
module RedactedStringTestHelper
|
200
209
|
refine RedactedString do
|
@@ -206,7 +215,7 @@ module RedactedStringTestHelper
|
|
206
215
|
end
|
207
216
|
|
208
217
|
unless defined?(SingleUseRedactedString)
|
209
|
-
require_relative '
|
218
|
+
require_relative '../../../lib/familia/features/transient_fields/single_use_redacted_string'
|
210
219
|
end
|
211
220
|
module SingleUseRedactedStringTestHelper
|
212
221
|
refine SingleUseRedactedString do
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/core/base_enhancements_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -97,14 +97,6 @@ EmptyBaseTest.respond_to?(:feature)
|
|
97
97
|
Familia::Base.features_available
|
98
98
|
#=:> Hash
|
99
99
|
|
100
|
-
## Dump and load methods are set
|
101
|
-
Familia::Base.dump_method
|
102
|
-
#=> :to_json
|
103
|
-
|
104
|
-
## Load method is set correctly
|
105
|
-
Familia::Base.load_method
|
106
|
-
#=> :from_json
|
107
|
-
|
108
100
|
## Base module provides inspect with class name
|
109
101
|
@base_uuid.inspect.include?('BaseUuidTest')
|
110
102
|
#=> true
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# try/unit/core/familia_logger_try.rb
|
2
|
+
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
|
+
require 'logger'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
## FamiliaLogger has trace method
|
8
|
+
logger = Familia::FamiliaLogger.new(StringIO.new)
|
9
|
+
logger.respond_to?(:trace)
|
10
|
+
#=> true
|
11
|
+
|
12
|
+
## trace method logs with TRACE level
|
13
|
+
output = StringIO.new
|
14
|
+
logger = Familia::FamiliaLogger.new(output)
|
15
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
16
|
+
logger.trace('Test message')
|
17
|
+
output.string
|
18
|
+
#=~> /Test message/
|
19
|
+
|
20
|
+
## FamiliaLogger has TRACE constant
|
21
|
+
Familia::FamiliaLogger::TRACE
|
22
|
+
#=> 0
|
23
|
+
|
24
|
+
## trace method accepts progname parameter
|
25
|
+
output = StringIO.new
|
26
|
+
logger = Familia::FamiliaLogger.new(output)
|
27
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
28
|
+
logger.trace('MyApp') { 'Test message' }
|
29
|
+
output.string
|
30
|
+
#=~> /MyApp/
|
31
|
+
#=~> /Test message/
|
32
|
+
|
33
|
+
## trace method accepts block for message
|
34
|
+
output = StringIO.new
|
35
|
+
logger = Familia::FamiliaLogger.new(output)
|
36
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
37
|
+
logger.trace { 'Block message' }
|
38
|
+
output.string
|
39
|
+
#=~> /Block message/
|
40
|
+
|
41
|
+
## LogFormatter properly formats TRACE messages
|
42
|
+
output = StringIO.new
|
43
|
+
logger = Familia::FamiliaLogger.new(output)
|
44
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
45
|
+
logger.formatter = Familia::LogFormatter.new
|
46
|
+
logger.trace('Trace test')
|
47
|
+
output.string
|
48
|
+
#=~> /^T,/
|
49
|
+
|
50
|
+
## Nested trace calls preserve TRACE context
|
51
|
+
output = StringIO.new
|
52
|
+
logger = Familia::FamiliaLogger.new(output)
|
53
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
54
|
+
logger.formatter = Familia::LogFormatter.new
|
55
|
+
logger.trace do
|
56
|
+
logger.trace('Inner trace message')
|
57
|
+
'Outer trace message'
|
58
|
+
end
|
59
|
+
output.string.lines.size
|
60
|
+
#=> 2
|
61
|
+
|
62
|
+
## Nested trace inner message formatted as TRACE
|
63
|
+
output = StringIO.new
|
64
|
+
logger = Familia::FamiliaLogger.new(output)
|
65
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
66
|
+
logger.formatter = Familia::LogFormatter.new
|
67
|
+
logger.trace do
|
68
|
+
logger.trace('Inner trace message')
|
69
|
+
'Outer trace message'
|
70
|
+
end
|
71
|
+
output.string.lines[0]
|
72
|
+
#=~> /^T,.*Inner trace message/
|
73
|
+
|
74
|
+
## Nested trace outer message formatted as TRACE
|
75
|
+
output = StringIO.new
|
76
|
+
logger = Familia::FamiliaLogger.new(output)
|
77
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
78
|
+
logger.formatter = Familia::LogFormatter.new
|
79
|
+
logger.trace do
|
80
|
+
logger.trace('Inner trace message')
|
81
|
+
'Outer trace message'
|
82
|
+
end
|
83
|
+
output.string.lines[1]
|
84
|
+
#=~> /^T,.*Outer trace message/
|
85
|
+
|
86
|
+
## Nested trace calls clean up context for subsequent debug calls
|
87
|
+
output = StringIO.new
|
88
|
+
logger = Familia::FamiliaLogger.new(output)
|
89
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
90
|
+
logger.formatter = Familia::LogFormatter.new
|
91
|
+
logger.trace do
|
92
|
+
logger.trace('Inner trace')
|
93
|
+
'Outer trace'
|
94
|
+
end
|
95
|
+
logger.debug('After nested traces')
|
96
|
+
output.string.lines.size
|
97
|
+
#=> 3
|
98
|
+
|
99
|
+
## Debug call after nested traces formatted as DEBUG not TRACE
|
100
|
+
output = StringIO.new
|
101
|
+
logger = Familia::FamiliaLogger.new(output)
|
102
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
103
|
+
logger.formatter = Familia::LogFormatter.new
|
104
|
+
logger.trace do
|
105
|
+
logger.trace('Inner trace')
|
106
|
+
'Outer trace'
|
107
|
+
end
|
108
|
+
logger.debug('After nested traces')
|
109
|
+
output.string.lines[2]
|
110
|
+
#=~> /^D,.*After nested traces/
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Test Valkey/Redis middleware components
|
4
4
|
# Mock Valkey/Redis client with middleware for testing
|
5
5
|
|
6
|
-
require_relative '
|
6
|
+
require_relative '../../support/helpers/test_helpers'
|
7
7
|
|
8
8
|
class MockDatabase
|
9
9
|
attr_reader :logged_commands
|
@@ -27,6 +27,46 @@ class MockDatabase
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
## increment_middleware_version! increases version counter
|
31
|
+
initial_version = Familia.middleware_version
|
32
|
+
Familia.increment_middleware_version!
|
33
|
+
Familia.middleware_version > initial_version
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## increment_middleware_version! increments by exactly 1
|
37
|
+
initial_version = Familia.middleware_version
|
38
|
+
Familia.increment_middleware_version!
|
39
|
+
Familia.middleware_version - initial_version
|
40
|
+
#=> 1
|
41
|
+
|
42
|
+
## fiber_connection= stores connection with current version
|
43
|
+
mock_connection = "test_connection"
|
44
|
+
Familia.fiber_connection=(mock_connection)
|
45
|
+
stored = Fiber[:familia_connection]
|
46
|
+
[stored[0], stored[1] == Familia.middleware_version]
|
47
|
+
#=> ["test_connection", true]
|
48
|
+
|
49
|
+
## fiber_connection= updates version when middleware version changes
|
50
|
+
mock_connection = "test_connection"
|
51
|
+
Familia.fiber_connection=(mock_connection)
|
52
|
+
old_version = Fiber[:familia_connection][1]
|
53
|
+
Familia.increment_middleware_version!
|
54
|
+
Familia.fiber_connection=(mock_connection)
|
55
|
+
new_version = Fiber[:familia_connection][1]
|
56
|
+
new_version > old_version
|
57
|
+
#=> true
|
58
|
+
|
59
|
+
## clear_fiber_connection! removes fiber-local connection
|
60
|
+
Familia.fiber_connection=("test_connection")
|
61
|
+
Familia.clear_fiber_connection!
|
62
|
+
Fiber[:familia_connection]
|
63
|
+
#=> nil
|
64
|
+
|
65
|
+
## clear_fiber_connection! is safe when no connection exists
|
66
|
+
Familia.clear_fiber_connection!
|
67
|
+
Fiber[:familia_connection]
|
68
|
+
#=> nil
|
69
|
+
|
30
70
|
## MockDatabase can log commands with timing
|
31
71
|
dbclient = MockDatabase.new
|
32
72
|
result = dbclient.get("test_key")
|