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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# lib/familia/thread_safety/monitor.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'concurrent-ruby'
|
|
6
|
+
|
|
7
|
+
module Familia
|
|
8
|
+
module ThreadSafety
|
|
9
|
+
# Thread safety monitoring for production observability
|
|
10
|
+
#
|
|
11
|
+
# Tracks mutex contention, race conditions, and synchronization metrics
|
|
12
|
+
# to provide insights into thread safety behavior in production.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# Familia::ThreadSafety::Monitor.start!
|
|
16
|
+
# # ... application runs ...
|
|
17
|
+
# report = Familia::ThreadSafety::Monitor.report
|
|
18
|
+
# puts report[:summary]
|
|
19
|
+
#
|
|
20
|
+
# @example Custom instrumentation
|
|
21
|
+
# Familia::ThreadSafety::Monitor.record_contention('connection_chain')
|
|
22
|
+
# Familia::ThreadSafety::Monitor.time_critical_section('field_registration') do
|
|
23
|
+
# # ... critical code ...
|
|
24
|
+
# end
|
|
25
|
+
class Monitor
|
|
26
|
+
class << self
|
|
27
|
+
def instance
|
|
28
|
+
@instance ||= new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Delegate all methods to singleton instance
|
|
32
|
+
def method_missing(method, *args, &block)
|
|
33
|
+
if instance.respond_to?(method)
|
|
34
|
+
instance.send(method, *args, &block)
|
|
35
|
+
else
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def respond_to_missing?(method, include_private = false)
|
|
41
|
+
instance.respond_to?(method) || super
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attr_reader :enabled, :started_at
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
@enabled = false
|
|
49
|
+
@started_at = nil
|
|
50
|
+
@mutex_contentions = Concurrent::AtomicFixnum.new(0)
|
|
51
|
+
@race_detections = Concurrent::AtomicFixnum.new(0)
|
|
52
|
+
@critical_sections = Concurrent::AtomicFixnum.new(0)
|
|
53
|
+
@deadlock_checks = Concurrent::AtomicFixnum.new(0)
|
|
54
|
+
|
|
55
|
+
# Track contention points with counts
|
|
56
|
+
@contention_points = Concurrent::Map.new
|
|
57
|
+
|
|
58
|
+
# Track wait time aggregates for critical sections (removed @wait_times to prevent memory leak)
|
|
59
|
+
@wait_time_totals = Concurrent::Map.new
|
|
60
|
+
@wait_time_counts = Concurrent::Map.new
|
|
61
|
+
|
|
62
|
+
# Track thread-local state for nested monitoring
|
|
63
|
+
@thread_state = Concurrent::Map.new
|
|
64
|
+
|
|
65
|
+
# Track concurrent operation counts
|
|
66
|
+
@concurrent_operations = Concurrent::Map.new
|
|
67
|
+
|
|
68
|
+
# Performance metrics
|
|
69
|
+
@section_timings = Concurrent::Map.new
|
|
70
|
+
@section_counts = Concurrent::Map.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Start monitoring
|
|
74
|
+
def start!
|
|
75
|
+
@enabled = true
|
|
76
|
+
@started_at = Time.now
|
|
77
|
+
reset_metrics
|
|
78
|
+
Familia.info("[ThreadSafety] Monitoring started")
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Stop monitoring
|
|
83
|
+
def stop!
|
|
84
|
+
@enabled = false
|
|
85
|
+
duration = @started_at ? Time.now - @started_at : 0
|
|
86
|
+
Familia.info("[ThreadSafety] Monitoring stopped after #{duration.round(2)}s")
|
|
87
|
+
@started_at = nil
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Reset all metrics
|
|
92
|
+
def reset_metrics
|
|
93
|
+
@mutex_contentions.value = 0
|
|
94
|
+
@race_detections.value = 0
|
|
95
|
+
@critical_sections.value = 0
|
|
96
|
+
@deadlock_checks.value = 0
|
|
97
|
+
@contention_points.clear
|
|
98
|
+
# @wait_times.clear - removed to prevent memory leak
|
|
99
|
+
@wait_time_totals.clear
|
|
100
|
+
@wait_time_counts.clear
|
|
101
|
+
@thread_state.clear
|
|
102
|
+
@concurrent_operations.clear
|
|
103
|
+
@section_timings.clear
|
|
104
|
+
@section_counts.clear
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Record a mutex contention event
|
|
108
|
+
def record_contention(location, wait_time = nil)
|
|
109
|
+
return unless @enabled
|
|
110
|
+
|
|
111
|
+
@mutex_contentions.increment
|
|
112
|
+
@contention_points[location] = @contention_points.fetch(location, 0) + 1
|
|
113
|
+
|
|
114
|
+
if wait_time
|
|
115
|
+
record_wait_time(location, wait_time)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Familia.trace(:THREAD_CONTENTION, nil, "Contention at #{location} (wait: #{wait_time&.round(4)}s)")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Record wait time for a location
|
|
122
|
+
def record_wait_time(location, wait_time)
|
|
123
|
+
# Note: @wait_times was removed to prevent memory leak from unbounded array growth
|
|
124
|
+
# We only need the aggregated totals and counts for calculations
|
|
125
|
+
@wait_time_totals[location] = @wait_time_totals.fetch(location, 0.0) + wait_time
|
|
126
|
+
@wait_time_counts[location] = @wait_time_counts.fetch(location, 0) + 1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Record a potential race condition detection
|
|
130
|
+
def record_race_condition(location, details = nil)
|
|
131
|
+
return unless @enabled
|
|
132
|
+
|
|
133
|
+
@race_detections.increment
|
|
134
|
+
msg = "Potential race condition at #{location}"
|
|
135
|
+
msg += ": #{details}" if details
|
|
136
|
+
Familia.warn("[ThreadSafety] #{msg}")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Time a critical section with contention tracking
|
|
140
|
+
def time_critical_section(name)
|
|
141
|
+
return yield unless @enabled
|
|
142
|
+
|
|
143
|
+
thread_id = Thread.current.object_id
|
|
144
|
+
start_time = Familia.now_in_μs
|
|
145
|
+
|
|
146
|
+
# Check for concurrent execution
|
|
147
|
+
concurrent_count = @concurrent_operations[name] = @concurrent_operations.fetch(name, 0) + 1
|
|
148
|
+
if concurrent_count > 1
|
|
149
|
+
record_contention(name)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
@critical_sections.increment
|
|
153
|
+
|
|
154
|
+
begin
|
|
155
|
+
result = yield
|
|
156
|
+
ensure
|
|
157
|
+
end_time = Familia.now_in_μs
|
|
158
|
+
duration_μs = end_time - start_time
|
|
159
|
+
|
|
160
|
+
# Record timing in microseconds
|
|
161
|
+
@section_timings[name] = @section_timings.fetch(name, 0) + duration_μs
|
|
162
|
+
@section_counts[name] = @section_counts.fetch(name, 0) + 1
|
|
163
|
+
|
|
164
|
+
# Decrement concurrent count
|
|
165
|
+
@concurrent_operations[name] = @concurrent_operations.fetch(name, 1) - 1
|
|
166
|
+
|
|
167
|
+
if duration_μs > 100_000 # Log slow critical sections (> 100ms = 100,000μs)
|
|
168
|
+
Familia.warn("[ThreadSafety] Slow critical section '#{name}': #{(duration_μs / 1000.0).round(2)}ms")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
result
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# NOTE: monitor_mutex method was removed as it was unused and had flawed
|
|
176
|
+
# exception handling that could lead to deadlocks. The InstrumentedMutex
|
|
177
|
+
# class should be used instead for mutex monitoring.
|
|
178
|
+
|
|
179
|
+
# Check for potential deadlocks
|
|
180
|
+
def check_deadlock
|
|
181
|
+
return unless @enabled
|
|
182
|
+
|
|
183
|
+
@deadlock_checks.increment
|
|
184
|
+
|
|
185
|
+
# This is a simple check - in production you might want more sophisticated detection
|
|
186
|
+
thread_count = Thread.list.count
|
|
187
|
+
if thread_count > 100
|
|
188
|
+
Familia.warn("[ThreadSafety] High thread count: #{thread_count}")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check for threads waiting on mutexes (simplified)
|
|
192
|
+
waiting_threads = Thread.list.select { |t| t.status == "sleep" }
|
|
193
|
+
if waiting_threads.size > thread_count * 0.8
|
|
194
|
+
Familia.warn("[ThreadSafety] Potential deadlock: #{waiting_threads.size}/#{thread_count} threads sleeping")
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Generate a comprehensive report
|
|
199
|
+
def report
|
|
200
|
+
return { enabled: false, message: "Monitoring not enabled" } unless @started_at
|
|
201
|
+
|
|
202
|
+
duration = Time.now - @started_at
|
|
203
|
+
|
|
204
|
+
# Calculate hot spots
|
|
205
|
+
hot_spots = []
|
|
206
|
+
@contention_points.each_pair do |location, count|
|
|
207
|
+
hot_spots << [location, count]
|
|
208
|
+
end
|
|
209
|
+
hot_spots = hot_spots
|
|
210
|
+
.sort_by { |_, count| -count }
|
|
211
|
+
.first(10)
|
|
212
|
+
.map { |location, count|
|
|
213
|
+
avg_wait_μs = if @wait_time_counts[location] && @wait_time_counts[location] > 0
|
|
214
|
+
(@wait_time_totals[location] / @wait_time_counts[location]).round(0)
|
|
215
|
+
else
|
|
216
|
+
0
|
|
217
|
+
end
|
|
218
|
+
{
|
|
219
|
+
location: location,
|
|
220
|
+
contentions: count,
|
|
221
|
+
avg_wait_μs: avg_wait_μs
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Calculate critical section performance
|
|
226
|
+
section_performance = []
|
|
227
|
+
@section_counts.each_pair do |name, count|
|
|
228
|
+
avg_time_μs = (@section_timings[name] / count).round(0)
|
|
229
|
+
section_performance << {
|
|
230
|
+
section: name,
|
|
231
|
+
calls: count,
|
|
232
|
+
avg_time_μs: avg_time_μs,
|
|
233
|
+
total_time_μs: @section_timings[name]
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
section_performance.sort_by! { |s| -s[:total_time_μs] }
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
summary: {
|
|
240
|
+
monitoring_duration_s: duration.round(2),
|
|
241
|
+
mutex_contentions: @mutex_contentions.value,
|
|
242
|
+
race_detections: @race_detections.value,
|
|
243
|
+
critical_sections: @critical_sections.value,
|
|
244
|
+
deadlock_checks: @deadlock_checks.value
|
|
245
|
+
},
|
|
246
|
+
hot_spots: hot_spots,
|
|
247
|
+
section_performance: section_performance,
|
|
248
|
+
health: calculate_health_score,
|
|
249
|
+
recommendations: generate_recommendations(hot_spots)
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Calculate a health score (0-100)
|
|
254
|
+
def calculate_health_score
|
|
255
|
+
return 100 unless @started_at
|
|
256
|
+
|
|
257
|
+
duration = Time.now - @started_at
|
|
258
|
+
return 100 if duration < 60 # Need at least 1 minute of data
|
|
259
|
+
|
|
260
|
+
contentions_per_hour = (@mutex_contentions.value / duration) * 3600
|
|
261
|
+
races_per_hour = (@race_detections.value / duration) * 3600
|
|
262
|
+
|
|
263
|
+
score = 100
|
|
264
|
+
score -= [contentions_per_hour / 10.0, 30].min # -3 points per 100 contentions/hour, max -30
|
|
265
|
+
score -= [races_per_hour * 10, 50].min # -10 points per race/hour, max -50
|
|
266
|
+
|
|
267
|
+
[score, 0].max.round
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Generate recommendations based on metrics
|
|
271
|
+
def generate_recommendations(hot_spots)
|
|
272
|
+
recommendations = []
|
|
273
|
+
|
|
274
|
+
if @race_detections.value > 0
|
|
275
|
+
recommendations << {
|
|
276
|
+
severity: 'critical',
|
|
277
|
+
message: "#{@race_detections.value} potential race conditions detected - investigate immediately"
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if hot_spots.any? { |h| h[:contentions] > 100 }
|
|
282
|
+
high_contention = hot_spots.select { |h| h[:contentions] > 100 }
|
|
283
|
+
locations = high_contention.map { |h| h[:location] }.join(', ')
|
|
284
|
+
recommendations << {
|
|
285
|
+
severity: 'warning',
|
|
286
|
+
message: "High contention detected at: #{locations}"
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
if hot_spots.any? { |h| h[:avg_wait_μs] > 100_000 } # > 100ms in microseconds
|
|
291
|
+
slow_spots = hot_spots.select { |h| h[:avg_wait_μs] > 100_000 }
|
|
292
|
+
recommendations << {
|
|
293
|
+
severity: 'warning',
|
|
294
|
+
message: "Long wait times at: #{slow_spots.map { |h| "#{h[:location]} (#{(h[:avg_wait_μs] / 1000.0).round(1)}ms)" }.join(', ')}"
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if @deadlock_checks.value > 0 && Thread.list.count > 50
|
|
299
|
+
recommendations << {
|
|
300
|
+
severity: 'info',
|
|
301
|
+
message: "Consider connection pooling - high thread count detected"
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
recommendations
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Export metrics in a format suitable for APM tools
|
|
309
|
+
def export_metrics
|
|
310
|
+
{
|
|
311
|
+
'familia.thread_safety.mutex_contentions' => @mutex_contentions.value,
|
|
312
|
+
'familia.thread_safety.race_detections' => @race_detections.value,
|
|
313
|
+
'familia.thread_safety.critical_sections' => @critical_sections.value,
|
|
314
|
+
'familia.thread_safety.deadlock_checks' => @deadlock_checks.value,
|
|
315
|
+
'familia.thread_safety.health_score' => calculate_health_score
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Hook for APM integration
|
|
320
|
+
def apm_transaction(name, &block)
|
|
321
|
+
return yield unless @enabled
|
|
322
|
+
|
|
323
|
+
# This is where you'd integrate with NewRelic, DataDog, etc.
|
|
324
|
+
time_critical_section(name, &block)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
data/lib/familia/utils.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/utils.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
4
6
|
# Family-related utility methods
|
|
@@ -42,6 +44,17 @@ module Familia
|
|
|
42
44
|
current_time.utc.to_f
|
|
43
45
|
end
|
|
44
46
|
|
|
47
|
+
# Returns the current time in microseconds.
|
|
48
|
+
# This is used to measure the duration of Database commands.
|
|
49
|
+
#
|
|
50
|
+
# Alias: now_in_microseconds
|
|
51
|
+
#
|
|
52
|
+
# @return [Integer] The current time in microseconds.
|
|
53
|
+
def now_in_μs
|
|
54
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
55
|
+
end
|
|
56
|
+
alias now_in_microseconds now_in_μs
|
|
57
|
+
|
|
45
58
|
# A quantized timestamp
|
|
46
59
|
#
|
|
47
60
|
# @param quantum [Integer] The time quantum in seconds (default: 10 minutes).
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lib/familia/verifiable_identifier.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require 'openssl'
|
|
4
6
|
require_relative 'secure_identifier'
|
|
@@ -33,7 +35,7 @@ module Familia
|
|
|
33
35
|
# $ openssl rand -hex 32
|
|
34
36
|
# > cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d
|
|
35
37
|
#
|
|
36
|
-
# 2.
|
|
38
|
+
# 2. Set it as an environment variable in your production environment:
|
|
37
39
|
# export VERIFIABLE_ID_HMAC_SECRET="cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
|
|
38
40
|
#
|
|
39
41
|
SECRET_KEY = ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')
|
data/lib/familia/version.rb
CHANGED
data/lib/familia.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# lib/familia.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require 'oj'
|
|
4
6
|
require 'redis'
|
|
5
7
|
require 'uri/valkey'
|
|
6
8
|
require 'connection_pool'
|
|
9
|
+
require 'concurrent-ruby'
|
|
7
10
|
|
|
8
11
|
# OJ configuration is handled internally by Familia::JsonSerializer
|
|
9
12
|
|
|
@@ -11,6 +14,8 @@ require_relative 'multi_result'
|
|
|
11
14
|
require_relative 'familia/refinements'
|
|
12
15
|
require_relative 'familia/errors'
|
|
13
16
|
require_relative 'familia/version'
|
|
17
|
+
require_relative 'familia/thread_safety/monitor'
|
|
18
|
+
require_relative 'familia/thread_safety/instrumented_mutex'
|
|
14
19
|
|
|
15
20
|
# Familia - A family warehouse for Valkey/Redis
|
|
16
21
|
#
|
|
@@ -39,9 +44,30 @@ module Familia
|
|
|
39
44
|
using Refinements::StylizeWords
|
|
40
45
|
|
|
41
46
|
class << self
|
|
42
|
-
|
|
47
|
+
attr_writer :debug
|
|
43
48
|
attr_reader :members
|
|
44
49
|
|
|
50
|
+
# Thread safety monitoring controls
|
|
51
|
+
def thread_safety_monitor
|
|
52
|
+
ThreadSafety::Monitor.instance
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def start_monitoring!
|
|
56
|
+
thread_safety_monitor.start!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def stop_monitoring!
|
|
60
|
+
thread_safety_monitor.stop!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def thread_safety_report
|
|
64
|
+
thread_safety_monitor.report
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def thread_safety_metrics
|
|
68
|
+
thread_safety_monitor.export_metrics
|
|
69
|
+
end
|
|
70
|
+
|
|
45
71
|
def included(member)
|
|
46
72
|
raise Problem, "#{member} should subclass Familia::Horreum"
|
|
47
73
|
end
|
|
@@ -87,7 +113,7 @@ module Familia
|
|
|
87
113
|
# @param klass [Class] The class to remove from members
|
|
88
114
|
# @return [Class, nil] The removed class or nil if not found
|
|
89
115
|
def unload_member(klass)
|
|
90
|
-
Familia.
|
|
116
|
+
Familia.debug "[unload_member] Removing #{klass} from members"
|
|
91
117
|
@members.delete(klass)
|
|
92
118
|
end
|
|
93
119
|
|
|
@@ -97,7 +123,7 @@ module Familia
|
|
|
97
123
|
# @return [Array<Class>] The removed anonymous classes
|
|
98
124
|
def clear_anonymous_members
|
|
99
125
|
anonymous_classes = @members.select { |m| m.name.nil? }
|
|
100
|
-
Familia.
|
|
126
|
+
Familia.debug "[clear_anonymous_members] Removing #{anonymous_classes.size} anonymous classes"
|
|
101
127
|
@members.reject! { |m| m.name.nil? }
|
|
102
128
|
anonymous_classes
|
|
103
129
|
end
|
|
@@ -128,7 +154,7 @@ module Familia
|
|
|
128
154
|
# Familia.member_by_config_name(:nonexistent) # => nil
|
|
129
155
|
#
|
|
130
156
|
def member_by_config_name(config_name)
|
|
131
|
-
Familia.
|
|
157
|
+
Familia.debug "[member_by_config_name] #{members.map(&:config_name)} #{config_name}"
|
|
132
158
|
|
|
133
159
|
members.find { |m| m.config_name.to_s.eql?(config_name.to_s) }
|
|
134
160
|
end
|
|
@@ -149,6 +175,7 @@ module Familia
|
|
|
149
175
|
extend Utils
|
|
150
176
|
end
|
|
151
177
|
|
|
178
|
+
require_relative 'familia/instrumentation'
|
|
152
179
|
require_relative 'familia/base'
|
|
153
180
|
require_relative 'familia/features'
|
|
154
181
|
require_relative 'familia/data_type'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# lib/middleware/database_command_counter.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require 'concurrent-ruby'
|
|
6
|
+
|
|
7
|
+
# DatabaseCommandCounter is redis-rb middleware for counting commands.
|
|
8
|
+
#
|
|
9
|
+
# This middleware counts the number of Redis commands executed. It can be
|
|
10
|
+
# useful for performance monitoring and debugging, allowing you to track
|
|
11
|
+
# the volume of Redis operations in your application.
|
|
12
|
+
#
|
|
13
|
+
# Familia uses the redis-rb gem (v4.8.1 to <6.0), which internally uses
|
|
14
|
+
# RedisClient infrastructure. Users work with Redis.new connections - the
|
|
15
|
+
# RedisClient middleware registration is handled automatically by Familia.
|
|
16
|
+
#
|
|
17
|
+
# ## User-Facing API
|
|
18
|
+
#
|
|
19
|
+
# Enable via Familia configuration:
|
|
20
|
+
# Familia.enable_database_counter = true
|
|
21
|
+
#
|
|
22
|
+
# Familia automatically calls RedisClient.register(DatabaseCommandCounter) internally.
|
|
23
|
+
#
|
|
24
|
+
# ## Middleware Chaining
|
|
25
|
+
#
|
|
26
|
+
# This middleware works correctly alongside DatabaseLogger because it uses
|
|
27
|
+
# `super` to properly chain method calls. See {DatabaseLogger} for detailed
|
|
28
|
+
# explanation of middleware chaining mechanics.
|
|
29
|
+
#
|
|
30
|
+
# @example Enable Redis command counting (recommended user-facing API)
|
|
31
|
+
# DatabaseCommandCounter.reset
|
|
32
|
+
# Familia.enable_database_counter = true
|
|
33
|
+
#
|
|
34
|
+
# @example Use with DatabaseLogger
|
|
35
|
+
# Familia.enable_database_logging = true
|
|
36
|
+
# Familia.enable_database_counter = true
|
|
37
|
+
# # Both middlewares registered automatically and execute correctly in sequence
|
|
38
|
+
#
|
|
39
|
+
# @see https://github.com/redis-rb/redis-client?tab=readme-ov-file#instrumentation-and-middlewares
|
|
40
|
+
# @see DatabaseLogger For middleware chain architecture details
|
|
41
|
+
#
|
|
42
|
+
module DatabaseCommandCounter
|
|
43
|
+
@count = Concurrent::AtomicFixnum.new(0)
|
|
44
|
+
|
|
45
|
+
# Commands to skip when counting.
|
|
46
|
+
#
|
|
47
|
+
# We skip SELECT because the frequency depends on connection architecture:
|
|
48
|
+
# - Connection-per-database: Only one SELECT when connection is made
|
|
49
|
+
# - Provider/thread-local: Could theoretically double statement count
|
|
50
|
+
#
|
|
51
|
+
# @return [Set<String>] Commands that won't be counted
|
|
52
|
+
@skip_commands = ::Set.new(['SELECT']).freeze
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
# Gets the set of commands to skip counting.
|
|
56
|
+
# @return [Set<String>] The commands that won't be counted
|
|
57
|
+
attr_reader :skip_commands
|
|
58
|
+
|
|
59
|
+
# Gets the current count of Redis commands executed.
|
|
60
|
+
# @return [Integer] The number of Redis commands executed
|
|
61
|
+
def count
|
|
62
|
+
@count.value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resets the command count to zero.
|
|
66
|
+
# This method is thread-safe.
|
|
67
|
+
# @return [Integer] The reset count (always 0)
|
|
68
|
+
def reset
|
|
69
|
+
@count.value = 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Increments the command count.
|
|
73
|
+
# This method is thread-safe.
|
|
74
|
+
# @return [Integer] The new count after incrementing
|
|
75
|
+
# @api private
|
|
76
|
+
def increment
|
|
77
|
+
@count.increment
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Checks if a command should be skipped.
|
|
81
|
+
# @param command [Array] The Redis command array
|
|
82
|
+
# @return [Boolean] true if command should be skipped
|
|
83
|
+
# @api private
|
|
84
|
+
def skip_command?(command)
|
|
85
|
+
skip_commands.include?(command.first.to_s.upcase)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Counts the number of Redis commands executed within a block.
|
|
89
|
+
#
|
|
90
|
+
# This method captures the command count before and after executing the
|
|
91
|
+
# provided block, returning the difference. This is useful for measuring
|
|
92
|
+
# how many Redis commands are executed by a specific operation.
|
|
93
|
+
#
|
|
94
|
+
# @yield [] The block of code to execute while counting commands
|
|
95
|
+
# @return [Integer] The number of Redis commands executed within the block
|
|
96
|
+
#
|
|
97
|
+
# @example Count commands in a block
|
|
98
|
+
# commands_executed = DatabaseCommandCounter.count_commands do
|
|
99
|
+
# dbclient.set('key1', 'value1')
|
|
100
|
+
# dbclient.get('key1')
|
|
101
|
+
# end
|
|
102
|
+
# # commands_executed will be 2
|
|
103
|
+
def count_commands
|
|
104
|
+
start_count = count # Capture the current command count before execution
|
|
105
|
+
yield # Execute the provided block
|
|
106
|
+
end_count = count # Capture the command count after execution
|
|
107
|
+
end_count - start_count # Return the difference (commands executed in block)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Reference to the module for use in instance methods
|
|
112
|
+
# @api private
|
|
113
|
+
def klass
|
|
114
|
+
DatabaseCommandCounter
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Counts the Redis command and delegates its execution.
|
|
118
|
+
#
|
|
119
|
+
# This method is part of the RedisClient middleware chain. It MUST use `super`
|
|
120
|
+
# instead of `yield` to properly chain with other middlewares like DatabaseLogger.
|
|
121
|
+
#
|
|
122
|
+
# @param command [Array] The Redis command and its arguments
|
|
123
|
+
# @param _config [RedisClient::Config, Hash] Connection configuration (unused)
|
|
124
|
+
# @return [Object] The result of the Redis command execution
|
|
125
|
+
def call(command, _config)
|
|
126
|
+
klass.increment unless klass.skip_command?(command)
|
|
127
|
+
super # CRITICAL: Must use super, not yield, to chain middlewares
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Counts commands in a pipeline and delegates execution.
|
|
131
|
+
#
|
|
132
|
+
# @param commands [Array<Array>] Array of command arrays
|
|
133
|
+
# @param _config [RedisClient::Config, Hash] Connection configuration (unused)
|
|
134
|
+
# @return [Array] Results from pipelined commands
|
|
135
|
+
def call_pipelined(commands, _config)
|
|
136
|
+
# Count all commands in the pipeline (except skipped ones)
|
|
137
|
+
commands.each do |command|
|
|
138
|
+
klass.increment unless klass.skip_command?(command)
|
|
139
|
+
end
|
|
140
|
+
super # CRITICAL: Must use super, not yield, to chain middlewares
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Counts a call_once command and delegates execution.
|
|
144
|
+
#
|
|
145
|
+
# @param command [Array] The Redis command and its arguments
|
|
146
|
+
# @param _config [RedisClient::Config, Hash] Connection configuration (unused)
|
|
147
|
+
# @return [Object] The result of the Redis command execution
|
|
148
|
+
def call_once(command, _config)
|
|
149
|
+
klass.increment unless klass.skip_command?(command)
|
|
150
|
+
super # CRITICAL: Must use super, not yield, to chain middlewares
|
|
151
|
+
end
|
|
152
|
+
end
|