familia 2.0.0.pre15 → 2.0.0.pre17
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 +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_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 +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,379 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/single_connection_transaction_confusions.rb
|
3
|
+
|
4
|
+
# Redis Single Connection Mode Confusions
|
5
|
+
#
|
6
|
+
# This file demonstrates why mixing Redis operation modes on a single connection
|
7
|
+
# causes subtle but critical failures in production applications.
|
8
|
+
#
|
9
|
+
# Key Concepts:
|
10
|
+
# - Normal mode: Commands execute immediately, return actual values
|
11
|
+
# - MULTI mode: Commands return "QUEUED", execute atomically on EXEC
|
12
|
+
# - Pipeline mode: Commands return Futures, execute in batch
|
13
|
+
#
|
14
|
+
# Production Impact:
|
15
|
+
# - Conditional logic breaks when expecting values but getting "QUEUED"
|
16
|
+
# - Business rules fail silently when code assumes immediate execution
|
17
|
+
# - Nested operations cause Redis protocol errors
|
18
|
+
# - Race conditions emerge from incorrect mode assumptions
|
19
|
+
#
|
20
|
+
# Key Insight: Connection source should determine which operations are
|
21
|
+
# allowed. This is how we prevent bugs like expecting values but getting "QUEUED".
|
22
|
+
#
|
23
|
+
# Summary of Behaviors:
|
24
|
+
#
|
25
|
+
# | Handler | Transaction | Pipeline | Ad-hoc Commands |
|
26
|
+
# |---------|------------|----------|-----------------|
|
27
|
+
# | **FiberTransaction** | Reentrant (same conn) | Error | Use transaction conn |
|
28
|
+
# | **FiberConnection** | Error | Error | ✓ Allowed |
|
29
|
+
# | **Provider** | ✓ New checkout | ✓ New checkout | ✓ New checkout |
|
30
|
+
# | **Default** | ✓ With guards | ✓ With guards | ✓ Check mode |
|
31
|
+
# | **Create** | ✓ Fresh conn | ✓ Fresh conn | ✓ Fresh conn |
|
32
|
+
#
|
33
|
+
#
|
34
|
+
# Usage:
|
35
|
+
#
|
36
|
+
# $ irb
|
37
|
+
# load './single_connection_transaction_confusions.rb'
|
38
|
+
# demo1_multi_queues_commands
|
39
|
+
# demo2_nested_multi_fails
|
40
|
+
# ...
|
41
|
+
|
42
|
+
require 'redis'
|
43
|
+
|
44
|
+
# Global Redis connection for demonstrations
|
45
|
+
$redis = Redis.new(host: 'localhost', port: 6379)
|
46
|
+
|
47
|
+
# Service class for demonstration 5
|
48
|
+
class OrderService
|
49
|
+
def initialize(redis_conn)
|
50
|
+
@redis = redis_conn
|
51
|
+
end
|
52
|
+
|
53
|
+
# This method assumes immediate command execution
|
54
|
+
def process_order(order_id)
|
55
|
+
# Step 1: Set processing status
|
56
|
+
@redis.set("order:#{order_id}:status", 'processing')
|
57
|
+
|
58
|
+
# Step 2: Verify status before proceeding (CRITICAL CHECK)
|
59
|
+
status = @redis.get("order:#{order_id}:status")
|
60
|
+
|
61
|
+
# Step 3: Conditional business logic
|
62
|
+
if status == 'processing'
|
63
|
+
@redis.set("order:#{order_id}:confirmed", 'true')
|
64
|
+
puts "✅ Order #{order_id}: Status confirmed, order processed"
|
65
|
+
else
|
66
|
+
puts "❌ Order #{order_id}: Status check failed - got #{status.inspect}"
|
67
|
+
# In production: would trigger error handling, alerts, etc.
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def setup_redis_demo
|
73
|
+
# Sets up clean Redis state for demonstrations
|
74
|
+
$redis.flushdb
|
75
|
+
$redis.set('counter', '10')
|
76
|
+
$redis.set('name', 'Alice')
|
77
|
+
$redis.set('score', '50')
|
78
|
+
$redis.set('value1', '100')
|
79
|
+
|
80
|
+
puts '=== Redis Demo Environment Ready ==='
|
81
|
+
puts "counter: #{$redis.get('counter')}"
|
82
|
+
puts "name: #{$redis.get('name')}"
|
83
|
+
puts "score: #{$redis.get('score')}"
|
84
|
+
puts
|
85
|
+
end
|
86
|
+
|
87
|
+
def demo1_multi_queues_commands
|
88
|
+
# CRITICAL: Commands inside MULTI don't execute immediately
|
89
|
+
#
|
90
|
+
# Problem: Business logic expecting immediate results gets "QUEUED" instead
|
91
|
+
# Impact: Conditional statements, calculations, and validations all break
|
92
|
+
|
93
|
+
puts '=== Demo 1: Commands inside MULTI return "QUEUED", not values ==='
|
94
|
+
|
95
|
+
$redis.multi do |conn|
|
96
|
+
puts 'Inside MULTI block - all commands are queued...'
|
97
|
+
|
98
|
+
# These increment counter but return "QUEUED"
|
99
|
+
conn.incr('counter')
|
100
|
+
result = conn.get('counter')
|
101
|
+
|
102
|
+
puts "❌ GET inside MULTI returns: #{result.inspect} (expected: '11')"
|
103
|
+
puts "❌ Direct redis.get still shows: #{$redis.get('counter')} (expected: '11')"
|
104
|
+
|
105
|
+
# This condition will ALWAYS be false during MULTI
|
106
|
+
if result == '11'
|
107
|
+
puts '✅ Counter validation passed'
|
108
|
+
else
|
109
|
+
puts "⚠️ Counter validation failed - got #{result.inspect}, not '11'"
|
110
|
+
end
|
111
|
+
|
112
|
+
conn.set('name', 'Bob')
|
113
|
+
end
|
114
|
+
|
115
|
+
puts "\n📊 After MULTI/EXEC completes:"
|
116
|
+
puts "counter: #{$redis.get('counter')} (now executed)"
|
117
|
+
puts "name: #{$redis.get('name')} (now executed)"
|
118
|
+
puts "\n💡 Lesson: Never use command results inside MULTI for logic\n\n"
|
119
|
+
end
|
120
|
+
|
121
|
+
def demo2_nested_multi_fails
|
122
|
+
# CRITICAL: Redis protocol forbids nested MULTI commands
|
123
|
+
#
|
124
|
+
# Problem: Code reuse becomes impossible when methods assume fresh connections
|
125
|
+
# Impact: Shared utilities break when called from within transactions
|
126
|
+
|
127
|
+
puts '=== Demo 2: Nested MULTI causes Redis protocol errors ==='
|
128
|
+
|
129
|
+
begin
|
130
|
+
$redis.multi do |outer_conn|
|
131
|
+
puts 'In outer MULTI transaction...'
|
132
|
+
outer_conn.incr('counter')
|
133
|
+
|
134
|
+
# This breaks the Redis protocol - MULTI calls cannot be nested
|
135
|
+
$redis.multi do |inner_conn|
|
136
|
+
puts 'Attempting inner MULTI...'
|
137
|
+
inner_conn.incr('counter')
|
138
|
+
end
|
139
|
+
end
|
140
|
+
rescue Redis::CommandError => e
|
141
|
+
puts "❌ Redis Error: #{e.message}"
|
142
|
+
puts '💡 This makes shared connection patterns dangerous'
|
143
|
+
end
|
144
|
+
|
145
|
+
puts "📊 Counter after error: #{$redis.get('counter')} (partial execution)\n\n"
|
146
|
+
end
|
147
|
+
|
148
|
+
def demo3_pipeline_multi_confusion
|
149
|
+
# COMPLEX: Pipeline and MULTI modes create execution order confusion
|
150
|
+
#
|
151
|
+
# Problem: Commands execute in unexpected batches with mixed return types
|
152
|
+
# Impact: Debugging becomes nearly impossible with interleaved operations
|
153
|
+
|
154
|
+
puts '=== Demo 3: Pipeline + MULTI creates execution chaos ==='
|
155
|
+
|
156
|
+
$redis.set('counter', '20')
|
157
|
+
|
158
|
+
results = $redis.pipelined do |pipeline|
|
159
|
+
# Returns Future object, not value
|
160
|
+
pipeline.get('counter')
|
161
|
+
|
162
|
+
# MULTI/EXEC block executes inside pipeline
|
163
|
+
pipeline.multi do |multi|
|
164
|
+
multi.incr('counter') # Will be 21
|
165
|
+
multi.incr('counter') # Will be 22
|
166
|
+
end
|
167
|
+
|
168
|
+
# This sees the final value after MULTI completes
|
169
|
+
pipeline.get('counter')
|
170
|
+
end
|
171
|
+
|
172
|
+
puts "📊 Pipeline results: #{results.inspect}"
|
173
|
+
puts '🔍 Analysis:'
|
174
|
+
puts " - results[0]: Initial value ('20')"
|
175
|
+
puts " - results[1]: MULTI/EXEC result (['21', '22'])"
|
176
|
+
puts " - results[2]: Final value ('22')"
|
177
|
+
puts "\n💡 Mixed return types make result processing error-prone\n\n"
|
178
|
+
end
|
179
|
+
|
180
|
+
def demo4_interrupted_multi_cleanup
|
181
|
+
# RECOVERY: Redis auto-DISCARD on exceptions, but application state unclear
|
182
|
+
#
|
183
|
+
# Problem: Partial transaction state is invisible to application code
|
184
|
+
# Impact: Error recovery logic may make incorrect assumptions about data state
|
185
|
+
|
186
|
+
puts '=== Demo 4: Exception handling in MULTI transactions ==='
|
187
|
+
|
188
|
+
$redis.set('value1', '100')
|
189
|
+
original_value = $redis.get('value1')
|
190
|
+
|
191
|
+
begin
|
192
|
+
$redis.multi do |conn|
|
193
|
+
conn.incr('value1') # Queued but not executed
|
194
|
+
|
195
|
+
# Simulate business logic error
|
196
|
+
raise StandardError, 'Payment validation failed'
|
197
|
+
|
198
|
+
conn.incr('value1') # Never reached
|
199
|
+
end
|
200
|
+
rescue StandardError => e
|
201
|
+
puts "🔥 Exception caught: #{e.message}"
|
202
|
+
|
203
|
+
# Redis automatically DISCARD on exception
|
204
|
+
current_value = $redis.get('value1')
|
205
|
+
puts "📊 Value after exception: #{current_value} (#{current_value == original_value ? 'unchanged ✅' : 'modified ❌'})"
|
206
|
+
end
|
207
|
+
|
208
|
+
puts "💡 Redis auto-DISCARD protects consistency but masks partial state\n\n"
|
209
|
+
end
|
210
|
+
|
211
|
+
def demo5_shared_connection_logic_failure
|
212
|
+
# PRODUCTION BUG: Business logic fails silently with shared connections
|
213
|
+
#
|
214
|
+
# Problem: Service classes can't detect when they're called inside transactions
|
215
|
+
# Impact: Critical business rules execute incorrectly without visible errors
|
216
|
+
|
217
|
+
puts '=== Demo 5: Shared connection breaks business logic ==='
|
218
|
+
|
219
|
+
service = OrderService.new($redis)
|
220
|
+
|
221
|
+
puts '📈 Normal operation:'
|
222
|
+
service.process_order(1)
|
223
|
+
puts " Result: #{$redis.get('order:1:confirmed')}"
|
224
|
+
|
225
|
+
puts "\n🔄 Inside MULTI transaction:"
|
226
|
+
$redis.multi do |conn|
|
227
|
+
# Same service class, but now connection is in MULTI mode
|
228
|
+
transaction_service = OrderService.new(conn)
|
229
|
+
transaction_service.process_order(2) # Logic fails silently
|
230
|
+
end
|
231
|
+
puts " Result: #{$redis.get('order:2:confirmed')} (business logic bypassed!)"
|
232
|
+
|
233
|
+
puts "\n💡 Service classes need connection mode awareness for safety\n\n"
|
234
|
+
end
|
235
|
+
|
236
|
+
def demo6_pipeline_future_confusion
|
237
|
+
# TIMING: Pipeline returns Future objects that don't behave like values
|
238
|
+
#
|
239
|
+
# Problem: Conditional logic using Future objects produces unexpected results
|
240
|
+
# Impact: Business rules execute incorrectly based on Future truthiness
|
241
|
+
|
242
|
+
puts '=== Demo 6: Pipeline Futures break conditional logic ==='
|
243
|
+
|
244
|
+
$redis.set('score', '30')
|
245
|
+
|
246
|
+
puts '❌ Broken approach - using Future in conditional:'
|
247
|
+
begin
|
248
|
+
$redis.pipelined do |pipeline|
|
249
|
+
current_score = pipeline.get('score') # Returns Redis::Future
|
250
|
+
|
251
|
+
puts " Future object: #{current_score.class}"
|
252
|
+
puts " Future value (immediate): #{current_score.inspect}"
|
253
|
+
|
254
|
+
# This will cause an error - Future doesn't have to_i method
|
255
|
+
if current_score.to_i > 40
|
256
|
+
pipeline.set('achievement', 'high_score')
|
257
|
+
puts ' ❌ Achievement awarded (incorrect - score is 30!)'
|
258
|
+
end
|
259
|
+
end
|
260
|
+
rescue NoMethodError => e
|
261
|
+
puts " ❌ Error: #{e.message}"
|
262
|
+
puts " 💡 Future objects don't behave like values!"
|
263
|
+
end
|
264
|
+
|
265
|
+
puts "\n✅ Correct approach - wait for pipeline completion:"
|
266
|
+
results = $redis.pipelined do |pipeline|
|
267
|
+
pipeline.get('score')
|
268
|
+
pipeline.exists('achievement')
|
269
|
+
end
|
270
|
+
|
271
|
+
current_score = results[0].to_i
|
272
|
+
has_achievement = results[1]
|
273
|
+
|
274
|
+
puts " Actual score: #{current_score}"
|
275
|
+
puts " Has achievement: #{has_achievement} (incorrect from above)"
|
276
|
+
|
277
|
+
puts "\n💡 Always extract values from pipeline results before logic\n\n"
|
278
|
+
end
|
279
|
+
|
280
|
+
def demo7_mode_switching_catastrophe
|
281
|
+
# CATASTROPHIC: Methods that switch connection modes mid-operation
|
282
|
+
#
|
283
|
+
# Problem: Utility functions work in normal mode but break in MULTI mode
|
284
|
+
# Impact: Subtle bugs that only appear when code paths intersect
|
285
|
+
|
286
|
+
puts '=== Demo 7: Mode switching breaks utility functions ==='
|
287
|
+
|
288
|
+
# Utility function that assumes immediate execution
|
289
|
+
def update_stats(redis_conn, key, increment = 10)
|
290
|
+
# Read current value
|
291
|
+
current = redis_conn.get(key).to_i
|
292
|
+
puts " 📖 Read current value: #{current}"
|
293
|
+
|
294
|
+
# Business logic
|
295
|
+
new_value = current + increment
|
296
|
+
puts " 🧮 Calculated new value: #{new_value}"
|
297
|
+
|
298
|
+
# Write new value
|
299
|
+
redis_conn.set(key, new_value)
|
300
|
+
|
301
|
+
# Verification read (common pattern for critical updates)
|
302
|
+
verified = redis_conn.get(key)
|
303
|
+
puts " ✅ Verification read: #{verified}"
|
304
|
+
|
305
|
+
verified
|
306
|
+
rescue NoMethodError => e
|
307
|
+
puts " ❌ Error: #{e.message}"
|
308
|
+
puts ' 💡 Function expects immediate values but got Future objects!'
|
309
|
+
"ERROR: #{e.message}"
|
310
|
+
end
|
311
|
+
|
312
|
+
$redis.set('stats', '5')
|
313
|
+
|
314
|
+
puts '📈 Normal mode - utility function works:'
|
315
|
+
result = update_stats($redis, 'stats')
|
316
|
+
puts " Final result: #{result} ✅"
|
317
|
+
|
318
|
+
puts "\n🔄 MULTI mode - same function breaks:"
|
319
|
+
$redis.set('stats', '5') # Reset
|
320
|
+
|
321
|
+
$redis.multi do |conn|
|
322
|
+
result = update_stats(conn, 'stats')
|
323
|
+
puts " Final result: #{result.inspect} ❌ (expected: '15')"
|
324
|
+
end
|
325
|
+
|
326
|
+
# Check what actually happened
|
327
|
+
actual_value = $redis.get('stats')
|
328
|
+
puts " Actual final value: #{actual_value}"
|
329
|
+
|
330
|
+
puts "\n💡 Utility functions need explicit mode handling or connection isolation\n\n"
|
331
|
+
end
|
332
|
+
|
333
|
+
def summary_and_solutions
|
334
|
+
# Summary of problems and production-ready solutions
|
335
|
+
|
336
|
+
puts '=== Summary: Redis Connection Mode Problems ==='
|
337
|
+
puts
|
338
|
+
puts '🔴 PROBLEMS:'
|
339
|
+
puts ' 1. MULTI mode returns "QUEUED" instead of values'
|
340
|
+
puts ' 2. Pipeline mode returns Futures instead of values'
|
341
|
+
puts ' 3. Nested MULTI operations cause protocol errors'
|
342
|
+
puts ' 4. Business logic fails silently with wrong assumptions'
|
343
|
+
puts ' 5. Conditional statements break with mode confusion'
|
344
|
+
puts ' 6. Error recovery becomes unpredictable'
|
345
|
+
puts
|
346
|
+
puts '🟢 SOLUTIONS:'
|
347
|
+
puts ' 1. Connection pooling - fresh connection per operation type'
|
348
|
+
puts ' 2. Mode-aware service classes with explicit connection requirements'
|
349
|
+
puts ' 3. Never share connections between normal and transactional code'
|
350
|
+
puts ' 4. Use connection decorators that enforce mode contracts'
|
351
|
+
puts ' 5. Implement connection mode detection and validation'
|
352
|
+
puts ' 6. Design APIs that make mode requirements explicit'
|
353
|
+
puts
|
354
|
+
puts '📚 PRODUCTION PATTERNS:'
|
355
|
+
puts ' - Repository pattern with mode-specific connections'
|
356
|
+
puts ' - Command/Query separation with dedicated connection pools'
|
357
|
+
puts ' - Transaction boundaries clearly defined at service layer'
|
358
|
+
puts ' - Connection mode validation in development/testing'
|
359
|
+
puts
|
360
|
+
end
|
361
|
+
|
362
|
+
# Main execution - run all demonstrations
|
363
|
+
if __FILE__ == $PROGRAM_NAME
|
364
|
+
setup_redis_demo
|
365
|
+
demo1_multi_queues_commands
|
366
|
+
demo2_nested_multi_fails
|
367
|
+
demo3_pipeline_multi_confusion
|
368
|
+
demo4_interrupted_multi_cleanup
|
369
|
+
demo5_shared_connection_logic_failure
|
370
|
+
demo6_pipeline_future_confusion
|
371
|
+
demo7_mode_switching_catastrophe
|
372
|
+
summary_and_solutions
|
373
|
+
|
374
|
+
puts '🎯 Demo complete! Load in IRB to run individual methods:'
|
375
|
+
puts " load './single_connection_transaction_confusions.rb'"
|
376
|
+
puts ' demo1_multi_queues_commands'
|
377
|
+
puts ' demo2_nested_multi_fails'
|
378
|
+
puts ' # ... etc'
|
379
|
+
end
|
data/lib/familia/base.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# lib/familia/base.rb
|
2
2
|
|
3
|
-
#
|
4
3
|
module Familia
|
5
4
|
# A common module for Familia::DataType and Familia::Horreum to include.
|
6
5
|
#
|
@@ -14,7 +13,6 @@ module Familia
|
|
14
13
|
# @see Familia::DataType
|
15
14
|
#
|
16
15
|
module Base
|
17
|
-
|
18
16
|
using Familia::Refinements::TimeLiterals
|
19
17
|
|
20
18
|
@features_available = nil
|
@@ -33,14 +31,15 @@ module Familia
|
|
33
31
|
attr_reader :features_available, :feature_definitions
|
34
32
|
attr_accessor :dump_method, :load_method
|
35
33
|
|
36
|
-
def add_feature(klass, feature_name, depends_on: [])
|
34
|
+
def add_feature(klass, feature_name, depends_on: [], field_group: nil)
|
37
35
|
@features_available ||= {}
|
38
|
-
Familia.trace :ADD_FEATURE, klass, feature_name
|
36
|
+
Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
|
39
37
|
|
40
38
|
# Create field definition object
|
41
39
|
feature_def = FeatureDefinition.new(
|
42
40
|
name: feature_name,
|
43
41
|
depends_on: depends_on,
|
42
|
+
field_group: field_group
|
44
43
|
)
|
45
44
|
|
46
45
|
# Track field definitions after defining field methods
|
@@ -66,19 +65,63 @@ module Familia
|
|
66
65
|
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
67
66
|
end
|
68
67
|
|
68
|
+
# Prepares the object for JSON serialization by converting it to a hash.
|
69
|
+
# This method provides the data preparation step in the standard Ruby JSON
|
70
|
+
# pattern: to_json → as_json → JSON serialization.
|
71
|
+
#
|
72
|
+
# Implementing classes can override this method to customize their JSON
|
73
|
+
# representation. For Horreum objects, this delegates to to_h which returns
|
74
|
+
# only the public fields. For DataType objects, this returns the raw value.
|
75
|
+
#
|
76
|
+
# @param options [Hash] Optional parameters for customizing JSON output
|
77
|
+
# @return [Hash, Object] JSON-serializable representation of the object
|
78
|
+
#
|
79
|
+
def as_json(options = nil)
|
80
|
+
if respond_to?(:to_h)
|
81
|
+
# Horreum objects - return their field hash
|
82
|
+
to_h
|
83
|
+
elsif respond_to?(:members)
|
84
|
+
# DataType objects (List, Set, etc.) - return their members
|
85
|
+
members
|
86
|
+
elsif respond_to?(:value)
|
87
|
+
# String-like objects or simple values
|
88
|
+
value
|
89
|
+
else
|
90
|
+
# Fallback for objects that don't have standard value methods
|
91
|
+
# This ensures we don't expose internal state accidentally
|
92
|
+
{ class: self.class.name, id: respond_to?(:identifier) ? identifier : object_id }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Converts the object to a JSON string using Familia's JsonSerializer.
|
97
|
+
# This method completes the standard Ruby JSON pattern by calling as_json
|
98
|
+
# to prepare the data, then using JsonSerializer.dump for serialization.
|
99
|
+
#
|
100
|
+
# This maintains security by ensuring all JSON serialization goes through
|
101
|
+
# Familia's controlled JsonSerializer (OJ in strict mode) rather than
|
102
|
+
# potentially unsafe serialization methods.
|
103
|
+
#
|
104
|
+
# @param options [Hash] Optional parameters passed to as_json
|
105
|
+
# @return [String] JSON string representation of the object
|
106
|
+
#
|
107
|
+
def to_json(options = nil)
|
108
|
+
Familia::JsonSerializer.dump(as_json(options))
|
109
|
+
end
|
110
|
+
|
69
111
|
# Module-level methods for Familia::Base itself
|
70
112
|
class << self
|
71
113
|
attr_reader :features_available, :feature_definitions
|
72
114
|
attr_accessor :dump_method, :load_method
|
73
115
|
|
74
|
-
def add_feature(klass, feature_name, depends_on: [])
|
116
|
+
def add_feature(klass, feature_name, depends_on: [], field_group: nil)
|
75
117
|
@features_available ||= {}
|
76
|
-
Familia.trace :ADD_FEATURE, klass, feature_name
|
118
|
+
Familia.trace :ADD_FEATURE, klass, feature_name if Familia.debug?
|
77
119
|
|
78
120
|
# Create field definition object
|
79
121
|
feature_def = FeatureDefinition.new(
|
80
122
|
name: feature_name,
|
81
123
|
depends_on: depends_on,
|
124
|
+
field_group: field_group
|
82
125
|
)
|
83
126
|
|
84
127
|
# Track field definitions after defining field methods
|
@@ -99,9 +142,7 @@ module Familia
|
|
99
142
|
next unless ancestor.respond_to?(:features_available)
|
100
143
|
next unless ancestor.features_available
|
101
144
|
|
102
|
-
if ancestor.features_available.key?(feature_name)
|
103
|
-
return ancestor.features_available[feature_name]
|
104
|
-
end
|
145
|
+
return ancestor.features_available[feature_name] if ancestor.features_available.key?(feature_name)
|
105
146
|
end
|
106
147
|
|
107
148
|
nil
|
@@ -109,7 +150,7 @@ module Familia
|
|
109
150
|
end
|
110
151
|
|
111
152
|
def generate_id
|
112
|
-
@identifier ||= Familia.generate_id
|
153
|
+
@identifier ||= Familia.generate_id
|
113
154
|
end
|
114
155
|
|
115
156
|
def uuid
|