familia 2.0.0.pre18 → 2.0.0.pre21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +4 -9
- data/.github/workflows/code-smells.yml +64 -3
- data/.pre-commit-config.yaml +8 -6
- data/.reek.yml +10 -9
- data/.rubocop.yml +4 -0
- data/CHANGELOG.rst +205 -88
- data/CLAUDE.md +62 -10
- data/Gemfile +3 -3
- data/Gemfile.lock +27 -62
- data/README.md +39 -0
- data/bin/try +16 -0
- data/bin/tryouts +16 -0
- data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
- data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
- data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
- data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
- data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
- data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
- data/docs/1106-participates_in-bidirectional-solution.md +129 -0
- data/docs/guides/encryption.md +486 -0
- data/docs/guides/feature-encrypted-fields.md +123 -7
- data/docs/guides/feature-expiration.md +177 -133
- data/docs/guides/feature-external-identifiers.md +415 -443
- data/docs/guides/feature-object-identifiers.md +400 -269
- data/docs/guides/feature-quantization.md +120 -6
- data/docs/guides/feature-relationships-indexing.md +318 -0
- data/docs/guides/feature-relationships-methods.md +146 -604
- data/docs/guides/feature-relationships-participation.md +263 -0
- data/docs/guides/feature-relationships.md +118 -136
- data/docs/guides/feature-system-devs.md +176 -693
- data/docs/guides/feature-system.md +119 -6
- data/docs/guides/feature-transient-fields.md +81 -0
- data/docs/guides/field-system.md +778 -0
- data/docs/guides/index.md +32 -15
- data/docs/guides/logging.md +187 -0
- data/docs/guides/optimized-loading.md +674 -0
- data/docs/guides/thread-safety-monitoring.md +61 -0
- data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/migrating/v2.0.0-pre22.md +241 -0
- data/docs/overview.md +7 -9
- data/docs/reference/api-technical.md +267 -320
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
- data/examples/autoloader/mega_customer.rb +2 -0
- data/examples/datatype_standalone.rb +282 -0
- data/examples/encrypted_fields.rb +2 -1
- data/examples/json_usage_patterns.rb +2 -0
- data/examples/relationships.rb +3 -0
- data/examples/safe_dump.rb +2 -1
- data/examples/sampling_demo.rb +53 -0
- data/examples/single_connection_transaction_confusions.rb +2 -1
- data/familia.gemspec +2 -1
- data/lib/familia/base.rb +2 -0
- data/lib/familia/connection/behavior.rb +254 -0
- data/lib/familia/connection/handlers.rb +97 -0
- data/lib/familia/connection/individual_command_proxy.rb +2 -0
- data/lib/familia/connection/middleware.rb +34 -24
- data/lib/familia/connection/operation_core.rb +3 -1
- data/lib/familia/connection/operations.rb +2 -0
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
- data/lib/familia/connection/transaction_core.rb +75 -9
- data/lib/familia/connection.rb +21 -5
- data/lib/familia/data_type/class_methods.rb +3 -1
- data/lib/familia/data_type/connection.rb +153 -7
- data/lib/familia/data_type/database_commands.rb +9 -4
- data/lib/familia/data_type/serialization.rb +10 -4
- data/lib/familia/data_type/settings.rb +2 -0
- data/lib/familia/data_type/types/counter.rb +2 -0
- data/lib/familia/data_type/types/hashkey.rb +8 -6
- data/lib/familia/data_type/types/listkey.rb +2 -0
- data/lib/familia/data_type/types/lock.rb +2 -0
- data/lib/familia/data_type/types/sorted_set.rb +2 -0
- data/lib/familia/data_type/types/stringkey.rb +2 -0
- data/lib/familia/data_type/types/unsorted_set.rb +2 -0
- data/lib/familia/data_type.rb +2 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -2
- data/lib/familia/encryption/manager.rb +2 -0
- data/lib/familia/encryption/provider.rb +2 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
- data/lib/familia/encryption/registry.rb +2 -0
- data/lib/familia/encryption/request_cache.rb +2 -0
- data/lib/familia/encryption.rb +9 -2
- data/lib/familia/errors.rb +53 -14
- data/lib/familia/features/autoloader.rb +2 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
- data/lib/familia/features/encrypted_fields.rb +2 -2
- data/lib/familia/features/expiration/extensions.rb +11 -11
- data/lib/familia/features/expiration.rb +29 -21
- data/lib/familia/features/external_identifier.rb +33 -7
- data/lib/familia/features/object_identifier.rb +2 -0
- data/lib/familia/features/quantization.rb +3 -1
- data/lib/familia/features/relationships/README.md +3 -1
- data/lib/familia/features/relationships/collection_operations.rb +2 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
- data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
- data/lib/familia/features/relationships/indexing.rb +40 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
- data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
- data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
- data/lib/familia/features/relationships/participation.rb +155 -69
- data/lib/familia/features/relationships/participation_membership.rb +69 -0
- data/lib/familia/features/relationships/participation_relationship.rb +34 -6
- data/lib/familia/features/relationships/score_encoding.rb +2 -0
- data/lib/familia/features/relationships.rb +5 -3
- data/lib/familia/features/safe_dump.rb +2 -0
- data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
- data/lib/familia/features/transient_fields.rb +2 -0
- data/lib/familia/features.rb +2 -0
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/connection.rb +28 -36
- data/lib/familia/horreum/database_commands.rb +131 -10
- data/lib/familia/horreum/definition.rb +18 -7
- data/lib/familia/horreum/management.rb +233 -57
- data/lib/familia/horreum/persistence.rb +314 -122
- data/lib/familia/horreum/related_fields.rb +2 -0
- data/lib/familia/horreum/serialization.rb +26 -4
- data/lib/familia/horreum/settings.rb +2 -0
- data/lib/familia/horreum/utils.rb +2 -8
- data/lib/familia/horreum.rb +46 -13
- data/lib/familia/identifier_extractor.rb +2 -0
- data/lib/familia/instrumentation.rb +156 -0
- data/lib/familia/json_serializer.rb +2 -0
- data/lib/familia/logging.rb +94 -37
- data/lib/familia/refinements/dear_json.rb +2 -0
- data/lib/familia/refinements/stylize_words.rb +2 -14
- data/lib/familia/refinements/time_literals.rb +2 -0
- data/lib/familia/refinements.rb +2 -0
- data/lib/familia/secure_identifier.rb +10 -2
- data/lib/familia/settings.rb +9 -7
- data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
- data/lib/familia/thread_safety/monitor.rb +328 -0
- data/lib/familia/utils.rb +13 -0
- data/lib/familia/verifiable_identifier.rb +3 -1
- data/lib/familia/version.rb +3 -1
- data/lib/familia.rb +31 -4
- data/lib/middleware/database_command_counter.rb +152 -0
- data/lib/middleware/database_logger.rb +325 -129
- data/lib/multi_result.rb +2 -0
- data/try/edge_cases/empty_identifiers_try.rb +2 -0
- data/try/edge_cases/hash_symbolization_try.rb +2 -0
- data/try/edge_cases/json_serialization_try.rb +2 -0
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
- data/try/edge_cases/race_conditions_try.rb +4 -0
- data/try/edge_cases/reserved_keywords_try.rb +4 -0
- data/try/edge_cases/string_coercion_try.rb +6 -4
- data/try/edge_cases/ttl_side_effects_try.rb +4 -0
- data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
- data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
- data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
- data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
- data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
- data/try/features/encrypted_fields/memory_security_try.rb +4 -0
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
- data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
- data/try/features/encryption/config_persistence_try.rb +4 -0
- data/try/features/encryption/core_try.rb +4 -0
- data/try/features/encryption/instance_variable_scope_try.rb +4 -0
- data/try/features/encryption/module_loading_try.rb +4 -0
- data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
- data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
- data/try/features/encryption/roundtrip_validation_try.rb +4 -0
- data/try/features/encryption/secure_memory_handling_try.rb +4 -0
- data/try/features/expiration/expiration_try.rb +5 -1
- data/try/features/external_identifier/external_identifier_try.rb +171 -8
- data/try/features/feature_dependencies_try.rb +2 -0
- data/try/features/feature_improvements_try.rb +2 -0
- data/try/features/field_groups_try.rb +2 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
- data/try/features/object_identifier/object_identifier_try.rb +2 -0
- data/try/features/quantization/quantization_try.rb +4 -0
- data/try/features/real_feature_integration_try.rb +2 -0
- data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
- data/try/features/relationships/indexing_rebuild_try.rb +600 -0
- data/try/features/relationships/indexing_try.rb +30 -4
- data/try/features/relationships/participation_bidirectional_try.rb +242 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
- data/try/features/relationships/participation_commands_verification_try.rb +2 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
- data/try/features/relationships/participation_reverse_index_try.rb +15 -13
- data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
- data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
- data/try/features/relationships/relationships_api_changes_try.rb +6 -4
- data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
- data/try/features/relationships/relationships_performance_try.rb +4 -0
- data/try/features/relationships/relationships_performance_working_try.rb +4 -0
- data/try/features/relationships/relationships_try.rb +6 -4
- data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
- data/try/features/safe_dump/safe_dump_try.rb +4 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/refresh_reset_try.rb +3 -0
- data/try/features/transient_fields/simple_refresh_test.rb +3 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
- data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
- data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
- data/try/integration/connection/handler_constraints_try.rb +4 -0
- data/try/integration/connection/isolated_dbclient_try.rb +4 -0
- data/try/integration/connection/middleware_reconnect_try.rb +2 -0
- data/try/integration/connection/operation_mode_guards_try.rb +5 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
- data/try/integration/connection/pools_try.rb +4 -0
- data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
- data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
- data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
- data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
- data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
- data/try/integration/connection/transaction_modes_try.rb +4 -0
- data/try/integration/conventional_inheritance_try.rb +4 -0
- data/try/integration/create_method_try.rb +26 -22
- data/try/integration/cross_component_try.rb +4 -0
- data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
- data/try/integration/data_types/datatype_transactions_try.rb +251 -0
- data/try/integration/database_consistency_try.rb +4 -0
- data/try/integration/familia_extended_try.rb +4 -0
- data/try/integration/familia_members_methods_try.rb +4 -0
- data/try/integration/models/customer_safe_dump_try.rb +9 -1
- data/try/integration/models/customer_try.rb +4 -0
- data/try/integration/models/datatype_base_try.rb +4 -0
- data/try/integration/models/familia_object_try.rb +5 -1
- data/try/integration/persistence_operations_try.rb +166 -10
- data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
- data/try/integration/save_methods_consistency_try.rb +241 -0
- data/try/integration/scenarios_try.rb +4 -0
- data/try/integration/secure_identifier_try.rb +4 -0
- data/try/integration/transaction_safety_core_try.rb +176 -0
- data/try/integration/transaction_safety_workflow_try.rb +291 -0
- data/try/integration/verifiable_identifier_try.rb +4 -0
- data/try/investigation/pipeline_routing/README.md +228 -0
- data/try/performance/benchmarks_try.rb +4 -0
- data/try/performance/transaction_safety_benchmark_try.rb +238 -0
- data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
- data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
- data/try/support/debugging/cache_behavior_tracer.rb +4 -0
- data/try/support/debugging/debug_aad_process.rb +3 -0
- data/try/support/debugging/debug_concealed_internal.rb +3 -0
- data/try/support/debugging/debug_concealed_reveal.rb +3 -0
- data/try/support/debugging/debug_context_aad.rb +3 -0
- data/try/support/debugging/debug_context_simple.rb +3 -0
- data/try/support/debugging/debug_cross_context.rb +3 -0
- data/try/support/debugging/debug_database_load.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
- data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
- data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
- data/try/support/debugging/debug_field_decrypt.rb +3 -0
- data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
- data/try/support/debugging/debug_load_path.rb +3 -0
- data/try/support/debugging/debug_method_definition.rb +3 -0
- data/try/support/debugging/debug_method_resolution.rb +3 -0
- data/try/support/debugging/debug_minimal.rb +3 -0
- data/try/support/debugging/debug_provider.rb +3 -0
- data/try/support/debugging/debug_secure_behavior.rb +3 -0
- data/try/support/debugging/debug_string_class.rb +3 -0
- data/try/support/debugging/debug_test.rb +3 -0
- data/try/support/debugging/debug_test_design.rb +3 -0
- data/try/support/debugging/encryption_method_tracer.rb +4 -0
- data/try/support/debugging/provider_diagnostics.rb +4 -0
- data/try/support/helpers/test_cleanup.rb +4 -0
- data/try/support/helpers/test_helpers.rb +5 -0
- data/try/support/memory/memory_basic_test.rb +4 -0
- data/try/support/memory/memory_detailed_test.rb +4 -0
- data/try/support/memory/memory_search_for_string.rb +4 -0
- data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
- data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
- data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
- data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
- data/try/support/prototypes/atomic_saves_v4.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
- data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
- data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
- data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
- data/try/support/prototypes/pooling/pool_siege.rb +4 -2
- data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
- data/try/thread_safety/README.md +496 -0
- data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
- data/try/thread_safety/connection_chain_race_try.rb +148 -0
- data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
- data/try/thread_safety/feature_registry_race_try.rb +226 -0
- data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
- data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
- data/try/thread_safety/field_registration_race_try.rb +222 -0
- data/try/thread_safety/logger_initialization_race_try.rb +170 -0
- data/try/thread_safety/middleware_registration_race_try.rb +154 -0
- data/try/thread_safety/module_config_race_try.rb +175 -0
- data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
- data/try/unit/core/autoloader_try.rb +4 -0
- data/try/unit/core/base_enhancements_try.rb +4 -0
- data/try/unit/core/connection_try.rb +4 -0
- data/try/unit/core/errors_try.rb +4 -0
- data/try/unit/core/extensions_try.rb +4 -0
- data/try/unit/core/familia_logger_try.rb +2 -0
- data/try/unit/core/familia_try.rb +4 -0
- data/try/unit/core/middleware_sampling_try.rb +335 -0
- data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
- data/try/unit/core/middleware_thread_safety_try.rb +245 -0
- data/try/unit/core/middleware_try.rb +4 -0
- data/try/unit/core/settings_try.rb +4 -0
- data/try/unit/core/time_utils_try.rb +4 -0
- data/try/unit/core/tools_try.rb +4 -0
- data/try/unit/core/utils_try.rb +37 -0
- data/try/unit/data_types/boolean_try.rb +5 -1
- data/try/unit/data_types/counter_try.rb +4 -0
- data/try/unit/data_types/datatype_base_try.rb +4 -0
- data/try/unit/data_types/hash_try.rb +4 -0
- data/try/unit/data_types/list_try.rb +4 -0
- data/try/unit/data_types/lock_try.rb +4 -0
- data/try/unit/data_types/sorted_set_try.rb +4 -0
- data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
- data/try/unit/data_types/string_try.rb +5 -1
- data/try/unit/data_types/unsortedset_try.rb +4 -0
- data/try/unit/familia_resolve_class_try.rb +116 -0
- data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
- data/try/unit/horreum/base_try.rb +5 -1
- data/try/unit/horreum/class_methods_try.rb +6 -2
- data/try/unit/horreum/commands_try.rb +4 -0
- data/try/unit/horreum/defensive_initialization_try.rb +4 -0
- data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
- data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
- data/try/unit/horreum/field_categories_try.rb +4 -0
- data/try/unit/horreum/field_definition_try.rb +4 -0
- data/try/unit/horreum/initialization_try.rb +5 -1
- data/try/unit/horreum/json_type_preservation_try.rb +2 -0
- data/try/unit/horreum/optimized_loading_try.rb +156 -0
- data/try/unit/horreum/relations_try.rb +8 -4
- data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
- data/try/unit/horreum/serialization_try.rb +6 -2
- data/try/unit/horreum/settings_try.rb +4 -0
- data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
- data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
- data/try/unit/middleware/database_logger_methods_try.rb +251 -0
- data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
- data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
- data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
- data/try/unit/thread_safety_monitor_try.rb +149 -0
- metadata +81 -14
- data/.github/workflows/code-quality.yml +0 -138
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
- data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
- data/docs/archive/FAMILIA_UPDATE.md +0 -226
- data/docs/archive/README.md +0 -64
- data/docs/archive/api-reference.md +0 -333
- data/docs/guides/core-field-system.md +0 -806
- data/docs/guides/implementation.md +0 -276
- data/docs/guides/security-model.md +0 -183
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Familia
|
|
4
|
+
module Features
|
|
5
|
+
module Relationships
|
|
6
|
+
module Indexing
|
|
7
|
+
# RebuildStrategies provides atomic index rebuild operations with zero downtime.
|
|
8
|
+
#
|
|
9
|
+
# All rebuild strategies follow a consistent pattern:
|
|
10
|
+
# 1. Build index in temporary key
|
|
11
|
+
# 2. Batch processing with transactions per batch (not entire rebuild)
|
|
12
|
+
# 3. Atomic swap via Lua script at completion
|
|
13
|
+
# 4. Progress callbacks throughout
|
|
14
|
+
#
|
|
15
|
+
# This ensures:
|
|
16
|
+
# - Zero downtime during rebuild (live index remains available)
|
|
17
|
+
# - Memory efficiency (batch processing)
|
|
18
|
+
# - Consistent progress reporting
|
|
19
|
+
# - Safe failure handling (temp key abandoned on error)
|
|
20
|
+
#
|
|
21
|
+
# @example Via instances collection
|
|
22
|
+
# RebuildStrategies.rebuild_via_instances(
|
|
23
|
+
# User,
|
|
24
|
+
# :email,
|
|
25
|
+
# :add_to_email_index,
|
|
26
|
+
# batch_size: 100
|
|
27
|
+
# ) { |progress| puts "Processed: #{progress[:completed]}/#{progress[:total]}" }
|
|
28
|
+
#
|
|
29
|
+
# @example Via participation relationship
|
|
30
|
+
# RebuildStrategies.rebuild_via_participation(
|
|
31
|
+
# company,
|
|
32
|
+
# Employee,
|
|
33
|
+
# :department,
|
|
34
|
+
# :add_to_company_dept_index,
|
|
35
|
+
# company.employees_collection,
|
|
36
|
+
# batch_size: 100
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# @example Via SCAN (fallback for complex scenarios)
|
|
40
|
+
# RebuildStrategies.rebuild_via_scan(
|
|
41
|
+
# User,
|
|
42
|
+
# :email,
|
|
43
|
+
# :add_to_email_index,
|
|
44
|
+
# batch_size: 100
|
|
45
|
+
# )
|
|
46
|
+
#
|
|
47
|
+
module RebuildStrategies
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
# Rebuilds index by loading objects from ModelClass.instances sorted set.
|
|
51
|
+
#
|
|
52
|
+
# This is the preferred strategy for models with class-level indexes that
|
|
53
|
+
# maintain an instances collection. It's efficient because:
|
|
54
|
+
# - Direct access to all object identifiers via ZRANGE
|
|
55
|
+
# - Bulk loading via load_multi
|
|
56
|
+
# - No key pattern matching required
|
|
57
|
+
#
|
|
58
|
+
# Process:
|
|
59
|
+
# 1. Enumerate identifiers from ModelClass.instances.members
|
|
60
|
+
# 2. Load objects in batches via load_multi(identifiers).compact
|
|
61
|
+
# 3. Build temp index via transactions (one per batch)
|
|
62
|
+
# 4. Atomic swap temp -> final key via Lua
|
|
63
|
+
#
|
|
64
|
+
# @param indexed_class [Class] The model class being indexed (e.g., User)
|
|
65
|
+
# @param field [Symbol] The field to index (e.g., :email)
|
|
66
|
+
# @param add_method [Symbol] The mutation method to call (e.g., :add_to_email_index)
|
|
67
|
+
# @param batch_size [Integer] Number of objects per batch (default: 100)
|
|
68
|
+
# @yield [Hash] Progress info: {completed:, total:, rate:, elapsed:}
|
|
69
|
+
# @return [Integer] Number of objects processed
|
|
70
|
+
#
|
|
71
|
+
# @example Rebuild user email index
|
|
72
|
+
# count = RebuildStrategies.rebuild_via_instances(
|
|
73
|
+
# User,
|
|
74
|
+
# :email,
|
|
75
|
+
# :add_to_email_index,
|
|
76
|
+
# batch_size: 100
|
|
77
|
+
# ) { |p| puts "#{p[:completed]}/#{p[:total]} (#{p[:rate]}/s)" }
|
|
78
|
+
#
|
|
79
|
+
def rebuild_via_instances(indexed_class, field, add_method, batch_size: 100, &progress)
|
|
80
|
+
unless indexed_class.respond_to?(:instances)
|
|
81
|
+
raise ArgumentError, "#{indexed_class.name} does not have an instances collection"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
instances = indexed_class.instances
|
|
85
|
+
total = instances.size
|
|
86
|
+
start_time = Familia.now
|
|
87
|
+
|
|
88
|
+
Familia.info "[Rebuild] Starting via_instances for #{indexed_class.name}.#{field} (#{total} objects)"
|
|
89
|
+
|
|
90
|
+
# Determine the final index key by examining the class-level index
|
|
91
|
+
# Extract index name from add_method (e.g., add_to_email_index -> email_index)
|
|
92
|
+
# or add_to_class_email_index -> email_index
|
|
93
|
+
index_name = add_method.to_s.gsub(/^(add_to|update_in|remove_from)_(class_)?/, '')
|
|
94
|
+
|
|
95
|
+
# Access the class-level index directly
|
|
96
|
+
unless indexed_class.respond_to?(index_name)
|
|
97
|
+
raise ArgumentError, "#{indexed_class.name} does not have index accessor: #{index_name}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
index_hashkey = indexed_class.send(index_name)
|
|
101
|
+
final_key = index_hashkey.dbkey
|
|
102
|
+
temp_key = RebuildStrategies.build_temp_key(final_key)
|
|
103
|
+
|
|
104
|
+
processed = 0
|
|
105
|
+
indexed_count = 0
|
|
106
|
+
|
|
107
|
+
# Process in batches - use membersraw to get raw identifiers without deserialization
|
|
108
|
+
instances.membersraw.each_slice(batch_size) do |identifiers|
|
|
109
|
+
# Bulk load objects, filtering out nils (deleted/missing objects)
|
|
110
|
+
objects = indexed_class.load_multi(identifiers).compact
|
|
111
|
+
|
|
112
|
+
# Transaction per batch (NOT entire rebuild)
|
|
113
|
+
batch_indexed = 0
|
|
114
|
+
indexed_class.transaction do |tx|
|
|
115
|
+
objects.each do |obj|
|
|
116
|
+
value = obj.send(field)
|
|
117
|
+
# Skip nil/empty field values gracefully
|
|
118
|
+
next unless value && !value.to_s.strip.empty?
|
|
119
|
+
|
|
120
|
+
# For class-level indexes, use HSET directly into temp key
|
|
121
|
+
tx.hset(temp_key, value.to_s, obj.identifier.to_s)
|
|
122
|
+
batch_indexed += 1
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
processed += identifiers.size
|
|
127
|
+
indexed_count += batch_indexed
|
|
128
|
+
elapsed = Familia.now - start_time
|
|
129
|
+
rate = processed / elapsed
|
|
130
|
+
|
|
131
|
+
progress&.call(
|
|
132
|
+
completed: processed,
|
|
133
|
+
total: total,
|
|
134
|
+
rate: rate.round(2),
|
|
135
|
+
elapsed: elapsed.round(2)
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Atomic swap: temp -> final (ZERO DOWNTIME)
|
|
140
|
+
RebuildStrategies.atomic_swap(temp_key, final_key, indexed_class.dbclient)
|
|
141
|
+
|
|
142
|
+
elapsed = Familia.now - start_time
|
|
143
|
+
Familia.info "[Rebuild] Completed via_instances: #{indexed_count} indexed (#{processed} total) in #{elapsed.round(2)}s"
|
|
144
|
+
|
|
145
|
+
indexed_count
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Rebuilds index by loading objects from a participation collection.
|
|
149
|
+
#
|
|
150
|
+
# This strategy is for instance-scoped indexes where objects participate
|
|
151
|
+
# in a parent's collection (e.g., employees in company.employees_collection).
|
|
152
|
+
#
|
|
153
|
+
# Process:
|
|
154
|
+
# 1. Enumerate members from collection (SortedSet, UnsortedSet, or ListKey)
|
|
155
|
+
# 2. Load objects in batches via load_multi(identifiers).compact
|
|
156
|
+
# 3. Build temp index via transactions (one per batch)
|
|
157
|
+
# 4. Atomic swap temp -> final key via Lua
|
|
158
|
+
#
|
|
159
|
+
# @param scope_instance [Object] The parent instance providing scope (e.g., company)
|
|
160
|
+
# @param indexed_class [Class] The model class being indexed (e.g., Employee)
|
|
161
|
+
# @param field [Symbol] The field to index (e.g., :badge_number)
|
|
162
|
+
# @param add_method [Symbol] The mutation method (e.g., :add_to_company_badge_index)
|
|
163
|
+
# @param collection [DataType] The collection containing members (SortedSet/UnsortedSet/ListKey)
|
|
164
|
+
# @param cardinality [Symbol] The index cardinality (:unique or :multi) - must be :unique
|
|
165
|
+
# @param batch_size [Integer] Number of objects per batch (default: 100)
|
|
166
|
+
# @yield [Hash] Progress info: {completed:, total:, rate:, elapsed:}
|
|
167
|
+
# @return [Integer] Number of objects processed
|
|
168
|
+
#
|
|
169
|
+
# @example Rebuild company badge index
|
|
170
|
+
# count = RebuildStrategies.rebuild_via_participation(
|
|
171
|
+
# company,
|
|
172
|
+
# Employee,
|
|
173
|
+
# :badge_number,
|
|
174
|
+
# :add_to_company_badge_index,
|
|
175
|
+
# company.employees_collection,
|
|
176
|
+
# :unique,
|
|
177
|
+
# batch_size: 100
|
|
178
|
+
# )
|
|
179
|
+
#
|
|
180
|
+
def rebuild_via_participation(scope_instance, indexed_class, field, add_method, collection, cardinality, batch_size: 100, &progress)
|
|
181
|
+
total = collection.size
|
|
182
|
+
start_time = Familia.now
|
|
183
|
+
|
|
184
|
+
scope_class = scope_instance.class.name
|
|
185
|
+
Familia.info "[Rebuild] Starting via_participation for #{scope_class}##{indexed_class.name}.#{field} (#{total} objects)"
|
|
186
|
+
|
|
187
|
+
# Guard: This method only supports unique indexes
|
|
188
|
+
if cardinality != :unique
|
|
189
|
+
raise ArgumentError, <<~ERROR.strip
|
|
190
|
+
rebuild_via_participation only supports unique indexes (cardinality: :unique)
|
|
191
|
+
Received cardinality: #{cardinality.inspect} for field: #{field}
|
|
192
|
+
|
|
193
|
+
Multi-indexes require field-value-specific keys and use specialized 4-phase rebuild logic.
|
|
194
|
+
Use the dedicated rebuild method generated on the scope instance instead.
|
|
195
|
+
ERROR
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Build temp key for the unique index.
|
|
199
|
+
#
|
|
200
|
+
# Extract index name from add_method. The add_method follows the pattern:
|
|
201
|
+
# add_to_{scope_class_config}_{index_name}
|
|
202
|
+
#
|
|
203
|
+
# For example:
|
|
204
|
+
# add_to_test_company_badge_index -> badge_index
|
|
205
|
+
# add_to_company_badge_index -> badge_index
|
|
206
|
+
#
|
|
207
|
+
# We need to remove the "add_to_{scope_class_config}_" prefix.
|
|
208
|
+
scope_class_config = scope_instance.class.config_name
|
|
209
|
+
prefix = "add_to_#{scope_class_config}_"
|
|
210
|
+
index_name = add_method.to_s.gsub(/^#{Regexp.escape(prefix)}/, '')
|
|
211
|
+
|
|
212
|
+
# Get the actual index accessor from the scope instance to derive the correct key.
|
|
213
|
+
# This ensures we use the same dbkey as the actual index DataType.
|
|
214
|
+
unless scope_instance.respond_to?(index_name)
|
|
215
|
+
raise ArgumentError, "#{scope_instance.class} does not have index accessor: #{index_name}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
index_datatype = scope_instance.send(index_name)
|
|
219
|
+
final_key = index_datatype.dbkey
|
|
220
|
+
temp_key = RebuildStrategies.build_temp_key(final_key)
|
|
221
|
+
|
|
222
|
+
processed = 0
|
|
223
|
+
indexed_count = 0
|
|
224
|
+
|
|
225
|
+
# Process in batches - use membersraw to get raw identifiers
|
|
226
|
+
collection.membersraw.each_slice(batch_size) do |identifiers|
|
|
227
|
+
objects = indexed_class.load_multi(identifiers).compact
|
|
228
|
+
|
|
229
|
+
# Transaction per batch
|
|
230
|
+
batch_indexed = 0
|
|
231
|
+
scope_instance.transaction do |tx|
|
|
232
|
+
objects.each do |obj|
|
|
233
|
+
value = obj.send(field)
|
|
234
|
+
next unless value && !value.to_s.strip.empty?
|
|
235
|
+
|
|
236
|
+
# For unique index: HSET temp_key field_value identifier
|
|
237
|
+
# For multi-index: SADD temp_key:field_value identifier
|
|
238
|
+
tx.hset(temp_key, value.to_s, obj.identifier.to_s)
|
|
239
|
+
batch_indexed += 1
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
processed += identifiers.size
|
|
244
|
+
indexed_count += batch_indexed
|
|
245
|
+
elapsed = Familia.now - start_time
|
|
246
|
+
rate = processed / elapsed
|
|
247
|
+
|
|
248
|
+
progress&.call(
|
|
249
|
+
completed: processed,
|
|
250
|
+
total: total,
|
|
251
|
+
rate: rate.round(2),
|
|
252
|
+
elapsed: elapsed.round(2)
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Atomic swap
|
|
257
|
+
RebuildStrategies.atomic_swap(temp_key, final_key, scope_instance.dbclient)
|
|
258
|
+
|
|
259
|
+
elapsed = Familia.now - start_time
|
|
260
|
+
Familia.info "[Rebuild] Completed via_participation: #{indexed_count} indexed (#{processed} total) in #{elapsed.round(2)}s"
|
|
261
|
+
|
|
262
|
+
indexed_count
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Rebuilds index by scanning all keys matching a pattern.
|
|
266
|
+
#
|
|
267
|
+
# This is the fallback strategy when:
|
|
268
|
+
# - No instances collection available
|
|
269
|
+
# - No participation relationship
|
|
270
|
+
# - Need to rebuild from raw keys
|
|
271
|
+
#
|
|
272
|
+
# Uses SCAN (not KEYS) for memory-efficient iteration. Filters by scope
|
|
273
|
+
# if scope_instance provided.
|
|
274
|
+
#
|
|
275
|
+
# Process:
|
|
276
|
+
# 1. Use redis.scan_each(match: pattern, count: batch_size)
|
|
277
|
+
# 2. Filter by scope_instance if provided
|
|
278
|
+
# 3. Load objects in batches via load_multi_by_keys
|
|
279
|
+
# 4. Build temp index via transactions (one per batch)
|
|
280
|
+
# 5. Atomic swap temp -> final key via Lua
|
|
281
|
+
#
|
|
282
|
+
# @param indexed_class [Class] The model class being indexed
|
|
283
|
+
# @param field [Symbol] The field to index
|
|
284
|
+
# @param add_method [Symbol] The mutation method
|
|
285
|
+
# @param scope_instance [Object, nil] Optional scope for filtering
|
|
286
|
+
# @param batch_size [Integer] Number of keys per SCAN iteration (default: 100)
|
|
287
|
+
# @yield [Hash] Progress info: {completed:, scanned:, rate:, elapsed:}
|
|
288
|
+
# @return [Integer] Number of objects processed
|
|
289
|
+
#
|
|
290
|
+
# @example Rebuild without instances collection
|
|
291
|
+
# count = RebuildStrategies.rebuild_via_scan(
|
|
292
|
+
# User,
|
|
293
|
+
# :email,
|
|
294
|
+
# :add_to_email_index,
|
|
295
|
+
# batch_size: 100
|
|
296
|
+
# )
|
|
297
|
+
#
|
|
298
|
+
def rebuild_via_scan(indexed_class, field, add_method, scope_instance: nil, batch_size: 100, &progress)
|
|
299
|
+
start_time = Familia.now
|
|
300
|
+
|
|
301
|
+
# Build key pattern for SCAN
|
|
302
|
+
# For instance-scoped indexes, we still scan all objects of indexed_class
|
|
303
|
+
# (not scoped under parent), then filter by scope during processing
|
|
304
|
+
pattern = "#{indexed_class.config_name}:*:object"
|
|
305
|
+
|
|
306
|
+
Familia.info "[Rebuild] Starting via_scan for #{indexed_class.name}.#{field} (pattern: #{pattern})"
|
|
307
|
+
Familia.warn "[Rebuild] Using SCAN fallback - consider adding instances collection for better performance"
|
|
308
|
+
|
|
309
|
+
# Determine final key by examining the index
|
|
310
|
+
# Extract index name from add_method (e.g., add_to_class_email_index -> email_index)
|
|
311
|
+
# For instance-scoped: add_to_rebuild_test_company_badge_index -> badge_index
|
|
312
|
+
index_name = add_method.to_s.gsub(/^(add_to|update_in|remove_from)_(class_)?/, '')
|
|
313
|
+
|
|
314
|
+
# Strip scope class config prefix if present (e.g., rebuild_test_company_badge_index -> badge_index)
|
|
315
|
+
# For instance-scoped indexes, the index lives on scope_instance, not indexed_class
|
|
316
|
+
if scope_instance
|
|
317
|
+
scope_config = scope_instance.class.config_name
|
|
318
|
+
index_name = index_name.gsub(/^#{scope_config}_/, '')
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# For instance-scoped indexes, check scope_instance for accessor
|
|
322
|
+
# For class-level indexes, check indexed_class
|
|
323
|
+
index_owner = scope_instance || indexed_class
|
|
324
|
+
unless index_owner.respond_to?(index_name)
|
|
325
|
+
raise ArgumentError, "#{index_owner.class.name} does not have index accessor: #{index_name}"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
index_hashkey = index_owner.send(index_name)
|
|
329
|
+
final_key = index_hashkey.dbkey
|
|
330
|
+
temp_key = RebuildStrategies.build_temp_key(final_key)
|
|
331
|
+
|
|
332
|
+
processed = 0
|
|
333
|
+
indexed_count = 0
|
|
334
|
+
scanned = 0
|
|
335
|
+
redis = indexed_class.dbclient
|
|
336
|
+
|
|
337
|
+
# Use SCAN (not KEYS) for memory efficiency
|
|
338
|
+
batch = []
|
|
339
|
+
redis.scan_each(match: pattern, count: batch_size) do |key|
|
|
340
|
+
batch << key
|
|
341
|
+
scanned += 1
|
|
342
|
+
|
|
343
|
+
# Process in batches
|
|
344
|
+
if batch.size >= batch_size
|
|
345
|
+
batch_indexed = RebuildStrategies.process_scan_batch(batch, indexed_class, field, temp_key, scope_instance)
|
|
346
|
+
processed += batch.size
|
|
347
|
+
indexed_count += batch_indexed
|
|
348
|
+
|
|
349
|
+
elapsed = Familia.now - start_time
|
|
350
|
+
rate = processed / elapsed
|
|
351
|
+
|
|
352
|
+
progress&.call(
|
|
353
|
+
completed: processed,
|
|
354
|
+
scanned: scanned,
|
|
355
|
+
rate: rate.round(2),
|
|
356
|
+
elapsed: elapsed.round(2)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
batch.clear
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Process remaining batch
|
|
364
|
+
unless batch.empty?
|
|
365
|
+
batch_indexed = RebuildStrategies.process_scan_batch(batch, indexed_class, field, temp_key, scope_instance)
|
|
366
|
+
processed += batch.size
|
|
367
|
+
indexed_count += batch_indexed
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Atomic swap
|
|
371
|
+
RebuildStrategies.atomic_swap(temp_key, final_key, redis)
|
|
372
|
+
|
|
373
|
+
elapsed = Familia.now - start_time
|
|
374
|
+
Familia.info "[Rebuild] Completed via_scan: #{indexed_count} indexed (#{processed} total) in #{elapsed.round(2)}s (scanned: #{scanned})"
|
|
375
|
+
|
|
376
|
+
indexed_count
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Processes a batch of keys from SCAN (module_function helper)
|
|
380
|
+
#
|
|
381
|
+
# @param keys [Array<String>] Array of Redis keys
|
|
382
|
+
# @param indexed_class [Class] The model class
|
|
383
|
+
# @param field [Symbol] The field to index
|
|
384
|
+
# @param temp_key [String] The temporary index key
|
|
385
|
+
# @param scope_instance [Object, nil] Optional scope instance. If provided, only objects belonging to this scope will be indexed.
|
|
386
|
+
# @return [Integer] Number of objects indexed in this batch
|
|
387
|
+
#
|
|
388
|
+
def process_scan_batch(keys, indexed_class, field, temp_key, scope_instance)
|
|
389
|
+
# Load objects by keys
|
|
390
|
+
objects = indexed_class.load_multi_by_keys(keys).compact
|
|
391
|
+
|
|
392
|
+
# For instance-scoped indexes, filter objects by scope
|
|
393
|
+
if scope_instance
|
|
394
|
+
# Get the participation collection for this scope
|
|
395
|
+
participation = indexed_class.participation_relationships.find do |rel|
|
|
396
|
+
rel.target_class == scope_instance.class
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
if participation
|
|
400
|
+
collection_name = participation.collection_name
|
|
401
|
+
scope_collection = scope_instance.send(collection_name)
|
|
402
|
+
# Filter to only objects that belong to this scope
|
|
403
|
+
objects = objects.select { |obj| scope_collection.member?(obj.identifier) }
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Transaction per batch
|
|
408
|
+
batch_indexed = 0
|
|
409
|
+
indexed_class.transaction do |tx|
|
|
410
|
+
objects.each do |obj|
|
|
411
|
+
value = obj.send(field)
|
|
412
|
+
next unless value && !value.to_s.strip.empty?
|
|
413
|
+
|
|
414
|
+
tx.hset(temp_key, value.to_s, obj.identifier.to_s)
|
|
415
|
+
batch_indexed += 1
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
batch_indexed
|
|
419
|
+
rescue StandardError => e
|
|
420
|
+
Familia.warn "[Rebuild] Error processing batch: #{e.message}"
|
|
421
|
+
0
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Builds a temporary key name for atomic swaps
|
|
425
|
+
#
|
|
426
|
+
# @param base_key [String] The final index key
|
|
427
|
+
# @return [String] Temporary key with timestamp suffix
|
|
428
|
+
#
|
|
429
|
+
def build_temp_key(base_key)
|
|
430
|
+
timestamp = Familia.now.to_i
|
|
431
|
+
"#{base_key}:rebuild:#{timestamp}"
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Performs atomic swap of temp key to final key.
|
|
435
|
+
#
|
|
436
|
+
# This ensures zero downtime during rebuild:
|
|
437
|
+
# 1. DEL final_key (remove old index)
|
|
438
|
+
# 2. RENAME temp_key final_key (atomically replace)
|
|
439
|
+
#
|
|
440
|
+
# RENAME is atomic, so the old index remains queryable until replaced:
|
|
441
|
+
# - Partial updates
|
|
442
|
+
# - Race conditions
|
|
443
|
+
# - Stale data visibility
|
|
444
|
+
#
|
|
445
|
+
# @param temp_key [String] The temporary key containing rebuilt index
|
|
446
|
+
# @param final_key [String] The live index key
|
|
447
|
+
# @param redis [Redis] The Redis connection
|
|
448
|
+
#
|
|
449
|
+
def atomic_swap(temp_key, final_key, redis)
|
|
450
|
+
# Check if temp key exists first - RENAME fails on non-existent keys
|
|
451
|
+
unless redis.exists(temp_key) > 0
|
|
452
|
+
Familia.info "[Rebuild] No temp key to swap (empty result set)"
|
|
453
|
+
# Just ensure final key is cleared
|
|
454
|
+
redis.del(final_key)
|
|
455
|
+
return
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Atomic swap: DEL final key, then RENAME temp -> final
|
|
459
|
+
# RENAME is already atomic, so we just need to clear the final key first
|
|
460
|
+
redis.del(final_key)
|
|
461
|
+
redis.rename(temp_key, final_key)
|
|
462
|
+
Familia.info "[Rebuild] Atomic swap completed: #{temp_key} -> #{final_key}"
|
|
463
|
+
rescue Redis::CommandError => e
|
|
464
|
+
# If temp key doesn't exist, just log and return (already handled above)
|
|
465
|
+
if e.message.include?("no such key")
|
|
466
|
+
Familia.info "[Rebuild] Temp key vanished during swap (concurrent operation?)"
|
|
467
|
+
return
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# For other errors, preserve temp key for debugging
|
|
471
|
+
Familia.warn "[Rebuild] Atomic swap failed: #{e.message}"
|
|
472
|
+
Familia.warn "[Rebuild] Temp key preserved for debugging: #{temp_key}"
|
|
473
|
+
raise
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|