familia 2.0.0.pre18 → 2.0.0.pre21
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/claude-code-review.yml +4 -9
- data/.github/workflows/code-smells.yml +64 -3
- data/.pre-commit-config.yaml +8 -6
- data/.reek.yml +10 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.rst +205 -88
- data/CLAUDE.md +62 -10
- data/Gemfile +3 -3
- data/Gemfile.lock +27 -62
- data/README.md +39 -0
- data/bin/try +16 -0
- data/bin/tryouts +16 -0
- data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
- data/docs/1106-participates_in-bidirectional-solution.md +129 -0
- data/docs/guides/encryption.md +486 -0
- data/docs/guides/feature-encrypted-fields.md +123 -7
- data/docs/guides/feature-expiration.md +177 -133
- data/docs/guides/feature-external-identifiers.md +415 -443
- data/docs/guides/feature-object-identifiers.md +400 -269
- data/docs/guides/feature-quantization.md +120 -6
- data/docs/guides/feature-relationships-indexing.md +318 -0
- data/docs/guides/feature-relationships-methods.md +146 -604
- data/docs/guides/feature-relationships-participation.md +263 -0
- data/docs/guides/feature-relationships.md +118 -136
- data/docs/guides/feature-system-devs.md +176 -693
- data/docs/guides/feature-system.md +119 -6
- data/docs/guides/feature-transient-fields.md +81 -0
- data/docs/guides/field-system.md +778 -0
- data/docs/guides/index.md +32 -15
- data/docs/guides/logging.md +187 -0
- data/docs/guides/optimized-loading.md +674 -0
- data/docs/guides/thread-safety-monitoring.md +61 -0
- data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/migrating/v2.0.0-pre22.md +241 -0
- data/docs/overview.md +7 -9
- data/docs/reference/api-technical.md +267 -320
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
- data/examples/autoloader/mega_customer.rb +2 -0
- data/examples/datatype_standalone.rb +282 -0
- data/examples/encrypted_fields.rb +2 -1
- data/examples/json_usage_patterns.rb +2 -0
- data/examples/relationships.rb +3 -0
- data/examples/safe_dump.rb +2 -1
- data/examples/sampling_demo.rb +53 -0
- data/examples/single_connection_transaction_confusions.rb +2 -1
- data/familia.gemspec +2 -1
- data/lib/familia/base.rb +2 -0
- data/lib/familia/connection/behavior.rb +254 -0
- data/lib/familia/connection/handlers.rb +97 -0
- data/lib/familia/connection/individual_command_proxy.rb +2 -0
- data/lib/familia/connection/middleware.rb +34 -24
- data/lib/familia/connection/operation_core.rb +3 -1
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
- data/lib/familia/connection/transaction_core.rb +75 -9
- data/lib/familia/connection.rb +21 -5
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +153 -7
- data/lib/familia/data_type/database_commands.rb +9 -4
- data/lib/familia/data_type/serialization.rb +10 -4
- data/lib/familia/data_type/settings.rb +2 -0
- data/lib/familia/data_type/types/counter.rb +2 -0
- data/lib/familia/data_type/types/hashkey.rb +8 -6
- data/lib/familia/data_type/types/listkey.rb +2 -0
- data/lib/familia/data_type/types/lock.rb +2 -0
- data/lib/familia/data_type/types/sorted_set.rb +2 -0
- data/lib/familia/data_type/types/stringkey.rb +2 -0
- data/lib/familia/data_type/types/unsorted_set.rb +2 -0
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -2
- data/lib/familia/encryption/manager.rb +2 -0
- data/lib/familia/encryption/provider.rb +2 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/registry.rb +2 -0
- data/lib/familia/encryption/request_cache.rb +2 -0
- data/lib/familia/encryption.rb +9 -2
- data/lib/familia/errors.rb +53 -14
- data/lib/familia/features/autoloader.rb +2 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +11 -11
- data/lib/familia/features/expiration.rb +29 -21
- data/lib/familia/features/external_identifier.rb +33 -7
- data/lib/familia/features/object_identifier.rb +2 -0
- data/lib/familia/features/quantization.rb +3 -1
- data/lib/familia/features/relationships/README.md +3 -1
- data/lib/familia/features/relationships/collection_operations.rb +2 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
- data/lib/familia/features/relationships/indexing.rb +40 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
- data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
- data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
- data/lib/familia/features/relationships/participation.rb +155 -69
- data/lib/familia/features/relationships/participation_membership.rb +69 -0
- data/lib/familia/features/relationships/participation_relationship.rb +34 -6
- data/lib/familia/features/relationships/score_encoding.rb +2 -0
- data/lib/familia/features/relationships.rb +5 -3
- data/lib/familia/features/safe_dump.rb +2 -0
- data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
- data/lib/familia/features/transient_fields.rb +2 -0
- data/lib/familia/features.rb +2 -0
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/connection.rb +28 -36
- data/lib/familia/horreum/database_commands.rb +131 -10
- data/lib/familia/horreum/definition.rb +18 -7
- data/lib/familia/horreum/management.rb +233 -57
- data/lib/familia/horreum/persistence.rb +314 -122
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +26 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -8
- data/lib/familia/horreum.rb +46 -13
- data/lib/familia/identifier_extractor.rb +2 -0
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +94 -37
- data/lib/familia/refinements/dear_json.rb +2 -0
- data/lib/familia/refinements/stylize_words.rb +2 -14
- data/lib/familia/refinements/time_literals.rb +2 -0
- data/lib/familia/refinements.rb +2 -0
- data/lib/familia/secure_identifier.rb +10 -2
- data/lib/familia/settings.rb +9 -7
- data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
- data/lib/familia/thread_safety/monitor.rb +328 -0
- data/lib/familia/utils.rb +13 -0
- data/lib/familia/verifiable_identifier.rb +3 -1
- data/lib/familia/version.rb +3 -1
- data/lib/familia.rb +31 -4
- data/lib/middleware/database_command_counter.rb +152 -0
- data/lib/middleware/database_logger.rb +325 -129
- data/lib/multi_result.rb +2 -0
- data/try/edge_cases/empty_identifiers_try.rb +2 -0
- data/try/edge_cases/hash_symbolization_try.rb +2 -0
- data/try/edge_cases/json_serialization_try.rb +2 -0
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
- data/try/edge_cases/race_conditions_try.rb +4 -0
- data/try/edge_cases/reserved_keywords_try.rb +4 -0
- data/try/edge_cases/string_coercion_try.rb +6 -4
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
- data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
- data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
- data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
- data/try/features/encrypted_fields/memory_security_try.rb +4 -0
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
- data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
- data/try/features/encryption/config_persistence_try.rb +4 -0
- data/try/features/encryption/core_try.rb +4 -0
- data/try/features/encryption/instance_variable_scope_try.rb +4 -0
- data/try/features/encryption/module_loading_try.rb +4 -0
- data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
- data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
- data/try/features/encryption/roundtrip_validation_try.rb +4 -0
- data/try/features/encryption/secure_memory_handling_try.rb +4 -0
- data/try/features/expiration/expiration_try.rb +5 -1
- data/try/features/external_identifier/external_identifier_try.rb +171 -8
- data/try/features/feature_dependencies_try.rb +2 -0
- data/try/features/feature_improvements_try.rb +2 -0
- data/try/features/field_groups_try.rb +2 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
- data/try/features/object_identifier/object_identifier_try.rb +2 -0
- data/try/features/quantization/quantization_try.rb +4 -0
- data/try/features/real_feature_integration_try.rb +2 -0
- data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
- data/try/features/relationships/indexing_rebuild_try.rb +600 -0
- data/try/features/relationships/indexing_try.rb +30 -4
- data/try/features/relationships/participation_bidirectional_try.rb +242 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
- data/try/features/relationships/participation_commands_verification_try.rb +2 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
- data/try/features/relationships/participation_reverse_index_try.rb +15 -13
- data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
- data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
- data/try/features/relationships/relationships_api_changes_try.rb +6 -4
- data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
- data/try/features/relationships/relationships_performance_try.rb +4 -0
- data/try/features/relationships/relationships_performance_working_try.rb +4 -0
- data/try/features/relationships/relationships_try.rb +6 -4
- data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
- data/try/features/safe_dump/safe_dump_try.rb +4 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/refresh_reset_try.rb +3 -0
- data/try/features/transient_fields/simple_refresh_test.rb +3 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
- data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
- data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
- data/try/integration/connection/handler_constraints_try.rb +4 -0
- data/try/integration/connection/isolated_dbclient_try.rb +4 -0
- data/try/integration/connection/middleware_reconnect_try.rb +2 -0
- data/try/integration/connection/operation_mode_guards_try.rb +5 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
- data/try/integration/connection/pools_try.rb +4 -0
- data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
- data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
- data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
- data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
- data/try/integration/connection/transaction_modes_try.rb +4 -0
- data/try/integration/conventional_inheritance_try.rb +4 -0
- data/try/integration/create_method_try.rb +26 -22
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
- data/try/integration/data_types/datatype_transactions_try.rb +251 -0
- data/try/integration/database_consistency_try.rb +4 -0
- data/try/integration/familia_extended_try.rb +4 -0
- data/try/integration/familia_members_methods_try.rb +4 -0
- data/try/integration/models/customer_safe_dump_try.rb +9 -1
- data/try/integration/models/customer_try.rb +4 -0
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +5 -1
- data/try/integration/persistence_operations_try.rb +166 -10
- data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
- data/try/integration/save_methods_consistency_try.rb +241 -0
- data/try/integration/scenarios_try.rb +4 -0
- data/try/integration/secure_identifier_try.rb +4 -0
- data/try/integration/transaction_safety_core_try.rb +176 -0
- data/try/integration/transaction_safety_workflow_try.rb +291 -0
- data/try/integration/verifiable_identifier_try.rb +4 -0
- data/try/investigation/pipeline_routing/README.md +228 -0
- data/try/performance/benchmarks_try.rb +4 -0
- data/try/performance/transaction_safety_benchmark_try.rb +238 -0
- data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
- data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
- data/try/support/debugging/cache_behavior_tracer.rb +4 -0
- data/try/support/debugging/debug_aad_process.rb +3 -0
- data/try/support/debugging/debug_concealed_internal.rb +3 -0
- data/try/support/debugging/debug_concealed_reveal.rb +3 -0
- data/try/support/debugging/debug_context_aad.rb +3 -0
- data/try/support/debugging/debug_context_simple.rb +3 -0
- data/try/support/debugging/debug_cross_context.rb +3 -0
- data/try/support/debugging/debug_database_load.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
- data/try/support/debugging/debug_field_decrypt.rb +3 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
- data/try/support/debugging/debug_load_path.rb +3 -0
- data/try/support/debugging/debug_method_definition.rb +3 -0
- data/try/support/debugging/debug_method_resolution.rb +3 -0
- data/try/support/debugging/debug_minimal.rb +3 -0
- data/try/support/debugging/debug_provider.rb +3 -0
- data/try/support/debugging/debug_secure_behavior.rb +3 -0
- data/try/support/debugging/debug_string_class.rb +3 -0
- data/try/support/debugging/debug_test.rb +3 -0
- data/try/support/debugging/debug_test_design.rb +3 -0
- data/try/support/debugging/encryption_method_tracer.rb +4 -0
- data/try/support/debugging/provider_diagnostics.rb +4 -0
- data/try/support/helpers/test_cleanup.rb +4 -0
- data/try/support/helpers/test_helpers.rb +5 -0
- data/try/support/memory/memory_basic_test.rb +4 -0
- data/try/support/memory/memory_detailed_test.rb +4 -0
- data/try/support/memory/memory_search_for_string.rb +4 -0
- data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
- data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
- data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
- data/try/support/prototypes/atomic_saves_v4.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
- data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
- data/try/support/prototypes/pooling/pool_siege.rb +4 -2
- data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
- data/try/thread_safety/README.md +496 -0
- data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
- data/try/thread_safety/connection_chain_race_try.rb +148 -0
- data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
- data/try/thread_safety/feature_registry_race_try.rb +226 -0
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
- data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
- data/try/thread_safety/field_registration_race_try.rb +222 -0
- data/try/thread_safety/logger_initialization_race_try.rb +170 -0
- data/try/thread_safety/middleware_registration_race_try.rb +154 -0
- data/try/thread_safety/module_config_race_try.rb +175 -0
- data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
- data/try/unit/core/autoloader_try.rb +4 -0
- data/try/unit/core/base_enhancements_try.rb +4 -0
- data/try/unit/core/connection_try.rb +4 -0
- data/try/unit/core/errors_try.rb +4 -0
- data/try/unit/core/extensions_try.rb +4 -0
- data/try/unit/core/familia_logger_try.rb +2 -0
- data/try/unit/core/familia_try.rb +4 -0
- data/try/unit/core/middleware_sampling_try.rb +335 -0
- data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
- data/try/unit/core/middleware_thread_safety_try.rb +245 -0
- data/try/unit/core/middleware_try.rb +4 -0
- data/try/unit/core/settings_try.rb +4 -0
- data/try/unit/core/time_utils_try.rb +4 -0
- data/try/unit/core/tools_try.rb +4 -0
- data/try/unit/core/utils_try.rb +37 -0
- data/try/unit/data_types/boolean_try.rb +5 -1
- data/try/unit/data_types/counter_try.rb +4 -0
- data/try/unit/data_types/datatype_base_try.rb +4 -0
- data/try/unit/data_types/hash_try.rb +4 -0
- data/try/unit/data_types/list_try.rb +4 -0
- data/try/unit/data_types/lock_try.rb +4 -0
- data/try/unit/data_types/sorted_set_try.rb +4 -0
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
- data/try/unit/data_types/string_try.rb +5 -1
- data/try/unit/data_types/unsortedset_try.rb +4 -0
- data/try/unit/familia_resolve_class_try.rb +116 -0
- data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
- data/try/unit/horreum/base_try.rb +5 -1
- data/try/unit/horreum/class_methods_try.rb +6 -2
- data/try/unit/horreum/commands_try.rb +4 -0
- data/try/unit/horreum/defensive_initialization_try.rb +4 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
- data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
- data/try/unit/horreum/field_categories_try.rb +4 -0
- data/try/unit/horreum/field_definition_try.rb +4 -0
- data/try/unit/horreum/initialization_try.rb +5 -1
- data/try/unit/horreum/json_type_preservation_try.rb +2 -0
- data/try/unit/horreum/optimized_loading_try.rb +156 -0
- data/try/unit/horreum/relations_try.rb +8 -4
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +6 -2
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
- data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
- data/try/unit/middleware/database_logger_methods_try.rb +251 -0
- data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
- data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
- data/try/unit/thread_safety_monitor_try.rb +149 -0
- metadata +81 -14
- data/.github/workflows/code-quality.yml +0 -138
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# try/integration/transaction_safety_core_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Core Transaction Safety Tests
|
|
6
|
+
#
|
|
7
|
+
# Tests the fundamental transaction safety rules from docs/transaction_safety.md
|
|
8
|
+
# Focuses on the most critical safety mechanisms.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
require_relative '../support/helpers/test_helpers'
|
|
12
|
+
|
|
13
|
+
# Simple test model for transaction safety
|
|
14
|
+
class ::SafetyTestCustomer < Familia::Horreum
|
|
15
|
+
identifier_field :email
|
|
16
|
+
field :email
|
|
17
|
+
field :login_count
|
|
18
|
+
field :status
|
|
19
|
+
|
|
20
|
+
list :orders
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Helper for unique test IDs
|
|
24
|
+
def safety_test_id(prefix = 'test')
|
|
25
|
+
"#{prefix}_#{Time.now.to_i}_#{rand(1000000)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
## save raises OperationModeError inside transaction
|
|
29
|
+
error_raised = false
|
|
30
|
+
SafetyTestCustomer.transaction do
|
|
31
|
+
customer = SafetyTestCustomer.new(email: "#{safety_test_id}@example.com")
|
|
32
|
+
begin
|
|
33
|
+
customer.save
|
|
34
|
+
rescue Familia::OperationModeError => e
|
|
35
|
+
error_raised = true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
error_raised
|
|
39
|
+
#=> true
|
|
40
|
+
|
|
41
|
+
## save_if_not_exists! raises OperationModeError inside transaction
|
|
42
|
+
error_raised_conditional = false
|
|
43
|
+
SafetyTestCustomer.transaction do
|
|
44
|
+
customer = SafetyTestCustomer.new(email: "#{safety_test_id}@example.com")
|
|
45
|
+
begin
|
|
46
|
+
customer.save_if_not_exists!
|
|
47
|
+
rescue Familia::OperationModeError => e
|
|
48
|
+
error_raised_conditional = true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
error_raised_conditional
|
|
52
|
+
#=> true
|
|
53
|
+
|
|
54
|
+
## create! raises OperationModeError inside transaction
|
|
55
|
+
error_raised_create = false
|
|
56
|
+
SafetyTestCustomer.transaction do
|
|
57
|
+
begin
|
|
58
|
+
SafetyTestCustomer.create!(email: "#{safety_test_id}@example.com")
|
|
59
|
+
rescue Familia::OperationModeError => e
|
|
60
|
+
error_raised_create = true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
error_raised_create
|
|
64
|
+
#=> true
|
|
65
|
+
|
|
66
|
+
## correct pattern save before transaction works
|
|
67
|
+
@correct_customer = SafetyTestCustomer.new(email: "#{safety_test_id('correct')}@example.com")
|
|
68
|
+
@correct_customer.save
|
|
69
|
+
#=> true
|
|
70
|
+
|
|
71
|
+
## operations work inside transaction after save
|
|
72
|
+
@correct_customer.transaction do
|
|
73
|
+
@correct_customer.hset(:login_count, '1')
|
|
74
|
+
@correct_customer.hset(:status, 'active')
|
|
75
|
+
end
|
|
76
|
+
@correct_customer.hget(:login_count).to_i >= 1
|
|
77
|
+
#=> true
|
|
78
|
+
|
|
79
|
+
## write-only operations work inside transactions
|
|
80
|
+
@write_customer = SafetyTestCustomer.new(email: "#{safety_test_id('write')}@example.com")
|
|
81
|
+
@write_customer.save
|
|
82
|
+
|
|
83
|
+
@write_result = @write_customer.transaction do |conn|
|
|
84
|
+
conn.hset(@write_customer.dbkey, 'status', 'premium')
|
|
85
|
+
conn.hset(@write_customer.dbkey, 'login_count', '5')
|
|
86
|
+
conn.expire(@write_customer.dbkey, 3600)
|
|
87
|
+
end
|
|
88
|
+
@write_result.class.name
|
|
89
|
+
#=> "MultiResult"
|
|
90
|
+
|
|
91
|
+
## nested transactions reuse same connection
|
|
92
|
+
@nested_customer = SafetyTestCustomer.new(email: "#{safety_test_id('nested')}@example.com")
|
|
93
|
+
@nested_customer.save
|
|
94
|
+
|
|
95
|
+
@outer_conn_id = nil
|
|
96
|
+
@inner_conn_id = nil
|
|
97
|
+
|
|
98
|
+
SafetyTestCustomer.transaction do |outer_conn|
|
|
99
|
+
@outer_conn_id = outer_conn.object_id
|
|
100
|
+
@nested_customer.hset(:login_count, '1')
|
|
101
|
+
|
|
102
|
+
@nested_customer.transaction do |inner_conn|
|
|
103
|
+
@inner_conn_id = inner_conn.object_id
|
|
104
|
+
@nested_customer.hset(:status, 'nested')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@outer_conn_id == @inner_conn_id
|
|
109
|
+
#=> true
|
|
110
|
+
|
|
111
|
+
## read operations return Future objects inside transaction
|
|
112
|
+
@read_customer = SafetyTestCustomer.new(email: "#{safety_test_id('read')}@example.com")
|
|
113
|
+
@read_customer.save
|
|
114
|
+
|
|
115
|
+
@future_object = nil
|
|
116
|
+
@read_customer.transaction do |conn|
|
|
117
|
+
@future_object = conn.hget(@read_customer.dbkey, 'email')
|
|
118
|
+
end
|
|
119
|
+
@future_object.class.name.include?('Future')
|
|
120
|
+
#=> true
|
|
121
|
+
|
|
122
|
+
## exists check returns Future inside transaction always truthy
|
|
123
|
+
@pitfall_customer = SafetyTestCustomer.new(email: "#{safety_test_id('pitfall')}@example.com")
|
|
124
|
+
|
|
125
|
+
@wrong_result = nil
|
|
126
|
+
SafetyTestCustomer.transaction do |conn|
|
|
127
|
+
existence = conn.exists?(@pitfall_customer.dbkey)
|
|
128
|
+
@wrong_result = if existence
|
|
129
|
+
'always_executed'
|
|
130
|
+
else
|
|
131
|
+
'never_executed'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
@wrong_result
|
|
135
|
+
#=> 'always_executed'
|
|
136
|
+
|
|
137
|
+
## create with success callback works
|
|
138
|
+
@callback_executed = false
|
|
139
|
+
SafetyTestCustomer.create!(email: "#{safety_test_id('callback')}@example.com") do |customer|
|
|
140
|
+
@callback_executed = true
|
|
141
|
+
customer.hset(:login_count, '1')
|
|
142
|
+
end
|
|
143
|
+
@callback_executed
|
|
144
|
+
#=> true
|
|
145
|
+
|
|
146
|
+
## multi-object atomic updates work
|
|
147
|
+
@order_customer = SafetyTestCustomer.new(email: "#{safety_test_id('order')}@example.com")
|
|
148
|
+
@order_customer.save
|
|
149
|
+
|
|
150
|
+
@multi_result = SafetyTestCustomer.transaction do
|
|
151
|
+
@order_customer.hset(:status, 'confirmed')
|
|
152
|
+
@order_customer.orders.push('order123')
|
|
153
|
+
@order_customer.hset(:login_count, '1')
|
|
154
|
+
end
|
|
155
|
+
@multi_result.successful?
|
|
156
|
+
#=> true
|
|
157
|
+
|
|
158
|
+
## watch pattern for optimistic locking works
|
|
159
|
+
@watch_customer = SafetyTestCustomer.new(email: "#{safety_test_id('watch')}@example.com")
|
|
160
|
+
@watch_customer.save
|
|
161
|
+
@watch_customer.hset(:balance, '1000')
|
|
162
|
+
|
|
163
|
+
@success = @watch_customer.watch do
|
|
164
|
+
current_balance = @watch_customer.hget(:balance).to_i
|
|
165
|
+
|
|
166
|
+
if current_balance >= 100
|
|
167
|
+
@watch_customer.transaction do
|
|
168
|
+
@watch_customer.hset(:balance, (current_balance - 100).to_s)
|
|
169
|
+
@watch_customer.hset(:purchases, '1')
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
@new_balance = @watch_customer.hget(:balance).to_i
|
|
175
|
+
@new_balance <= 900
|
|
176
|
+
#=> true
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# try/integration/transaction_safety_workflow_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Transaction Safety Workflow Integration Test
|
|
6
|
+
#
|
|
7
|
+
# Demonstrates the complete transaction safety workflow with realistic
|
|
8
|
+
# business scenarios that show correct usage patterns.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
require_relative '../support/helpers/test_helpers'
|
|
12
|
+
|
|
13
|
+
# Business models for realistic workflow testing
|
|
14
|
+
class ::WorkflowCustomer < Familia::Horreum
|
|
15
|
+
identifier_field :customer_id
|
|
16
|
+
field :customer_id
|
|
17
|
+
field :email
|
|
18
|
+
field :balance
|
|
19
|
+
field :status
|
|
20
|
+
field :login_count
|
|
21
|
+
field :last_login
|
|
22
|
+
|
|
23
|
+
list :orders
|
|
24
|
+
set :preferences
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ::WorkflowOrder < Familia::Horreum
|
|
28
|
+
identifier_field :order_id
|
|
29
|
+
field :order_id
|
|
30
|
+
field :customer_id
|
|
31
|
+
field :amount
|
|
32
|
+
field :status
|
|
33
|
+
field :created_at
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ::WorkflowInventory < Familia::Horreum
|
|
37
|
+
identifier_field :product_id
|
|
38
|
+
field :product_id
|
|
39
|
+
field :quantity
|
|
40
|
+
field :reserved
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Helper for unique IDs
|
|
44
|
+
def workflow_id(prefix = 'wf')
|
|
45
|
+
"#{prefix}_#{Time.now.to_i}_#{rand(100000)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
## Complete customer registration workflow
|
|
49
|
+
@customer_email = "#{workflow_id('customer')}@example.com"
|
|
50
|
+
@customer_id = workflow_id('cust')
|
|
51
|
+
|
|
52
|
+
# Step 1: Create customer (validates uniqueness outside transaction)
|
|
53
|
+
@customer = WorkflowCustomer.new(
|
|
54
|
+
customer_id: @customer_id,
|
|
55
|
+
email: @customer_email,
|
|
56
|
+
balance: 1000,
|
|
57
|
+
status: 'pending',
|
|
58
|
+
login_count: 0
|
|
59
|
+
)
|
|
60
|
+
@customer.save
|
|
61
|
+
#=> true
|
|
62
|
+
|
|
63
|
+
## Customer exists after registration
|
|
64
|
+
@customer.exists?
|
|
65
|
+
#=> true
|
|
66
|
+
|
|
67
|
+
## E-commerce order processing workflow
|
|
68
|
+
@product_id = workflow_id('prod')
|
|
69
|
+
@order_id = workflow_id('order')
|
|
70
|
+
|
|
71
|
+
# Setup inventory
|
|
72
|
+
@inventory = WorkflowInventory.new(
|
|
73
|
+
product_id: @product_id,
|
|
74
|
+
quantity: 50,
|
|
75
|
+
reserved: 0
|
|
76
|
+
)
|
|
77
|
+
@inventory.save
|
|
78
|
+
|
|
79
|
+
# Step 1: Create order (outside transaction for validation)
|
|
80
|
+
@order = WorkflowOrder.new(
|
|
81
|
+
order_id: @order_id,
|
|
82
|
+
customer_id: @customer_id,
|
|
83
|
+
amount: 99,
|
|
84
|
+
status: 'pending',
|
|
85
|
+
created_at: Time.now.to_i
|
|
86
|
+
)
|
|
87
|
+
@order.save
|
|
88
|
+
#=> true
|
|
89
|
+
|
|
90
|
+
## Atomic order processing with inventory update
|
|
91
|
+
@processing_result = WorkflowCustomer.transaction do |conn|
|
|
92
|
+
# Update customer
|
|
93
|
+
conn.hset(@customer.dbkey, 'balance', '901') # 1000 - 99
|
|
94
|
+
conn.hset(@customer.dbkey, 'last_login', Time.now.to_i.to_s)
|
|
95
|
+
@customer.orders.push(@order_id)
|
|
96
|
+
|
|
97
|
+
# Update order
|
|
98
|
+
conn.hset(@order.dbkey, 'status', 'confirmed')
|
|
99
|
+
|
|
100
|
+
# Update inventory
|
|
101
|
+
conn.hset(@inventory.dbkey, 'quantity', '49') # 50 - 1
|
|
102
|
+
conn.hset(@inventory.dbkey, 'reserved', '1')
|
|
103
|
+
|
|
104
|
+
# Add customer preference
|
|
105
|
+
@customer.preferences.add('email_notifications')
|
|
106
|
+
end
|
|
107
|
+
@processing_result.class.name
|
|
108
|
+
#=> "MultiResult"
|
|
109
|
+
|
|
110
|
+
## All updates applied atomically
|
|
111
|
+
[@customer.hget('balance').to_i, @order.hget('status'), @inventory.hget('quantity').to_i]
|
|
112
|
+
#=> [901, "confirmed", 49]
|
|
113
|
+
|
|
114
|
+
## Customer login tracking workflow with nested transactions
|
|
115
|
+
@login_start = Time.now
|
|
116
|
+
|
|
117
|
+
# Read current count outside transaction
|
|
118
|
+
@current_count = @customer.hget('login_count').to_i
|
|
119
|
+
|
|
120
|
+
@customer.transaction do |outer_conn|
|
|
121
|
+
# Outer transaction: main login processing
|
|
122
|
+
outer_conn.hset(@customer.dbkey, 'last_login', @login_start.to_i.to_s)
|
|
123
|
+
|
|
124
|
+
# Nested transaction: increment login count (reentrant)
|
|
125
|
+
@customer.transaction do |inner_conn|
|
|
126
|
+
inner_conn.hset(@customer.dbkey, 'login_count', (@current_count + 1).to_s)
|
|
127
|
+
|
|
128
|
+
# Add login preference tracking
|
|
129
|
+
@customer.preferences.add('frequent_user') if @current_count > 5
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Continue outer transaction
|
|
133
|
+
outer_conn.hset(@customer.dbkey, 'status', 'active')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@customer.hget('login_count').to_i >= 1
|
|
137
|
+
#=> true
|
|
138
|
+
|
|
139
|
+
## Bulk order fulfillment workflow
|
|
140
|
+
@order_ids = 3.times.map { |i| workflow_id("bulk_#{i}") }
|
|
141
|
+
|
|
142
|
+
# Step 1: Create all orders outside transaction
|
|
143
|
+
@bulk_orders = @order_ids.map do |order_id|
|
|
144
|
+
order = WorkflowOrder.new(
|
|
145
|
+
order_id: order_id,
|
|
146
|
+
customer_id: @customer_id,
|
|
147
|
+
amount: 25,
|
|
148
|
+
status: 'pending',
|
|
149
|
+
created_at: Time.now.to_i
|
|
150
|
+
)
|
|
151
|
+
order.save
|
|
152
|
+
order
|
|
153
|
+
end
|
|
154
|
+
@bulk_orders.all?(&:exists?)
|
|
155
|
+
#=> true
|
|
156
|
+
|
|
157
|
+
## Bulk fulfillment in single transaction
|
|
158
|
+
# Read balance outside transaction
|
|
159
|
+
@current_balance = @customer.hget('balance').to_i
|
|
160
|
+
@bulk_result = WorkflowOrder.transaction do |conn|
|
|
161
|
+
@bulk_orders.each do |order|
|
|
162
|
+
conn.hset(order.dbkey, 'status', 'fulfilled')
|
|
163
|
+
conn.hset(order.dbkey, 'fulfilled_at', Time.now.to_i.to_s)
|
|
164
|
+
@customer.orders.push(order.order_id)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Update customer balance for all orders
|
|
168
|
+
new_balance = @current_balance - (25 * 3)
|
|
169
|
+
conn.hset(@customer.dbkey, 'balance', new_balance.to_s)
|
|
170
|
+
end
|
|
171
|
+
@bulk_result.class.name
|
|
172
|
+
#=> "MultiResult"
|
|
173
|
+
|
|
174
|
+
## Error handling in transaction workflow
|
|
175
|
+
@error_order_id = workflow_id('error')
|
|
176
|
+
@error_handled = false
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
WorkflowOrder.transaction do |conn|
|
|
180
|
+
# Valid operation
|
|
181
|
+
conn.hset("test:#{@error_order_id}", 'status', 'processing')
|
|
182
|
+
|
|
183
|
+
# Simulate error during processing
|
|
184
|
+
raise StandardError, 'Payment processing failed'
|
|
185
|
+
|
|
186
|
+
# This would not execute due to error
|
|
187
|
+
conn.hset("test:#{@error_order_id}", 'status', 'completed')
|
|
188
|
+
end
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
@error_handled = e.message.include?('Payment processing failed')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
@error_handled
|
|
194
|
+
#=> true
|
|
195
|
+
|
|
196
|
+
## Transaction safety violation detection
|
|
197
|
+
@safety_violation_detected = false
|
|
198
|
+
|
|
199
|
+
WorkflowCustomer.transaction do
|
|
200
|
+
test_customer = WorkflowCustomer.new(
|
|
201
|
+
customer_id: workflow_id('safety'),
|
|
202
|
+
email: "#{workflow_id('safety')}@test.com"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
begin
|
|
206
|
+
# This should raise OperationModeError
|
|
207
|
+
test_customer.save
|
|
208
|
+
rescue Familia::OperationModeError => e
|
|
209
|
+
@safety_violation_detected = e.message.include?('Cannot call save within a transaction')
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
@safety_violation_detected
|
|
214
|
+
#=> true
|
|
215
|
+
|
|
216
|
+
## Performance comparison: individual vs batch operations
|
|
217
|
+
@perf_customers = 5.times.map do |i|
|
|
218
|
+
customer = WorkflowCustomer.new(
|
|
219
|
+
customer_id: workflow_id("perf_#{i}"),
|
|
220
|
+
email: "perf#{i}@example.com",
|
|
221
|
+
balance: 1000
|
|
222
|
+
)
|
|
223
|
+
customer.save
|
|
224
|
+
customer
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Individual transactions
|
|
228
|
+
@individual_start = Time.now
|
|
229
|
+
@perf_customers.each do |customer|
|
|
230
|
+
customer.transaction do |conn|
|
|
231
|
+
conn.hset(customer.dbkey, 'status', 'updated_individual')
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
@individual_duration = ((Time.now - @individual_start) * 1000).round(2)
|
|
235
|
+
|
|
236
|
+
# Single batch transaction
|
|
237
|
+
@batch_start = Time.now
|
|
238
|
+
WorkflowCustomer.transaction do |conn|
|
|
239
|
+
@perf_customers.each do |customer|
|
|
240
|
+
conn.hset(customer.dbkey, 'status', 'updated_batch')
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
@batch_duration = ((Time.now - @batch_start) * 1000).round(2)
|
|
244
|
+
|
|
245
|
+
# Batch should be faster or at least not significantly slower
|
|
246
|
+
@efficiency_ratio = @individual_duration / @batch_duration
|
|
247
|
+
@efficiency_ratio >= 0.1 # Batch should be reasonably fast
|
|
248
|
+
#=> true
|
|
249
|
+
|
|
250
|
+
## Watch pattern for optimistic concurrency control
|
|
251
|
+
@concurrent_customer = WorkflowCustomer.new(
|
|
252
|
+
customer_id: workflow_id('concurrent'),
|
|
253
|
+
email: 'concurrent@test.com',
|
|
254
|
+
balance: 500
|
|
255
|
+
)
|
|
256
|
+
@concurrent_customer.save
|
|
257
|
+
@concurrent_customer.hset(:version, '1')
|
|
258
|
+
|
|
259
|
+
@watch_success = @concurrent_customer.watch do
|
|
260
|
+
current_version = @concurrent_customer.hget(:version).to_i
|
|
261
|
+
current_balance = @concurrent_customer.hget(:balance).to_i
|
|
262
|
+
|
|
263
|
+
# Only proceed if version hasn't changed and sufficient balance
|
|
264
|
+
if current_version == 1 && current_balance >= 100
|
|
265
|
+
@concurrent_customer.transaction do |conn|
|
|
266
|
+
conn.hset(@concurrent_customer.dbkey, 'balance', (current_balance - 100).to_s)
|
|
267
|
+
conn.hset(@concurrent_customer.dbkey, 'version', '2')
|
|
268
|
+
conn.hset(@concurrent_customer.dbkey, 'last_transaction', Time.now.to_i.to_s)
|
|
269
|
+
end
|
|
270
|
+
true
|
|
271
|
+
else
|
|
272
|
+
false
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
@concurrent_customer.hget(:balance).to_i == 400
|
|
277
|
+
#=> true
|
|
278
|
+
|
|
279
|
+
## Workflow completed successfully with all safety checks
|
|
280
|
+
@workflow_summary = {
|
|
281
|
+
customer_created: @customer.exists?,
|
|
282
|
+
order_processed: @order.hget('status') == 'confirmed',
|
|
283
|
+
bulk_fulfilled: @bulk_orders.all? { |o| o.hget('status') == 'fulfilled' },
|
|
284
|
+
error_handled: @error_handled,
|
|
285
|
+
safety_enforced: @safety_violation_detected,
|
|
286
|
+
performance_acceptable: @efficiency_ratio >= 0.1,
|
|
287
|
+
concurrency_controlled: @concurrent_customer.hget(:version).to_i >= 2
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@workflow_summary.values.all?
|
|
291
|
+
#=> true
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Pipeline Routing Investigation (ARCHIVED)
|
|
2
|
+
|
|
3
|
+
**Status**: Investigation complete - NO BUG FOUND
|
|
4
|
+
**Conclusion**: See `CONCLUSION.md` for full analysis
|
|
5
|
+
**Test Files**: Renamed to `.rb.txt` to preserve as documentation without running in CI
|
|
6
|
+
|
|
7
|
+
## Problem Statement (Original)
|
|
8
|
+
|
|
9
|
+
In a concurrent test with 20 threads executing pipelined operations, we observed:
|
|
10
|
+
- All 20 threads successfully complete
|
|
11
|
+
- All 20 commands are captured in `DatabaseLogger.commands`
|
|
12
|
+
- BUT: Only 16 commands contain the pipeline separator `' | '`
|
|
13
|
+
- This appeared to mean 4 pipeline operations were logged via `call()` instead of `call_pipelined()`
|
|
14
|
+
|
|
15
|
+
**Investigation Result**: Single-command pipelines don't have `' | '` separator (expected `Array#join` behavior). This is NOT a bug.
|
|
16
|
+
|
|
17
|
+
## Investigation Goals
|
|
18
|
+
|
|
19
|
+
1. Determine if this is a RedisClient middleware dispatch issue
|
|
20
|
+
2. Determine if this is a Familia connection chain issue
|
|
21
|
+
3. Determine if this is timing-dependent (race condition)
|
|
22
|
+
4. Determine if connection reuse vs. fresh connections affects behavior
|
|
23
|
+
|
|
24
|
+
## Test Suite Structure
|
|
25
|
+
|
|
26
|
+
### 01_single_thread_baseline_try.rb
|
|
27
|
+
**Purpose**: Establish baseline behavior in single-threaded environment
|
|
28
|
+
|
|
29
|
+
**Tests**:
|
|
30
|
+
- 10 simple pipeline operations
|
|
31
|
+
- 25 pipeline operations with varying sizes (1-5 commands)
|
|
32
|
+
- Mixed single and pipelined operations (10 each)
|
|
33
|
+
|
|
34
|
+
**Expected**: 100% of pipeline operations route to `call_pipelined()`
|
|
35
|
+
|
|
36
|
+
**Key Question**: Does single-threaded execution work correctly?
|
|
37
|
+
|
|
38
|
+
### 02_small_concurrency_try.rb
|
|
39
|
+
**Purpose**: Test with minimal thread contention (5 threads)
|
|
40
|
+
|
|
41
|
+
**Tests**:
|
|
42
|
+
- 5 threads with CyclicBarrier (synchronized start)
|
|
43
|
+
- 5 threads without barrier (natural timing)
|
|
44
|
+
- 5 threads with varying pipeline sizes (1-5 commands)
|
|
45
|
+
|
|
46
|
+
**Expected**: 100% of pipeline operations route to `call_pipelined()`
|
|
47
|
+
|
|
48
|
+
**Key Question**: Does adding concurrency break routing?
|
|
49
|
+
|
|
50
|
+
### 03_reproduce_issue_try.rb
|
|
51
|
+
**Purpose**: Reproduce the exact scenario from the original failing test
|
|
52
|
+
|
|
53
|
+
**Tests**:
|
|
54
|
+
- 20 threads with CyclicBarrier (exact reproduction)
|
|
55
|
+
- 10 repeated trials to check for intermittent behavior
|
|
56
|
+
|
|
57
|
+
**Expected**: May reproduce the routing issue
|
|
58
|
+
|
|
59
|
+
**Key Questions**:
|
|
60
|
+
- Is the issue reproducible?
|
|
61
|
+
- Is it deterministic or intermittent?
|
|
62
|
+
- What's the failure rate?
|
|
63
|
+
|
|
64
|
+
### 04_high_contention_try.rb
|
|
65
|
+
**Purpose**: Test under high thread contention (50+ threads)
|
|
66
|
+
|
|
67
|
+
**Tests**:
|
|
68
|
+
- 50 threads with synchronized start
|
|
69
|
+
- 50 threads with varying pipeline sizes (1-10 commands)
|
|
70
|
+
- 10 threads × 5 rapid pipelines each (50 total)
|
|
71
|
+
|
|
72
|
+
**Expected**: If timing-related, higher contention should increase failure rate
|
|
73
|
+
|
|
74
|
+
**Key Question**: Does the problem scale with thread count?
|
|
75
|
+
|
|
76
|
+
### 05_connection_isolation_try.rb
|
|
77
|
+
**Purpose**: Test whether the issue is connection-specific
|
|
78
|
+
|
|
79
|
+
**Tests**:
|
|
80
|
+
- Fresh connection per thread (each thread calls `Familia.dbclient`)
|
|
81
|
+
- Shared connection from main thread (original pattern)
|
|
82
|
+
- Isolated connections via `create_dbclient` (no pooling)
|
|
83
|
+
- Connection via chain per thread (uses connection handlers)
|
|
84
|
+
|
|
85
|
+
**Expected**: Identifies which connection pattern is problematic
|
|
86
|
+
|
|
87
|
+
**Key Questions**:
|
|
88
|
+
- Does connection reuse cause the issue?
|
|
89
|
+
- Does the connection chain have a bug?
|
|
90
|
+
- Is middleware registration timing-dependent?
|
|
91
|
+
|
|
92
|
+
### 06_fiber_state_inspection_try.rb
|
|
93
|
+
**Purpose**: Inspect Fiber-local state during pipeline operations
|
|
94
|
+
|
|
95
|
+
**Tests**:
|
|
96
|
+
- Single-threaded Fiber state tracking (before/inside/after pipeline)
|
|
97
|
+
- Multi-threaded Fiber isolation verification (10 threads)
|
|
98
|
+
- Middleware call context inspection (capture what middleware sees)
|
|
99
|
+
- Pipeline routing verification (ensure single `call_pipelined` per pipeline)
|
|
100
|
+
|
|
101
|
+
**Expected**: Fiber-local state should be isolated per thread
|
|
102
|
+
|
|
103
|
+
**Key Questions**:
|
|
104
|
+
- Is `Fiber[:familia_pipeline]` being set correctly?
|
|
105
|
+
- Is cleanup happening in ensure blocks?
|
|
106
|
+
- Do threads share Fiber state incorrectly?
|
|
107
|
+
- Does middleware receive correct context?
|
|
108
|
+
|
|
109
|
+
## Running the Tests
|
|
110
|
+
|
|
111
|
+
### Run all investigation tests
|
|
112
|
+
```bash
|
|
113
|
+
FAMILIA_DEBUG=0 bundle exec try --agent try/investigation/pipeline_routing/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Run individual tests
|
|
117
|
+
```bash
|
|
118
|
+
# Baseline
|
|
119
|
+
bundle exec try --agent try/investigation/pipeline_routing/01_single_thread_baseline_try.rb
|
|
120
|
+
|
|
121
|
+
# Small concurrency
|
|
122
|
+
bundle exec try --agent try/investigation/pipeline_routing/02_small_concurrency_try.rb
|
|
123
|
+
|
|
124
|
+
# Reproduce issue
|
|
125
|
+
bundle exec try --agent try/investigation/pipeline_routing/03_reproduce_issue_try.rb
|
|
126
|
+
|
|
127
|
+
# High contention
|
|
128
|
+
bundle exec try --agent try/investigation/pipeline_routing/04_high_contention_try.rb
|
|
129
|
+
|
|
130
|
+
# Connection isolation
|
|
131
|
+
bundle exec try --agent try/investigation/pipeline_routing/05_connection_isolation_try.rb
|
|
132
|
+
|
|
133
|
+
# Fiber state
|
|
134
|
+
bundle exec try --agent try/investigation/pipeline_routing/06_fiber_state_inspection_try.rb
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Run with verbose output (for failures)
|
|
138
|
+
```bash
|
|
139
|
+
bundle exec try --verbose --fails --stack try/investigation/pipeline_routing/03_reproduce_issue_try.rb
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## What to Look For
|
|
143
|
+
|
|
144
|
+
### Success Indicators
|
|
145
|
+
- All pipeline operations contain `' | '` separator
|
|
146
|
+
- Command counts match expected values
|
|
147
|
+
- No "ROUTING ANOMALY" messages in output
|
|
148
|
+
|
|
149
|
+
### Failure Indicators
|
|
150
|
+
- Pipeline operations logged without `' | '` separator
|
|
151
|
+
- Fewer `call_pipelined()` invocations than expected
|
|
152
|
+
- "ROUTING ANOMALY DETECTED" in output
|
|
153
|
+
- Mismatched command counts
|
|
154
|
+
|
|
155
|
+
### Diagnostic Output
|
|
156
|
+
Each test prints detailed analysis including:
|
|
157
|
+
- Total commands captured
|
|
158
|
+
- Pipeline commands (with separator)
|
|
159
|
+
- Single commands (without separator)
|
|
160
|
+
- Percentage breakdown
|
|
161
|
+
- Specific examples of misrouted commands
|
|
162
|
+
|
|
163
|
+
## Hypothesis Checklist
|
|
164
|
+
|
|
165
|
+
After running the tests, we should be able to answer:
|
|
166
|
+
|
|
167
|
+
- [ ] Does single-threaded execution work correctly?
|
|
168
|
+
- [ ] Does small concurrency (5 threads) work correctly?
|
|
169
|
+
- [ ] Can we reproduce the issue with 20 threads?
|
|
170
|
+
- [ ] Is the issue deterministic or intermittent?
|
|
171
|
+
- [ ] Does the problem scale with thread count?
|
|
172
|
+
- [ ] Is it related to connection reuse vs. fresh connections?
|
|
173
|
+
- [ ] Is it related to the connection chain implementation?
|
|
174
|
+
- [ ] Is Fiber-local state being managed correctly?
|
|
175
|
+
- [ ] Does middleware receive the correct context?
|
|
176
|
+
|
|
177
|
+
## Next Steps
|
|
178
|
+
|
|
179
|
+
Based on results:
|
|
180
|
+
|
|
181
|
+
1. **If baseline fails**: RedisClient middleware routing is broken in general
|
|
182
|
+
2. **If only concurrent tests fail**: Thread safety issue in middleware dispatch
|
|
183
|
+
3. **If only shared connection fails**: Connection chain or pooling issue
|
|
184
|
+
4. **If Fiber state leaks**: Cleanup logic in `PipelineCore` is broken
|
|
185
|
+
5. **If intermittent**: Race condition requiring deeper investigation
|
|
186
|
+
|
|
187
|
+
## Background: How Pipeline Routing Should Work
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# RedisClient source (redis-client-0.25.1/lib/redis_client.rb:446)
|
|
191
|
+
def pipelined(exception: true)
|
|
192
|
+
pipeline = Pipeline.new(@command_builder)
|
|
193
|
+
yield pipeline
|
|
194
|
+
|
|
195
|
+
if pipeline._size == 0
|
|
196
|
+
[]
|
|
197
|
+
else
|
|
198
|
+
results = ensure_connected(retryable: pipeline._retryable?) do |connection|
|
|
199
|
+
commands = pipeline._commands
|
|
200
|
+
@middlewares.call_pipelined(commands, config) do # <-- Should call this
|
|
201
|
+
connection.call_pipelined(commands, pipeline._timeouts, exception: exception)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
pipeline._coerce!(results)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Expected flow**:
|
|
211
|
+
1. User calls `client.pipelined { |p| p.set(...) }`
|
|
212
|
+
2. RedisClient builds pipeline commands
|
|
213
|
+
3. RedisClient calls `@middlewares.call_pipelined(commands, config)`
|
|
214
|
+
4. DatabaseLogger.call_pipelined receives commands array
|
|
215
|
+
5. Logs with `' | '` separator joining commands
|
|
216
|
+
|
|
217
|
+
**Anomalous flow** (what we're seeing):
|
|
218
|
+
1. User calls `client.pipelined { |p| p.set(...) }`
|
|
219
|
+
2. ??? Something goes wrong ???
|
|
220
|
+
3. DatabaseLogger.call receives individual command
|
|
221
|
+
4. Logs without `' | '` separator
|
|
222
|
+
|
|
223
|
+
## Files Involved
|
|
224
|
+
|
|
225
|
+
- `/Users/d/Projects/opensource/d/familia/lib/middleware/database_logger.rb` - Middleware implementation
|
|
226
|
+
- `/Users/d/Projects/opensource/d/familia/lib/familia/connection.rb` - Connection management
|
|
227
|
+
- `/Users/d/Projects/opensource/d/familia/lib/familia/connection/pipelined_core.rb` - Pipeline execution
|
|
228
|
+
- `/Users/d/Projects/opensource/d/familia/try/unit/core/middleware_thread_safety_try.rb` - Original test that exposed this
|