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,39 +1,116 @@
|
|
|
1
1
|
# lib/middleware/database_logger.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require 'concurrent-ruby'
|
|
4
6
|
|
|
5
|
-
# DatabaseLogger is
|
|
7
|
+
# DatabaseLogger is redis-rb middleware for command logging and capture.
|
|
8
|
+
#
|
|
9
|
+
# Provides detailed Redis command logging for development and debugging.
|
|
10
|
+
# Familia uses the redis-rb gem (v4.8.1 to <6.0), which internally uses
|
|
11
|
+
# RedisClient infrastructure for middleware. Users work with Redis.new
|
|
12
|
+
# connections and Redis:: exceptions - RedisClient is an implementation detail.
|
|
13
|
+
#
|
|
14
|
+
# ## User-Facing API
|
|
15
|
+
#
|
|
16
|
+
# Enable via Familia configuration:
|
|
17
|
+
# Familia.enable_database_logging = true
|
|
18
|
+
#
|
|
19
|
+
# Familia automatically calls RedisClient.register(DatabaseLogger) internally.
|
|
20
|
+
#
|
|
21
|
+
# ## Critical: Uses `super` not `yield` for middleware chaining
|
|
22
|
+
# @see https://github.com/redis-rb/redis-client#instrumentation-and-middlewares
|
|
23
|
+
# ## Internal: RedisClient Middleware Architecture
|
|
24
|
+
#
|
|
25
|
+
# RedisClient middlewares are modules that are `include`d into the
|
|
26
|
+
# `RedisClient::Middlewares` class, which inherits from `BasicMiddleware`.
|
|
27
|
+
# The middleware chain works through Ruby's method lookup and `super`.
|
|
28
|
+
#
|
|
29
|
+
# ### Middleware Chain Flow (Internal)
|
|
30
|
+
#
|
|
31
|
+
# ```ruby
|
|
32
|
+
# # Internal registration order (last registered is called first):
|
|
33
|
+
# RedisClient.register(DatabaseLogger) # Called second (internal)
|
|
34
|
+
# RedisClient.register(DatabaseCommandCounter) # Called first (internal)
|
|
35
|
+
#
|
|
36
|
+
# # Execution flow when client.call('SET', 'key', 'value') is invoked:
|
|
37
|
+
# DatabaseCommandCounter.call(cmd, config) { |result| ... }
|
|
38
|
+
# └─> super # Implicitly passes block to next middleware
|
|
39
|
+
# └─> DatabaseLogger.call(cmd, config)
|
|
40
|
+
# └─> super # Implicitly passes block to next middleware
|
|
41
|
+
# └─> BasicMiddleware.call(cmd, config)
|
|
42
|
+
# └─> yield command # Executes actual Redis command
|
|
43
|
+
# └─> Returns result
|
|
44
|
+
# ← result flows back up
|
|
45
|
+
# ← result flows back up
|
|
46
|
+
# ← result flows back up
|
|
47
|
+
# ← result flows back up
|
|
48
|
+
# ```
|
|
6
49
|
#
|
|
7
|
-
#
|
|
8
|
-
# was removed from the redis-rb gem due to performance concerns. However, in
|
|
9
|
-
# many development and debugging scenarios, the ability to log Database commands
|
|
10
|
-
# can be invaluable.
|
|
50
|
+
# ### Critical Implementation Detail: `super` vs `yield`
|
|
11
51
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
52
|
+
# **MUST use `super`** to properly chain middlewares. Using `yield` breaks
|
|
53
|
+
# the chain because it executes the original block directly, bypassing other
|
|
54
|
+
# middlewares in the chain.
|
|
55
|
+
#
|
|
56
|
+
# ```ruby
|
|
57
|
+
# # ✅ CORRECT - Chains to next middleware
|
|
58
|
+
# def call(command, config)
|
|
59
|
+
# result = super # Calls next middleware, block passes implicitly
|
|
60
|
+
# result
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# # ❌ WRONG - Breaks middleware chain
|
|
64
|
+
# def call(command, config)
|
|
65
|
+
# result = yield # Executes block directly, skips other middlewares!
|
|
66
|
+
# result
|
|
67
|
+
# end
|
|
68
|
+
# ```
|
|
69
|
+
#
|
|
70
|
+
# When `super` is called:
|
|
71
|
+
# 1. Ruby automatically passes the block to the next method in the chain
|
|
72
|
+
# 2. The next middleware's `call` method executes
|
|
73
|
+
# 3. Eventually reaches `BasicMiddleware.call` which does `yield command`
|
|
74
|
+
# 4. The actual Redis command executes
|
|
75
|
+
# 5. Results flow back up through each middleware
|
|
76
|
+
#
|
|
77
|
+
# ## Usage Examples
|
|
78
|
+
#
|
|
79
|
+
# @example Enable Redis command logging (recommended user-facing API)
|
|
80
|
+
# Familia.enable_database_logging = true
|
|
15
81
|
#
|
|
16
82
|
# @example Capture commands for testing
|
|
17
83
|
# commands = DatabaseLogger.capture_commands do
|
|
18
84
|
# redis.set('key', 'value')
|
|
19
85
|
# redis.get('key')
|
|
20
86
|
# end
|
|
21
|
-
# puts commands.first
|
|
87
|
+
# puts commands.first.command # => "SET key value"
|
|
22
88
|
#
|
|
23
|
-
# @
|
|
89
|
+
# @example Use with DatabaseCommandCounter
|
|
90
|
+
# Familia.enable_database_logging = true
|
|
91
|
+
# Familia.enable_database_counter = true
|
|
92
|
+
# # Both middlewares registered automatically and execute correctly in sequence
|
|
24
93
|
#
|
|
25
|
-
#
|
|
26
|
-
# the redis-rb gem, this middleware is designed to be optional and can be
|
|
27
|
-
# easily enabled or disabled as needed. The performance impact is minimal
|
|
28
|
-
# when logging is disabled, and the benefits during development and debugging
|
|
29
|
-
# often outweigh the slight performance cost when enabled.
|
|
94
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
30
95
|
module DatabaseLogger
|
|
96
|
+
# Data structure for captured command metadata
|
|
97
|
+
CommandMessage = Data.define(:command, :μs, :timeline) do
|
|
98
|
+
alias_method :to_a, :deconstruct
|
|
99
|
+
|
|
100
|
+
def inspect
|
|
101
|
+
cmd, duration, timeline = to_a
|
|
102
|
+
format('%.6f %4dμs > %s', timeline, duration, cmd)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
31
106
|
@logger = nil
|
|
32
107
|
@commands = Concurrent::Array.new
|
|
33
108
|
@max_commands = 10_000
|
|
34
109
|
@process_start = Time.now.to_f.freeze
|
|
35
|
-
|
|
36
|
-
|
|
110
|
+
@structured_logging = false
|
|
111
|
+
@sample_rate = nil # nil = log everything, 0.1 = 10%, 0.01 = 1%
|
|
112
|
+
@sample_counter = Concurrent::AtomicFixnum.new(0)
|
|
113
|
+
@commands_mutex = Mutex.new # Protects compound operations on @commands
|
|
37
114
|
|
|
38
115
|
class << self
|
|
39
116
|
# Gets/sets the logger instance used by DatabaseLogger.
|
|
@@ -44,8 +121,41 @@ module DatabaseLogger
|
|
|
44
121
|
# @return [Integer] The maximum number of commands to capture.
|
|
45
122
|
attr_accessor :max_commands
|
|
46
123
|
|
|
124
|
+
# Gets/sets structured logging mode.
|
|
125
|
+
# When enabled, outputs Redis commands with structured key=value context
|
|
126
|
+
# instead of formatted string output.
|
|
127
|
+
#
|
|
128
|
+
# @return [Boolean] Whether structured logging is enabled
|
|
129
|
+
#
|
|
130
|
+
# @example Enable structured logging
|
|
131
|
+
# DatabaseLogger.structured_logging = true
|
|
132
|
+
# # Outputs: "Redis command cmd=SET args=[key, value] duration_ms=0.42 db=0"
|
|
133
|
+
#
|
|
134
|
+
# @example Disable (default formatted output)
|
|
135
|
+
# DatabaseLogger.structured_logging = false
|
|
136
|
+
# # Outputs: "[123] 0.001234 567μs > SET key value"
|
|
137
|
+
attr_accessor :structured_logging
|
|
138
|
+
|
|
139
|
+
# Gets/sets the sampling rate for logging.
|
|
140
|
+
# Controls what percentage of commands are logged to reduce noise.
|
|
141
|
+
#
|
|
142
|
+
# @return [Float, nil] Sample rate (0.0-1.0) or nil for no sampling
|
|
143
|
+
#
|
|
144
|
+
# @example Log 10% of commands
|
|
145
|
+
# DatabaseLogger.sample_rate = 0.1
|
|
146
|
+
#
|
|
147
|
+
# @example Log 1% of commands (high-traffic production)
|
|
148
|
+
# DatabaseLogger.sample_rate = 0.01
|
|
149
|
+
#
|
|
150
|
+
# @example Disable sampling (log everything)
|
|
151
|
+
# DatabaseLogger.sample_rate = nil
|
|
152
|
+
#
|
|
153
|
+
# @note Command capture is unaffected - only logger output is sampled.
|
|
154
|
+
# This means tests can still verify commands while production logs stay clean.
|
|
155
|
+
attr_accessor :sample_rate
|
|
156
|
+
|
|
47
157
|
# Gets the captured commands for testing purposes.
|
|
48
|
-
# @return [Array] Array of
|
|
158
|
+
# @return [Array<CommandMessage>] Array of captured command messages
|
|
49
159
|
attr_reader :commands
|
|
50
160
|
|
|
51
161
|
# Gets the timestamp when DatabaseLogger was loaded.
|
|
@@ -53,9 +163,14 @@ module DatabaseLogger
|
|
|
53
163
|
attr_reader :process_start
|
|
54
164
|
|
|
55
165
|
# Clears the captured commands array.
|
|
56
|
-
#
|
|
166
|
+
#
|
|
167
|
+
# Thread-safe via mutex to ensure test isolation.
|
|
168
|
+
#
|
|
169
|
+
# @return [nil]
|
|
57
170
|
def clear_commands
|
|
58
|
-
@
|
|
171
|
+
@commands_mutex.synchronize do
|
|
172
|
+
@commands.clear
|
|
173
|
+
end
|
|
59
174
|
nil
|
|
60
175
|
end
|
|
61
176
|
|
|
@@ -63,163 +178,244 @@ module DatabaseLogger
|
|
|
63
178
|
# This is useful for testing to see what commands were executed.
|
|
64
179
|
#
|
|
65
180
|
# @yield [] The block of code to execute while capturing commands.
|
|
66
|
-
# @return [Array] Array of captured
|
|
67
|
-
# Each command is a hash with :command, :duration, :timestamp keys.
|
|
181
|
+
# @return [Array<CommandMessage>] Array of captured command messages
|
|
68
182
|
#
|
|
69
183
|
# @example Test what Redis commands your code executes
|
|
70
184
|
# commands = DatabaseLogger.capture_commands do
|
|
71
185
|
# my_library_method()
|
|
72
186
|
# end
|
|
73
|
-
# assert_equal "SET", commands.first
|
|
74
|
-
# assert commands.first
|
|
187
|
+
# assert_equal "SET", commands.first.command.split.first
|
|
188
|
+
# assert commands.first.μs > 0
|
|
75
189
|
def capture_commands
|
|
76
190
|
clear_commands
|
|
77
191
|
yield
|
|
78
192
|
@commands.to_a
|
|
79
193
|
end
|
|
80
194
|
|
|
81
|
-
#
|
|
195
|
+
# Gets the current count of captured commands.
|
|
196
|
+
# @return [Integer] The number of commands currently captured
|
|
197
|
+
def index
|
|
198
|
+
@commands.size
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Appends a command message to the captured commands array.
|
|
202
|
+
#
|
|
203
|
+
# When the array reaches max_commands capacity, the oldest command is
|
|
204
|
+
# removed before adding the new one.
|
|
82
205
|
#
|
|
83
|
-
# @param message [
|
|
84
|
-
# @return [Array] The updated array of commands
|
|
206
|
+
# @param message [CommandMessage] The command message to append
|
|
207
|
+
# @return [Array<CommandMessage>] The updated array of commands
|
|
208
|
+
# @api private
|
|
85
209
|
def append_command(message)
|
|
210
|
+
# We can throw away commands and not worry about thread race conditions
|
|
211
|
+
# since no one is going to mind if the command list is +/- a few
|
|
212
|
+
# commands. Unlike how we care about the order that the commands
|
|
213
|
+
# appear in the list, we don't care about exact count when trimming.
|
|
86
214
|
@commands.shift if @commands.size >= @max_commands
|
|
87
|
-
@commands << message
|
|
215
|
+
@commands << message # this is threadsafe thanks to Concurrent::Array
|
|
88
216
|
end
|
|
89
217
|
|
|
90
218
|
# Returns the current time in microseconds.
|
|
91
|
-
# This is used to measure the duration of
|
|
219
|
+
# This is used to measure the duration of Redis commands.
|
|
92
220
|
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# @return [Integer] The current time in microseconds.
|
|
221
|
+
# @return [Integer] The current time in microseconds
|
|
96
222
|
def now_in_μs
|
|
97
223
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
98
224
|
end
|
|
99
225
|
alias now_in_microseconds now_in_μs
|
|
226
|
+
|
|
227
|
+
# Determines if this command should be logged based on sampling rate.
|
|
228
|
+
#
|
|
229
|
+
# Uses deterministic modulo-based sampling for consistent behavior.
|
|
230
|
+
# Thread-safe via atomic counter increment.
|
|
231
|
+
#
|
|
232
|
+
# @return [Boolean] true if command should be logged
|
|
233
|
+
# @api private
|
|
234
|
+
def should_log?
|
|
235
|
+
return true if @sample_rate.nil?
|
|
236
|
+
return false if @logger.nil?
|
|
237
|
+
|
|
238
|
+
# Deterministic sampling: every Nth command where N = 1/sample_rate
|
|
239
|
+
# e.g., 0.1 = every 10th, 0.01 = every 100th
|
|
240
|
+
sample_interval = (1.0 / @sample_rate).to_i
|
|
241
|
+
(@sample_counter.increment % sample_interval).zero?
|
|
242
|
+
end
|
|
100
243
|
end
|
|
101
244
|
|
|
102
|
-
# Logs the
|
|
245
|
+
# Logs the Redis command and its execution time.
|
|
103
246
|
#
|
|
104
|
-
# This method is
|
|
105
|
-
#
|
|
247
|
+
# This method is part of the RedisClient middleware chain. It MUST use `super`
|
|
248
|
+
# instead of `yield` to properly chain with other middlewares.
|
|
106
249
|
#
|
|
107
|
-
# @param command [Array] The
|
|
108
|
-
# @param
|
|
109
|
-
#
|
|
110
|
-
# @return [Object] The result of the Database command execution.
|
|
250
|
+
# @param command [Array] The Redis command and its arguments
|
|
251
|
+
# @param config [RedisClient::Config, Hash] Connection configuration
|
|
252
|
+
# @return [Object] The result of the Redis command execution
|
|
111
253
|
#
|
|
112
|
-
# @note Commands are always captured
|
|
113
|
-
#
|
|
114
|
-
def call(command,
|
|
254
|
+
# @note Commands are always captured for testing. Logging only occurs when
|
|
255
|
+
# DatabaseLogger.logger is set and sampling allows it.
|
|
256
|
+
def call(command, config)
|
|
115
257
|
block_start = DatabaseLogger.now_in_μs
|
|
116
|
-
result = yield
|
|
258
|
+
result = super # CRITICAL: Must use super, not yield, to chain middlewares
|
|
117
259
|
block_duration = DatabaseLogger.now_in_μs - block_start
|
|
118
|
-
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
|
119
260
|
|
|
120
261
|
# We intentionally use two different codepaths for getting the
|
|
121
262
|
# time, although they will almost always be so similar that the
|
|
122
263
|
# difference is negligible.
|
|
123
|
-
|
|
124
|
-
DatabaseLogger.append_command(message)
|
|
125
|
-
|
|
126
|
-
# Log if logger is set
|
|
127
|
-
DatabaseLogger.logger&.debug(Oj.dump(message.to_h, mode: :strict))
|
|
128
|
-
|
|
129
|
-
result
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# DatabaseCommandCounter is Valkey/RedisClient middleware.
|
|
134
|
-
#
|
|
135
|
-
# This middleware counts the number of Database commands executed. It can be
|
|
136
|
-
# useful for performance monitoring and debugging, allowing you to track
|
|
137
|
-
# the volume of Database operations in your application.
|
|
138
|
-
#
|
|
139
|
-
# @example Enable Database command counting
|
|
140
|
-
# DatabaseCommandCounter.reset
|
|
141
|
-
# RedisClient.register(DatabaseCommandCounter)
|
|
142
|
-
#
|
|
143
|
-
# @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
|
|
144
|
-
#
|
|
145
|
-
module DatabaseCommandCounter
|
|
146
|
-
@count = Concurrent::AtomicFixnum.new(0)
|
|
264
|
+
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
|
147
265
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# a configuration where there's a connection to each logical db, there's only
|
|
151
|
-
# one when the connection is made. When using a provider of via thread local
|
|
152
|
-
# it could theoretically double the number of statements executed.
|
|
153
|
-
@skip_commands = ::Set.new(['SELECT']).freeze
|
|
266
|
+
msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
|
|
267
|
+
DatabaseLogger.append_command(msgpack)
|
|
154
268
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
269
|
+
# Dual-mode logging with sampling
|
|
270
|
+
if DatabaseLogger.should_log?
|
|
271
|
+
if DatabaseLogger.structured_logging && DatabaseLogger.logger
|
|
272
|
+
duration_ms = (block_duration / 1000.0).round(2)
|
|
273
|
+
db_num = if config.respond_to?(:db)
|
|
274
|
+
config.db
|
|
275
|
+
elsif config.is_a?(Hash)
|
|
276
|
+
config[:db]
|
|
277
|
+
end
|
|
278
|
+
DatabaseLogger.logger.trace(
|
|
279
|
+
"Redis command cmd=#{command.first} args=#{command[1..-1].inspect} " \
|
|
280
|
+
"duration_μs=#{block_duration} duration_ms=#{duration_ms} " \
|
|
281
|
+
"timeline=#{lifetime_duration} db=#{db_num} index=#{DatabaseLogger.index}"
|
|
282
|
+
)
|
|
283
|
+
elsif DatabaseLogger.logger
|
|
284
|
+
# Existing formatted output
|
|
285
|
+
message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
|
|
286
|
+
DatabaseLogger.logger.trace(message)
|
|
287
|
+
end
|
|
164
288
|
end
|
|
165
289
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
290
|
+
# Notify instrumentation hooks
|
|
291
|
+
if defined?(Familia::Instrumentation)
|
|
292
|
+
duration_ms = (block_duration / 1000.0).round(2)
|
|
293
|
+
db_num = if config.respond_to?(:db)
|
|
294
|
+
config.db
|
|
295
|
+
elsif config.is_a?(Hash)
|
|
296
|
+
config[:db]
|
|
297
|
+
end
|
|
298
|
+
conn_id = if config.respond_to?(:custom)
|
|
299
|
+
config.custom&.dig(:id)
|
|
300
|
+
elsif config.is_a?(Hash)
|
|
301
|
+
config.dig(:custom, :id)
|
|
302
|
+
end
|
|
303
|
+
Familia::Instrumentation.notify_command(
|
|
304
|
+
command.first,
|
|
305
|
+
duration_ms,
|
|
306
|
+
full_command: command,
|
|
307
|
+
db: db_num,
|
|
308
|
+
connection_id: conn_id,
|
|
309
|
+
)
|
|
171
310
|
end
|
|
172
311
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
312
|
+
result
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Handle pipelined commands (including MULTI/EXEC transactions)
|
|
316
|
+
#
|
|
317
|
+
# Captures MULTI/EXEC and shows you the full transaction. The WATCH
|
|
318
|
+
# and EXISTS appear separately because they're executed as individual
|
|
319
|
+
# commands before the transaction starts.
|
|
320
|
+
#
|
|
321
|
+
# @param commands [Array<Array>] Array of command arrays
|
|
322
|
+
# @param config [RedisClient::Config, Hash] Connection configuration
|
|
323
|
+
# @return [Array] Results from pipelined commands
|
|
324
|
+
def call_pipelined(commands, config)
|
|
325
|
+
block_start = DatabaseLogger.now_in_μs
|
|
326
|
+
results = yield # CRITICAL: For call_pipelined, yield is correct (not chaining)
|
|
327
|
+
block_duration = DatabaseLogger.now_in_μs - block_start
|
|
328
|
+
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
|
179
329
|
|
|
180
|
-
|
|
181
|
-
|
|
330
|
+
# Log the entire pipeline as a single operation
|
|
331
|
+
cmd_string = commands.map { |cmd| cmd.join(' ') }.join(' | ')
|
|
332
|
+
msgpack = CommandMessage.new(cmd_string, block_duration, lifetime_duration)
|
|
333
|
+
DatabaseLogger.append_command(msgpack)
|
|
334
|
+
|
|
335
|
+
# Dual-mode logging with sampling
|
|
336
|
+
if DatabaseLogger.should_log?
|
|
337
|
+
if DatabaseLogger.structured_logging && DatabaseLogger.logger
|
|
338
|
+
duration_ms = (block_duration / 1000.0).round(2)
|
|
339
|
+
db_num = if config.respond_to?(:db)
|
|
340
|
+
config.db
|
|
341
|
+
elsif config.is_a?(Hash)
|
|
342
|
+
config[:db]
|
|
343
|
+
end
|
|
344
|
+
DatabaseLogger.logger.trace(
|
|
345
|
+
"Redis pipeline commands=#{commands.size} duration_μs=#{block_duration} " \
|
|
346
|
+
"duration_ms=#{duration_ms} timeline=#{lifetime_duration} " \
|
|
347
|
+
"db=#{db_num} index=#{DatabaseLogger.index}"
|
|
348
|
+
)
|
|
349
|
+
elsif DatabaseLogger.logger
|
|
350
|
+
message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
|
|
351
|
+
DatabaseLogger.logger.trace(message)
|
|
352
|
+
end
|
|
182
353
|
end
|
|
183
354
|
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
end_count - start_count # Return the difference (commands executed in block)
|
|
355
|
+
# Notify instrumentation hooks
|
|
356
|
+
if defined?(Familia::Instrumentation)
|
|
357
|
+
duration_ms = (block_duration / 1000.0).round(2)
|
|
358
|
+
db_num = if config.respond_to?(:db)
|
|
359
|
+
config.db
|
|
360
|
+
elsif config.is_a?(Hash)
|
|
361
|
+
config[:db]
|
|
362
|
+
end
|
|
363
|
+
conn_id = if config.respond_to?(:custom)
|
|
364
|
+
config.custom&.dig(:id)
|
|
365
|
+
elsif config.is_a?(Hash)
|
|
366
|
+
config.dig(:custom, :id)
|
|
367
|
+
end
|
|
368
|
+
Familia::Instrumentation.notify_pipeline(
|
|
369
|
+
commands.size,
|
|
370
|
+
duration_ms,
|
|
371
|
+
db: db_num,
|
|
372
|
+
connection_id: conn_id
|
|
373
|
+
)
|
|
204
374
|
end
|
|
205
|
-
end
|
|
206
375
|
|
|
207
|
-
|
|
208
|
-
DatabaseCommandCounter
|
|
376
|
+
results
|
|
209
377
|
end
|
|
210
378
|
|
|
211
|
-
#
|
|
379
|
+
# Handle call_once for commands requiring dedicated connection handling:
|
|
212
380
|
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
381
|
+
# * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
|
|
382
|
+
# * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
|
|
383
|
+
# * Commands requiring connection affinity
|
|
384
|
+
# * Explicit non-pooled command execution
|
|
216
385
|
#
|
|
217
|
-
# @param command [Array] The
|
|
218
|
-
# @param
|
|
219
|
-
# @return [Object] The result of the
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
yield
|
|
386
|
+
# @param command [Array] The Redis command and its arguments
|
|
387
|
+
# @param config [RedisClient::Config, Hash] Connection configuration
|
|
388
|
+
# @return [Object] The result of the Redis command execution
|
|
389
|
+
def call_once(command, config)
|
|
390
|
+
block_start = DatabaseLogger.now_in_μs
|
|
391
|
+
result = yield # CRITICAL: For call_once, yield is correct (not chaining)
|
|
392
|
+
block_duration = DatabaseLogger.now_in_μs - block_start
|
|
393
|
+
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
|
394
|
+
|
|
395
|
+
msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
|
|
396
|
+
DatabaseLogger.append_command(msgpack)
|
|
397
|
+
|
|
398
|
+
# Dual-mode logging with sampling
|
|
399
|
+
if DatabaseLogger.should_log?
|
|
400
|
+
if DatabaseLogger.structured_logging && DatabaseLogger.logger
|
|
401
|
+
duration_ms = (block_duration / 1000.0).round(2)
|
|
402
|
+
db_num = if config.respond_to?(:db)
|
|
403
|
+
config.db
|
|
404
|
+
elsif config.is_a?(Hash)
|
|
405
|
+
config[:db]
|
|
406
|
+
end
|
|
407
|
+
DatabaseLogger.logger.trace(
|
|
408
|
+
"Redis command_once cmd=#{command.first} args=#{command[1..-1].inspect} " \
|
|
409
|
+
"duration_μs=#{block_duration} duration_ms=#{duration_ms} " \
|
|
410
|
+
"timeline=#{lifetime_duration} db=#{db_num} index=#{DatabaseLogger.index}"
|
|
411
|
+
)
|
|
412
|
+
elsif DatabaseLogger.logger
|
|
413
|
+
message = format('[%s] %s', DatabaseLogger.index, msgpack.inspect)
|
|
414
|
+
DatabaseLogger.logger.trace(message)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
result
|
|
223
419
|
end
|
|
224
420
|
end
|
|
225
421
|
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
data/lib/multi_result.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# try/edge_cases/string_coercion_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative '../support/helpers/test_helpers'
|
|
4
6
|
|
|
@@ -130,15 +132,15 @@ process_identifier(@customer)
|
|
|
130
132
|
|
|
131
133
|
## Cleanup after test, 1
|
|
132
134
|
@metadata.delete!
|
|
133
|
-
#=>
|
|
135
|
+
#=> 1
|
|
134
136
|
|
|
135
137
|
## Cleanup after test, 2
|
|
136
138
|
@customer.delete!
|
|
137
|
-
#=>
|
|
139
|
+
#=> 1
|
|
138
140
|
|
|
139
141
|
## Cleanup after test, 3
|
|
140
142
|
@session.delete!
|
|
141
|
-
#=>
|
|
143
|
+
#=> 1
|
|
142
144
|
|
|
143
145
|
## to_s handles identifier errors gracefully
|
|
144
146
|
badboi = BadIdentifierTest.new
|
|
@@ -154,4 +156,4 @@ badboi.to_s # .include?('BadIdentifierTest')
|
|
|
154
156
|
|
|
155
157
|
## Delete customer2
|
|
156
158
|
[@customer2.exists?, @customer2.delete!]
|
|
157
|
-
#=> [false,
|
|
159
|
+
#=> [false, 0]
|