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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# pool_siege.rb
|
|
2
|
+
# try/support/prototypes/pooling/pool_siege.rb
|
|
3
3
|
#
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
# pool_siege.rb
|
|
4
7
|
# Simple Connection Pool Load Tester - Like siege, but for Database connection pools
|
|
5
|
-
#
|
|
6
8
|
# Usage:
|
|
7
9
|
# ruby pool_siege.rb -t 20 -p 5 -o 100 # 20 threads, 5 pool size, 100 ops each
|
|
8
10
|
# ruby pool_siege.rb --stress # Find breaking point
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# try/prototypes/run_stress_tests.rb
|
|
2
|
+
# try/support/prototypes/pooling/run_stress_tests.rb
|
|
3
3
|
#
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
# try/prototypes/run_stress_tests.rb
|
|
4
7
|
# Main Test Runner for Connection Pool Stress Tests
|
|
5
|
-
#
|
|
6
8
|
# This script orchestrates comprehensive stress testing of the connection pool
|
|
7
9
|
# implementation across different scenarios, threading models, and configurations.
|
|
8
10
|
# It generates detailed reports and comparisons to identify bottlenecks and
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# Thread Safety Test Suite
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This directory contains targeted tests that expose threading vulnerabilities in Familia's codebase. These tests were created following a systematic analysis that identified critical race conditions in production code that currently have no synchronization protection.
|
|
6
|
+
|
|
7
|
+
**Key Insight**: These tests are designed to **fail or expose inconsistencies** because the underlying code lacks proper synchronization. As of the latest updates, test assertions have been strengthened to properly verify thread safety invariants (singleton properties, consistency, etc.) rather than just checking thread completion.
|
|
8
|
+
|
|
9
|
+
**Current Status**: As of 2025-10-21, test suite has **61 passing / 4 failing** tests. The 4 failures properly detect real race conditions and bugs that need fixing in production code. This honest failure rate is intentional and expected - it shows exactly where work is needed.
|
|
10
|
+
|
|
11
|
+
## The Thread Safety Problem
|
|
12
|
+
|
|
13
|
+
Familia uses several patterns that are inherently unsafe under concurrent access:
|
|
14
|
+
|
|
15
|
+
### 1. Lazy Initialization Without Synchronization
|
|
16
|
+
```ruby
|
|
17
|
+
# This pattern appears throughout the codebase
|
|
18
|
+
def connection_chain
|
|
19
|
+
@connection_chain ||= build_connection_chain # RACE CONDITION
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**The Race**: Two threads can both see `@connection_chain` as nil, both call `build_connection_chain`, and create duplicate objects. The second write wins, but the first object is leaked.
|
|
24
|
+
|
|
25
|
+
**Where it appears**:
|
|
26
|
+
- Module-level connection chain
|
|
27
|
+
- Class-level connection chain (per Horreum subclass)
|
|
28
|
+
- Feature registry collections
|
|
29
|
+
- Field definition collections
|
|
30
|
+
- Various caches (encryption manager, SecureIdentifier, logger)
|
|
31
|
+
|
|
32
|
+
### 2. Check-Then-Act Races
|
|
33
|
+
```ruby
|
|
34
|
+
# From middleware registration
|
|
35
|
+
if !@logger_registered
|
|
36
|
+
RedisClient.register(DatabaseLogger) # RACE CONDITION
|
|
37
|
+
@logger_registered = true
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**The Race**: Thread A checks the flag (false), gets interrupted. Thread B checks (false), registers middleware, sets flag. Thread A resumes and registers again - now you have duplicate middleware in the chain.
|
|
42
|
+
|
|
43
|
+
### 3. Non-Atomic Updates
|
|
44
|
+
```ruby
|
|
45
|
+
# From middleware versioning
|
|
46
|
+
def increment_middleware_version!
|
|
47
|
+
@middleware_version += 1 # RACE CONDITION: read-modify-write
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**The Race**: Classic lost update problem. With 100 concurrent increments, you might get 97 instead of 100 due to interleaved reads.
|
|
52
|
+
|
|
53
|
+
### 4. Unprotected Shared State
|
|
54
|
+
```ruby
|
|
55
|
+
# Module configuration
|
|
56
|
+
@uri = URI.parse('redis://...')
|
|
57
|
+
@prefix = nil
|
|
58
|
+
@delim = ':'
|
|
59
|
+
# Multiple threads can read/write these without coordination
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**The Race**: Thread A reads `@delim` as ":", Thread B changes it to "::", Thread A uses the old value - inconsistent key generation.
|
|
63
|
+
|
|
64
|
+
## Testing Strategy
|
|
65
|
+
|
|
66
|
+
### What We're Testing For
|
|
67
|
+
|
|
68
|
+
1. **Singleton Violations**: Lazy initialization creating multiple instances
|
|
69
|
+
2. **Lost Updates**: Concurrent increments not all counted
|
|
70
|
+
3. **State Inconsistency**: Reads getting partially-updated values
|
|
71
|
+
4. **Duplicate Operations**: Check-then-act races causing double-execution
|
|
72
|
+
5. **Isolation Failures**: Fiber-local storage leaking between fibers
|
|
73
|
+
|
|
74
|
+
### How We Test
|
|
75
|
+
|
|
76
|
+
#### CyclicBarrier for Maximum Contention
|
|
77
|
+
We deliberately synchronize thread start to maximize the chance of exposing races:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
barrier = Concurrent::CyclicBarrier.new(50)
|
|
81
|
+
threads = 50.times.map do
|
|
82
|
+
Thread.new do
|
|
83
|
+
barrier.wait # All 50 threads pause here
|
|
84
|
+
# Now they all execute the racy code simultaneously
|
|
85
|
+
@connection_chain ||= build_connection_chain
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This isn't "production realistic" - it's **deliberately adversarial** to expose bugs that might only happen 1 in 1000 times in production.
|
|
91
|
+
|
|
92
|
+
#### Concurrent::Array for Safe Result Collection
|
|
93
|
+
Standard Ruby arrays aren't thread-safe. We use `Concurrent::Array` to collect results without adding our own bugs:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
results = Concurrent::Array.new # Thread-safe
|
|
97
|
+
threads.each { |t| results << t.value }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Multi-Property Assertion Pattern
|
|
101
|
+
Test multiple invariants together to catch different types of corruption:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Test array corruption, correctness, and completeness
|
|
105
|
+
[results.any?(nil), results.all? { |r| r == 'PONG' }, results.size]
|
|
106
|
+
#=> [false, true, 50]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This pattern catches:
|
|
110
|
+
- **Array corruption**: `results.any?(nil)` detects concurrent modification bugs
|
|
111
|
+
- **Correctness**: `results.all? { ... }` verifies logical correctness
|
|
112
|
+
- **Completeness**: `results.size` ensures no lost operations
|
|
113
|
+
|
|
114
|
+
#### CountDownLatch for Timeout Protection
|
|
115
|
+
Tests should never hang forever if there's a deadlock:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
latch = Concurrent::CountDownLatch.new(10)
|
|
119
|
+
# ... spawn threads ...
|
|
120
|
+
latch.wait(5) # Fail fast if deadlock occurs
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### What Success Looks Like
|
|
124
|
+
|
|
125
|
+
**Before Fix** (current state):
|
|
126
|
+
```ruby
|
|
127
|
+
## Concurrent connection chain initialization
|
|
128
|
+
# ...50 threads all call Familia.dbclient...
|
|
129
|
+
results.uniq.size
|
|
130
|
+
#=> 3 # FAIL: Created 3 different chains (race condition)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**After Fix** (with Mutex):
|
|
134
|
+
```ruby
|
|
135
|
+
## Concurrent connection chain initialization
|
|
136
|
+
# ...50 threads all call Familia.dbclient...
|
|
137
|
+
results.uniq.size
|
|
138
|
+
#=> 1 # PASS: Only one chain created (thread-safe)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Recent Changes (2025-10-21)
|
|
142
|
+
|
|
143
|
+
**Test Assertion Improvements**: Test assertions have been updated to properly verify thread safety rather than just checking thread completion. For example:
|
|
144
|
+
|
|
145
|
+
**Before** (weak assertion):
|
|
146
|
+
```ruby
|
|
147
|
+
chains << chain.class.name
|
|
148
|
+
threads.each(&:join)
|
|
149
|
+
chains.size # Only checks 50 threads completed
|
|
150
|
+
#=> 50
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**After** (strong assertion):
|
|
154
|
+
```ruby
|
|
155
|
+
chains << chain.object_id # Store actual object identity
|
|
156
|
+
threads.each(&:join)
|
|
157
|
+
chains.uniq.size # Checks singleton property
|
|
158
|
+
#=> 1 # Will FAIL if race creates multiple chains
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This makes tests properly fail when race conditions occur, providing honest feedback about thread safety status.
|
|
162
|
+
|
|
163
|
+
## Current Test Results
|
|
164
|
+
|
|
165
|
+
**Test Suite**: 63 total tests across 9 files
|
|
166
|
+
**Status**: 61 passing, 2 failing (736ms runtime)
|
|
167
|
+
**Coverage**: ~30% of identified thread safety risks (target: ~85%)
|
|
168
|
+
|
|
169
|
+
### Known Failures (Real Bugs Detected)
|
|
170
|
+
|
|
171
|
+
These 2 failures properly detect a real race condition in production code:
|
|
172
|
+
|
|
173
|
+
#### 1. Connection Chain Lazy Init (`connection_chain_race_try.rb:39`)
|
|
174
|
+
|
|
175
|
+
**Failure**: Expected `[false, 1]` but got `[false, 50]`
|
|
176
|
+
- Module-level `@connection_chain ||= build_connection_chain` lacks Mutex protection
|
|
177
|
+
- Creates 50 different chain instances under concurrent access (should be 1)
|
|
178
|
+
- **Root cause**: `lib/familia/connection.rb:95`
|
|
179
|
+
- **Fix needed**: Add Mutex synchronization around lazy initialization
|
|
180
|
+
|
|
181
|
+
#### 2. Connection Chain Rebuild (`connection_chain_race_try.rb:123`)
|
|
182
|
+
|
|
183
|
+
**Failure**: Expected `[false, true, 40]` but got `[false, false, 40]`
|
|
184
|
+
- One thread nils out `@connection_chain` while 39 others try to use it
|
|
185
|
+
- Some threads get errors instead of successful RedisClient instances
|
|
186
|
+
- **Root cause**: No synchronization protecting chain rebuilding
|
|
187
|
+
- **Fix needed**: Mutex around connection chain access
|
|
188
|
+
|
|
189
|
+
### Removed Tests (Investigation Revealed Not Bugs)
|
|
190
|
+
|
|
191
|
+
These tests were removed after thorough investigation revealed they were based on incorrect assumptions:
|
|
192
|
+
|
|
193
|
+
#### 3. Sample Rate Counter Test (REMOVED - NOT A BUG)
|
|
194
|
+
|
|
195
|
+
**Original Failure**: Expected `[true, false]` (sampled ~50%), got `[false, false]` (logged all 100)
|
|
196
|
+
|
|
197
|
+
**Investigation Result**:
|
|
198
|
+
- `sample_rate` controls **logging output**, not **command capture**
|
|
199
|
+
- The `commands` array always contains all commands regardless of sample_rate
|
|
200
|
+
- This is intentional behavior per `database_logger.rb:153-154`
|
|
201
|
+
- Comment states: "Command capture is unaffected - only logger output is sampled"
|
|
202
|
+
- **Status**: Removed from `middleware_thread_safety_try.rb:113`
|
|
203
|
+
|
|
204
|
+
#### 4. Pipeline Command Logging Test (REMOVED - NOT A BUG)
|
|
205
|
+
|
|
206
|
+
**Original Failure**: Expected 20 pipeline commands with ' | ', got only 16
|
|
207
|
+
|
|
208
|
+
**Investigation Result**:
|
|
209
|
+
- Single-command pipelines exist and are valid
|
|
210
|
+
- `Array#join(' | ')` only adds separators BETWEEN elements
|
|
211
|
+
- Single-command pipeline: `['SET key val']` → no separator (nothing to separate)
|
|
212
|
+
- Multi-command pipeline: `['SET key1 val1', 'SET key2 val2']` → has separator
|
|
213
|
+
- Backend-dev investigation created 7 diagnostic testcases confirming correct behavior
|
|
214
|
+
- See `try/investigation/pipeline_routing/CONCLUSION.md` for full technical analysis
|
|
215
|
+
- **Status**: Removed from `middleware_thread_safety_try.rb:224`
|
|
216
|
+
|
|
217
|
+
## Coverage Status
|
|
218
|
+
|
|
219
|
+
### Working Protection (2 areas)
|
|
220
|
+
- ✅ **DatabaseLogger**: Uses Mutex + Concurrent::Array
|
|
221
|
+
- ✅ **DatabaseCommandCounter**: Uses Concurrent::AtomicFixnum
|
|
222
|
+
|
|
223
|
+
These are good examples of proper synchronization.
|
|
224
|
+
|
|
225
|
+
### No Protection - HIGH RISK (6 areas)
|
|
226
|
+
- ❌ Middleware registration flags
|
|
227
|
+
- ❌ Middleware version counter
|
|
228
|
+
- ❌ Connection chain lazy init (module and class level)
|
|
229
|
+
- ❌ Field collections lazy init
|
|
230
|
+
- ❌ Module configuration state
|
|
231
|
+
|
|
232
|
+
### No Protection - MEDIUM RISK (6 areas)
|
|
233
|
+
- ⚠️ Various caches (encryption, SecureIdentifier, logger)
|
|
234
|
+
- ⚠️ Feature registry
|
|
235
|
+
- ⚠️ Class inheritance copying
|
|
236
|
+
|
|
237
|
+
### Fiber Isolation (needs verification)
|
|
238
|
+
- 🔍 Transaction fiber-local storage
|
|
239
|
+
- 🔍 Pipeline fiber-local storage
|
|
240
|
+
|
|
241
|
+
These *should* be safe by design (fiber-local variables), but we need tests to verify no leakage.
|
|
242
|
+
|
|
243
|
+
## Known Test Issues
|
|
244
|
+
|
|
245
|
+
Most tests need minor fixes before they can run properly:
|
|
246
|
+
|
|
247
|
+
### 1. Model Identifier Pattern
|
|
248
|
+
Tests currently use `identifier :object_id` which isn't valid Familia syntax.
|
|
249
|
+
|
|
250
|
+
**Need to use**:
|
|
251
|
+
```ruby
|
|
252
|
+
class TestModel < Familia::Horreum
|
|
253
|
+
identifier_field :model_id
|
|
254
|
+
field :model_id
|
|
255
|
+
|
|
256
|
+
def init
|
|
257
|
+
@model_id ||= SecureRandom.hex(8)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 2. Tryouts Expectation Format
|
|
263
|
+
Last line must be the value being tested, not a variable reference:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# Wrong
|
|
267
|
+
results.size
|
|
268
|
+
#=> results.size == 20
|
|
269
|
+
|
|
270
|
+
# Right
|
|
271
|
+
results.size
|
|
272
|
+
#=> 20
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 3. Fiber Cross-Thread Issues
|
|
276
|
+
Fibers can't be resumed across thread boundaries. Tests need restructuring to create fibers within threads, not across them.
|
|
277
|
+
|
|
278
|
+
## How to Use These Tests
|
|
279
|
+
|
|
280
|
+
### Running Tests
|
|
281
|
+
```bash
|
|
282
|
+
# All thread safety tests
|
|
283
|
+
bundle exec try try/thread_safety/
|
|
284
|
+
|
|
285
|
+
# Single file
|
|
286
|
+
bundle exec try try/thread_safety/middleware_registration_race_try.rb
|
|
287
|
+
|
|
288
|
+
# With LLM-friendly output
|
|
289
|
+
FAMILIA_DEBUG=0 bundle exec try --agent try/thread_safety/
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Interpreting Results
|
|
293
|
+
|
|
294
|
+
**Test Passes**: The code has proper synchronization for this scenario.
|
|
295
|
+
|
|
296
|
+
**Test Fails with Inconsistent Results**: Race condition confirmed. Example:
|
|
297
|
+
```
|
|
298
|
+
expected 1, got 3 # Created 3 objects instead of 1 (singleton violation)
|
|
299
|
+
expected 100, got 97 # Lost 3 updates (non-atomic increment)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Test Fails with Errors**: Usually means the test itself needs fixing (see Known Issues above), not necessarily a threading bug.
|
|
303
|
+
|
|
304
|
+
### Development Workflow
|
|
305
|
+
|
|
306
|
+
1. **Fix a test** to run properly (identifier, expectations, fiber issues)
|
|
307
|
+
2. **Run the test** - expect it to fail or show inconsistencies
|
|
308
|
+
3. **Add synchronization** to the production code (Mutex, AtomicFixnum, etc.)
|
|
309
|
+
4. **Run test again** - should now pass consistently
|
|
310
|
+
5. **Repeat** for next test
|
|
311
|
+
|
|
312
|
+
### When Adding New Code
|
|
313
|
+
|
|
314
|
+
Ask these questions:
|
|
315
|
+
|
|
316
|
+
1. **Is this shared mutable state?** → Needs synchronization
|
|
317
|
+
2. **Is this lazy initialization?** → Use Mutex or eager initialization
|
|
318
|
+
3. **Is this a counter?** → Use Concurrent::AtomicFixnum
|
|
319
|
+
4. **Is this check-then-act?** → Make atomic or use Mutex
|
|
320
|
+
5. **Is this a cache?** → Use ThreadSafe::Cache from concurrent-ruby
|
|
321
|
+
|
|
322
|
+
## Thread Counts Matter
|
|
323
|
+
|
|
324
|
+
- **10 threads**: Smoke test - catches obvious issues
|
|
325
|
+
- **20-50 threads**: Standard - catches most race conditions
|
|
326
|
+
- **100 threads**: Stress test - thorough but slower
|
|
327
|
+
- **1000+ threads**: Overkill - flaky, slow, not recommended
|
|
328
|
+
|
|
329
|
+
We use 20-100 threads in these tests as a sweet spot between detection and speed.
|
|
330
|
+
|
|
331
|
+
## Advanced Testing Patterns
|
|
332
|
+
|
|
333
|
+
These patterns are derived from the comprehensive middleware thread safety tests and can be applied to test any concurrent code.
|
|
334
|
+
|
|
335
|
+
### 1. Structure Validation Pattern
|
|
336
|
+
Verify that concurrent operations don't corrupt data structures:
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
all_valid = results.all? do |item|
|
|
340
|
+
item.field1.is_a?(String) &&
|
|
341
|
+
item.field2.is_a?(Integer) &&
|
|
342
|
+
item.field3.is_a?(Float)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
[results.size, results.any?(nil), all_valid]
|
|
346
|
+
#=> [50, false, true]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 2. Concurrent Clearing Pattern
|
|
350
|
+
Test that clearing operations don't corrupt during active usage:
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
# 50 threads writing
|
|
354
|
+
writers = 50.times.map do
|
|
355
|
+
Thread.new do
|
|
356
|
+
barrier.wait
|
|
357
|
+
10.times { write_operation }
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# 1 thread clearing
|
|
362
|
+
clearer = Thread.new do
|
|
363
|
+
barrier.wait
|
|
364
|
+
5.times { clear_operation }
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Array should never contain nil entries
|
|
368
|
+
results.any?(nil)
|
|
369
|
+
#=> false
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### 3. Mixed Operation Types Pattern
|
|
373
|
+
Test different operations concurrently to expose interaction bugs:
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
threads = 60.times.map do |i|
|
|
377
|
+
Thread.new do
|
|
378
|
+
barrier.wait
|
|
379
|
+
case i % 3
|
|
380
|
+
when 0 # Operation type A
|
|
381
|
+
regular_operation
|
|
382
|
+
when 1 # Operation type B
|
|
383
|
+
pipeline_operation
|
|
384
|
+
when 2 # Operation type C
|
|
385
|
+
rapid_operations
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 4. Rapid Sequential Calls Pattern
|
|
392
|
+
Test that rapid-fire operations within threads don't corrupt state:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
threads = 20.times.map do
|
|
396
|
+
Thread.new do
|
|
397
|
+
# Each thread hammers the system
|
|
398
|
+
10.times { operation }
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Should have 200 results (20 × 10), all valid
|
|
403
|
+
[results.size, results.any?(nil)]
|
|
404
|
+
#=> [200, false]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 5. Type and Method Validation Pattern
|
|
408
|
+
Ensure objects maintain their interface under concurrency:
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# All results should respond to expected methods
|
|
412
|
+
results.all? { |r| r.respond_to?(:expected_method) }
|
|
413
|
+
#=> true
|
|
414
|
+
|
|
415
|
+
# All results should be correct type
|
|
416
|
+
results.all? { |r| r.is_a?(ExpectedClass) }
|
|
417
|
+
#=> true
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Architecture Insights
|
|
421
|
+
|
|
422
|
+
### Why Familia Has These Issues
|
|
423
|
+
|
|
424
|
+
1. **Module-level state**: Familia uses module instance variables for global config
|
|
425
|
+
2. **Class-level state**: Each Horreum subclass has its own connection chain, field collections, etc.
|
|
426
|
+
3. **Lazy initialization**: Performance optimization that trades safety for speed
|
|
427
|
+
4. **No threading in original design**: Familia was designed for single-threaded use
|
|
428
|
+
|
|
429
|
+
### The Fiber Safety Assumption
|
|
430
|
+
|
|
431
|
+
Familia uses fiber-local storage for transactions and pipelines:
|
|
432
|
+
```ruby
|
|
433
|
+
Fiber[:familia_transaction] = conn
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
This is safe **within a fiber** but the tests verify:
|
|
437
|
+
- No leakage between fibers
|
|
438
|
+
- Proper cleanup after exceptions
|
|
439
|
+
- Correct behavior with fiber switching
|
|
440
|
+
|
|
441
|
+
## Reference Patterns
|
|
442
|
+
|
|
443
|
+
### Safe Lazy Initialization
|
|
444
|
+
```ruby
|
|
445
|
+
def connection_chain
|
|
446
|
+
@connection_chain_mutex ||= Mutex.new
|
|
447
|
+
@connection_chain_mutex.synchronize do
|
|
448
|
+
@connection_chain ||= build_connection_chain
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Safe Atomic Counter
|
|
454
|
+
```ruby
|
|
455
|
+
@middleware_version = Concurrent::AtomicFixnum.new(0)
|
|
456
|
+
|
|
457
|
+
def increment_middleware_version!
|
|
458
|
+
@middleware_version.increment
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Safe Check-Then-Act
|
|
463
|
+
```ruby
|
|
464
|
+
@registration_mutex = Mutex.new
|
|
465
|
+
|
|
466
|
+
def register_middleware
|
|
467
|
+
@registration_mutex.synchronize do
|
|
468
|
+
return if @middleware_registered
|
|
469
|
+
RedisClient.register(DatabaseLogger)
|
|
470
|
+
@middleware_registered = true
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Safe Caching
|
|
476
|
+
```ruby
|
|
477
|
+
require 'concurrent-ruby'
|
|
478
|
+
@cache = ThreadSafe::Cache.new
|
|
479
|
+
|
|
480
|
+
def get_or_create(key)
|
|
481
|
+
@cache.fetch_or_store(key) { expensive_operation }
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Further Reading
|
|
486
|
+
|
|
487
|
+
- **Main Analysis**: `/THREAD_SAFETY_ANALYSIS.md` - Comprehensive findings and specific line numbers
|
|
488
|
+
- **Thread Safety Cheatsheet**: Project memory `cheatsheet_thread_safety_testing`
|
|
489
|
+
- **Concurrent Ruby**: https://github.com/ruby-concurrency/concurrent-ruby
|
|
490
|
+
- **Ruby Threads**: https://docs.ruby-lang.org/en/master/Thread.html
|
|
491
|
+
|
|
492
|
+
## Philosophy
|
|
493
|
+
|
|
494
|
+
These tests exist because **threading bugs are hard to find and reproduce**. They might work 999 times and fail on the 1000th. These tests create the worst-case scenario on every run, making the invisible visible.
|
|
495
|
+
|
|
496
|
+
A test suite without thread safety tests is like a car without crash tests - it might work fine until someone gets hurt.
|