familia 2.0.0.pre19 → 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 +177 -112
- data/CLAUDE.md +28 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +20 -17
- 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 +161 -117
- 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-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 +4 -3
- 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 +2 -0
- data/lib/familia/connection/handlers.rb +2 -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 +2 -0
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/pipelined_core.rb +2 -0
- data/lib/familia/connection/transaction_core.rb +68 -0
- data/lib/familia/connection.rb +18 -3
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +2 -0
- data/lib/familia/data_type/database_commands.rb +2 -0
- data/lib/familia/data_type/serialization.rb +6 -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 +7 -5
- 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 +2 -0
- 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 +3 -1
- data/lib/familia/features/expiration.rb +12 -4
- 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 +138 -9
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +89 -21
- data/lib/familia/features/relationships/indexing.rb +3 -0
- data/lib/familia/features/relationships/indexing_relationship.rb +3 -1
- 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 +3 -1
- data/lib/familia/horreum/connection.rb +17 -1
- data/lib/familia/horreum/database_commands.rb +2 -0
- data/lib/familia/horreum/definition.rb +16 -6
- data/lib/familia/horreum/management.rb +212 -42
- data/lib/familia/horreum/persistence.rb +176 -108
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +23 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -0
- data/lib/familia/horreum.rb +15 -1
- 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 +92 -32
- 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 +2 -0
- 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 +295 -170
- 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 +2 -0
- 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 +4 -0
- 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 +2 -0
- 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 +2 -0
- 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 +4 -0
- 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 +4 -0
- data/try/integration/connection/pipeline_fallback_integration_try.rb +3 -0
- 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 +4 -0
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +4 -0
- data/try/integration/data_types/datatype_transactions_try.rb +4 -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 +4 -0
- 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 +4 -0
- data/try/integration/persistence_operations_try.rb +4 -0
- 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 +4 -0
- 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 +4 -0
- 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 +5 -1
- data/try/unit/horreum/automatic_index_validation_try.rb +2 -0
- data/try/unit/horreum/base_try.rb +4 -0
- data/try/unit/horreum/class_methods_try.rb +4 -0
- 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 +4 -0
- 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 +4 -0
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +4 -0
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +4 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +2 -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 +72 -17
- data/.github/workflows/code-quality.yml +0 -138
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +0 -91
- data/changelog.d/20251011_203905_delano_next.rst +0 -30
- data/changelog.d/20251011_212633_delano_next.rst +0 -13
- data/changelog.d/20251011_221253_delano_next.rst +0 -26
- 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,46 +1,117 @@
|
|
|
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
|
+
# ```
|
|
49
|
+
#
|
|
50
|
+
# ### Critical Implementation Detail: `super` vs `yield`
|
|
6
51
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# can be invaluable.
|
|
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.
|
|
11
55
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
|
31
|
-
|
|
32
|
-
@commands = Concurrent::Array.new
|
|
33
|
-
@max_commands = 10_000
|
|
34
|
-
@process_start = Time.now.to_f.freeze
|
|
35
|
-
|
|
96
|
+
# Data structure for captured command metadata
|
|
36
97
|
CommandMessage = Data.define(:command, :μs, :timeline) do
|
|
37
98
|
alias_method :to_a, :deconstruct
|
|
99
|
+
|
|
38
100
|
def inspect
|
|
39
101
|
cmd, duration, timeline = to_a
|
|
40
102
|
format('%.6f %4dμs > %s', timeline, duration, cmd)
|
|
41
103
|
end
|
|
42
104
|
end
|
|
43
105
|
|
|
106
|
+
@logger = nil
|
|
107
|
+
@commands = Concurrent::Array.new
|
|
108
|
+
@max_commands = 10_000
|
|
109
|
+
@process_start = Time.now.to_f.freeze
|
|
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
|
|
114
|
+
|
|
44
115
|
class << self
|
|
45
116
|
# Gets/sets the logger instance used by DatabaseLogger.
|
|
46
117
|
# @return [Logger, nil] The current logger instance or nil if not set.
|
|
@@ -50,8 +121,41 @@ module DatabaseLogger
|
|
|
50
121
|
# @return [Integer] The maximum number of commands to capture.
|
|
51
122
|
attr_accessor :max_commands
|
|
52
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
|
+
|
|
53
157
|
# Gets the captured commands for testing purposes.
|
|
54
|
-
# @return [Array] Array of
|
|
158
|
+
# @return [Array<CommandMessage>] Array of captured command messages
|
|
55
159
|
attr_reader :commands
|
|
56
160
|
|
|
57
161
|
# Gets the timestamp when DatabaseLogger was loaded.
|
|
@@ -59,9 +163,14 @@ module DatabaseLogger
|
|
|
59
163
|
attr_reader :process_start
|
|
60
164
|
|
|
61
165
|
# Clears the captured commands array.
|
|
62
|
-
#
|
|
166
|
+
#
|
|
167
|
+
# Thread-safe via mutex to ensure test isolation.
|
|
168
|
+
#
|
|
169
|
+
# @return [nil]
|
|
63
170
|
def clear_commands
|
|
64
|
-
@
|
|
171
|
+
@commands_mutex.synchronize do
|
|
172
|
+
@commands.clear
|
|
173
|
+
end
|
|
65
174
|
nil
|
|
66
175
|
end
|
|
67
176
|
|
|
@@ -69,63 +178,84 @@ module DatabaseLogger
|
|
|
69
178
|
# This is useful for testing to see what commands were executed.
|
|
70
179
|
#
|
|
71
180
|
# @yield [] The block of code to execute while capturing commands.
|
|
72
|
-
# @return [Array] Array of captured
|
|
73
|
-
# Each command is a hash with :command, :duration, :timestamp keys.
|
|
181
|
+
# @return [Array<CommandMessage>] Array of captured command messages
|
|
74
182
|
#
|
|
75
183
|
# @example Test what Redis commands your code executes
|
|
76
184
|
# commands = DatabaseLogger.capture_commands do
|
|
77
185
|
# my_library_method()
|
|
78
186
|
# end
|
|
79
|
-
# assert_equal "SET", commands.first
|
|
80
|
-
# assert commands.first
|
|
187
|
+
# assert_equal "SET", commands.first.command.split.first
|
|
188
|
+
# assert commands.first.μs > 0
|
|
81
189
|
def capture_commands
|
|
82
190
|
clear_commands
|
|
83
191
|
yield
|
|
84
192
|
@commands.to_a
|
|
85
193
|
end
|
|
86
194
|
|
|
87
|
-
# Gets the current count of
|
|
88
|
-
# @return [Integer] The number of
|
|
195
|
+
# Gets the current count of captured commands.
|
|
196
|
+
# @return [Integer] The number of commands currently captured
|
|
89
197
|
def index
|
|
90
198
|
@commands.size
|
|
91
199
|
end
|
|
92
200
|
|
|
93
|
-
#
|
|
201
|
+
# Appends a command message to the captured commands array.
|
|
94
202
|
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
203
|
+
# When the array reaches max_commands capacity, the oldest command is
|
|
204
|
+
# removed before adding the new one.
|
|
205
|
+
#
|
|
206
|
+
# @param message [CommandMessage] The command message to append
|
|
207
|
+
# @return [Array<CommandMessage>] The updated array of commands
|
|
208
|
+
# @api private
|
|
97
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.
|
|
98
214
|
@commands.shift if @commands.size >= @max_commands
|
|
99
|
-
@commands << message
|
|
215
|
+
@commands << message # this is threadsafe thanks to Concurrent::Array
|
|
100
216
|
end
|
|
101
217
|
|
|
102
218
|
# Returns the current time in microseconds.
|
|
103
|
-
# This is used to measure the duration of
|
|
104
|
-
#
|
|
105
|
-
# Alias: now_in_microseconds
|
|
219
|
+
# This is used to measure the duration of Redis commands.
|
|
106
220
|
#
|
|
107
|
-
# @return [Integer] The current time in microseconds
|
|
221
|
+
# @return [Integer] The current time in microseconds
|
|
108
222
|
def now_in_μs
|
|
109
223
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
110
224
|
end
|
|
111
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
|
|
112
243
|
end
|
|
113
244
|
|
|
114
|
-
# Logs the
|
|
245
|
+
# Logs the Redis command and its execution time.
|
|
115
246
|
#
|
|
116
|
-
# This method is
|
|
117
|
-
#
|
|
247
|
+
# This method is part of the RedisClient middleware chain. It MUST use `super`
|
|
248
|
+
# instead of `yield` to properly chain with other middlewares.
|
|
118
249
|
#
|
|
119
|
-
# @param command [Array] The
|
|
120
|
-
# @param
|
|
121
|
-
#
|
|
122
|
-
# @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
|
|
123
253
|
#
|
|
124
|
-
# @note Commands are always captured
|
|
125
|
-
#
|
|
126
|
-
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)
|
|
127
257
|
block_start = DatabaseLogger.now_in_μs
|
|
128
|
-
result = yield
|
|
258
|
+
result = super # CRITICAL: Must use super, not yield, to chain middlewares
|
|
129
259
|
block_duration = DatabaseLogger.now_in_μs - block_start
|
|
130
260
|
|
|
131
261
|
# We intentionally use two different codepaths for getting the
|
|
@@ -136,9 +266,48 @@ module DatabaseLogger
|
|
|
136
266
|
msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
|
|
137
267
|
DatabaseLogger.append_command(msgpack)
|
|
138
268
|
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
288
|
+
end
|
|
289
|
+
|
|
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
|
+
)
|
|
310
|
+
end
|
|
142
311
|
|
|
143
312
|
result
|
|
144
313
|
end
|
|
@@ -148,9 +317,13 @@ module DatabaseLogger
|
|
|
148
317
|
# Captures MULTI/EXEC and shows you the full transaction. The WATCH
|
|
149
318
|
# and EXISTS appear separately because they're executed as individual
|
|
150
319
|
# commands before the transaction starts.
|
|
151
|
-
|
|
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)
|
|
152
325
|
block_start = DatabaseLogger.now_in_μs
|
|
153
|
-
results = yield
|
|
326
|
+
results = yield # CRITICAL: For call_pipelined, yield is correct (not chaining)
|
|
154
327
|
block_duration = DatabaseLogger.now_in_μs - block_start
|
|
155
328
|
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
|
156
329
|
|
|
@@ -159,138 +332,90 @@ module DatabaseLogger
|
|
|
159
332
|
msgpack = CommandMessage.new(cmd_string, block_duration, lifetime_duration)
|
|
160
333
|
DatabaseLogger.append_command(msgpack)
|
|
161
334
|
|
|
162
|
-
|
|
163
|
-
DatabaseLogger.
|
|
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
|
|
353
|
+
end
|
|
354
|
+
|
|
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
|
+
)
|
|
374
|
+
end
|
|
164
375
|
|
|
165
376
|
results
|
|
166
377
|
end
|
|
167
378
|
|
|
168
|
-
# call_once
|
|
379
|
+
# Handle call_once for commands requiring dedicated connection handling:
|
|
169
380
|
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
381
|
+
# * Blocking commands (BLPOP, BRPOP, BRPOPLPUSH)
|
|
382
|
+
# * Pub/sub operations (SUBSCRIBE, PSUBSCRIBE)
|
|
383
|
+
# * Commands requiring connection affinity
|
|
384
|
+
# * Explicit non-pooled command execution
|
|
174
385
|
#
|
|
175
|
-
|
|
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)
|
|
176
390
|
block_start = DatabaseLogger.now_in_μs
|
|
177
|
-
result = yield
|
|
391
|
+
result = yield # CRITICAL: For call_once, yield is correct (not chaining)
|
|
178
392
|
block_duration = DatabaseLogger.now_in_μs - block_start
|
|
179
393
|
lifetime_duration = (Time.now.to_f - DatabaseLogger.process_start).round(6)
|
|
180
394
|
|
|
181
395
|
msgpack = CommandMessage.new(command.join(' '), block_duration, lifetime_duration)
|
|
182
396
|
DatabaseLogger.append_command(msgpack)
|
|
183
397
|
|
|
184
|
-
|
|
185
|
-
DatabaseLogger.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
#
|
|
203
|
-
module DatabaseCommandCounter
|
|
204
|
-
@count = Concurrent::AtomicFixnum.new(0)
|
|
205
|
-
|
|
206
|
-
# We skip SELECT because depending on how the Familia is connecting to redis
|
|
207
|
-
# the number of SELECT commands can be a lot or just a little. For example in
|
|
208
|
-
# a configuration where there's a connection to each logical db, there's only
|
|
209
|
-
# one when the connection is made. When using a provider of via thread local
|
|
210
|
-
# it could theoretically double the number of statements executed.
|
|
211
|
-
@skip_commands = ::Set.new(['SELECT']).freeze
|
|
212
|
-
|
|
213
|
-
class << self
|
|
214
|
-
# Gets the set of commands to skip counting.
|
|
215
|
-
# @return [UnsortedSet] The commands that won't be counted.
|
|
216
|
-
attr_reader :skip_commands
|
|
217
|
-
|
|
218
|
-
# Gets the current count of Database commands executed.
|
|
219
|
-
# @return [Integer] The number of Database commands executed.
|
|
220
|
-
def count
|
|
221
|
-
@count.value
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Resets the command count to zero.
|
|
225
|
-
# This method is thread-safe.
|
|
226
|
-
# @return [Integer] The reset count (always 0).
|
|
227
|
-
def reset
|
|
228
|
-
@count.value = 0
|
|
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
|
|
229
416
|
end
|
|
230
417
|
|
|
231
|
-
|
|
232
|
-
# This method is thread-safe.
|
|
233
|
-
# @return [Integer] The new count after incrementing.
|
|
234
|
-
def increment
|
|
235
|
-
@count.increment
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def skip_command?(command)
|
|
239
|
-
skip_commands.include?(command.first.to_s.upcase)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
# Counts the number of Database commands executed within a block.
|
|
243
|
-
#
|
|
244
|
-
# This method captures the command count before and after executing the
|
|
245
|
-
# provided block, returning the difference. This is useful for measuring
|
|
246
|
-
# how many Database commands are executed by a specific operation.
|
|
247
|
-
#
|
|
248
|
-
# @yield [] The block of code to execute while counting commands.
|
|
249
|
-
# @return [Integer] The number of Database commands executed within the block.
|
|
250
|
-
#
|
|
251
|
-
# @example Count commands in a block
|
|
252
|
-
# commands_executed = DatabaseCommandCounter.count_commands do
|
|
253
|
-
# dbclient.set('key1', 'value1')
|
|
254
|
-
# dbclient.get('key1')
|
|
255
|
-
# end
|
|
256
|
-
# # commands_executed will be 2
|
|
257
|
-
def count_commands
|
|
258
|
-
start_count = count # Capture the current command count before execution
|
|
259
|
-
yield # Execute the provided block
|
|
260
|
-
end_count = count # Capture the command count after execution
|
|
261
|
-
end_count - start_count # Return the difference (commands executed in block)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def klass
|
|
266
|
-
DatabaseCommandCounter
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# Counts the Database command and delegates its execution.
|
|
270
|
-
#
|
|
271
|
-
# This method is called for each Database command when the middleware is active.
|
|
272
|
-
# It increments the command count (unless the command is in the skip list)
|
|
273
|
-
# and then yields to execute the actual command.
|
|
274
|
-
#
|
|
275
|
-
# @param command [Array] The Database command and its arguments.
|
|
276
|
-
# @param _config [Hash] The configuration options for the Database connection.
|
|
277
|
-
# @return [Object] The result of the Database command execution.
|
|
278
|
-
def call(command, _config)
|
|
279
|
-
klass.increment unless klass.skip_command?(command)
|
|
280
|
-
yield
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def call_pipelined(commands, _config)
|
|
284
|
-
# Count all commands in the pipeline (except skipped ones)
|
|
285
|
-
commands.each do |command|
|
|
286
|
-
klass.increment unless klass.skip_command?(command)
|
|
287
|
-
end
|
|
288
|
-
yield
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
def call_once(command, _config)
|
|
292
|
-
klass.increment unless klass.skip_command?(command)
|
|
293
|
-
yield
|
|
418
|
+
result
|
|
294
419
|
end
|
|
295
420
|
end
|
|
296
421
|
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
data/lib/multi_result.rb
CHANGED