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,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
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
# lib/familia/features/relationships/indexing/unique_index_generators.rb
|
|
2
|
+
#
|
|
1
3
|
# frozen_string_literal: true
|
|
2
4
|
|
|
5
|
+
require_relative 'rebuild_strategies'
|
|
6
|
+
|
|
3
7
|
module Familia
|
|
4
8
|
module Features
|
|
5
9
|
module Relationships
|
|
@@ -104,6 +108,9 @@ module Familia
|
|
|
104
108
|
# Ensure the index field is declared (creates accessor that returns DataType)
|
|
105
109
|
actual_scope_class.send(:ensure_index_field, actual_scope_class, index_name, :hashkey)
|
|
106
110
|
|
|
111
|
+
# Get scope_class_config for method naming (needed for rebuild methods)
|
|
112
|
+
scope_class_config = actual_scope_class.config_name
|
|
113
|
+
|
|
107
114
|
# Generate instance query method (e.g., company.find_by_badge_number)
|
|
108
115
|
actual_scope_class.class_eval do
|
|
109
116
|
define_method(:"find_by_#{field}") do |provided_value|
|
|
@@ -145,16 +152,66 @@ module Familia
|
|
|
145
152
|
# No need to manually define it here
|
|
146
153
|
|
|
147
154
|
# Generate method to rebuild the unique index for this parent instance
|
|
148
|
-
define_method(:"rebuild_#{index_name}") do
|
|
149
|
-
#
|
|
150
|
-
|
|
155
|
+
define_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
|
|
156
|
+
# Find the collection containing the indexed class.
|
|
157
|
+
#
|
|
158
|
+
# Strategy 1: Check if indexed_class has a participation relationship
|
|
159
|
+
# pointing back to this scope class. Participation relationships are
|
|
160
|
+
# stored on the PARTICIPANT class (indexed_class), not the target.
|
|
161
|
+
#
|
|
162
|
+
# Example: When RebuildTestEmployee.participates_in(RebuildTestCompany, :employees),
|
|
163
|
+
# the relationship is stored on RebuildTestEmployee, and we need to find it
|
|
164
|
+
# by matching target_class (RebuildTestCompany) with self.class.
|
|
165
|
+
collection = nil
|
|
166
|
+
if indexed_class.respond_to?(:participation_relationships)
|
|
167
|
+
participation = indexed_class.participation_relationships.find do |rel|
|
|
168
|
+
rel.target_class == self.class
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if participation
|
|
172
|
+
collection = send(participation.collection_name)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
151
175
|
|
|
152
|
-
#
|
|
153
|
-
|
|
176
|
+
# Strategy 2: Fallback to checking related_fields for explicit class: option
|
|
177
|
+
unless collection
|
|
178
|
+
if self.class.respond_to?(:related_fields)
|
|
179
|
+
self.class.related_fields&.each do |name, field_def|
|
|
180
|
+
# Check if this DataType's class option matches the indexed class
|
|
181
|
+
if field_def.opts[:class] == indexed_class
|
|
182
|
+
collection = send(name)
|
|
183
|
+
break
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
if collection
|
|
190
|
+
# Find the IndexingRelationship to get cardinality metadata
|
|
191
|
+
index_config = indexed_class.indexing_relationships.find { |rel| rel.index_name == index_name }
|
|
192
|
+
|
|
193
|
+
# Strategy 2: Use participation-based rebuild
|
|
194
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_participation(
|
|
195
|
+
self, # scope_instance (e.g., company)
|
|
196
|
+
indexed_class, # e.g., Employee
|
|
197
|
+
field, # e.g., :badge_number
|
|
198
|
+
:"add_to_#{scope_class_config}_#{index_name}", # e.g., :add_to_company_badge_index
|
|
199
|
+
collection,
|
|
200
|
+
index_config.cardinality, # :unique or :multi
|
|
201
|
+
batch_size: batch_size,
|
|
202
|
+
&progress_block
|
|
203
|
+
)
|
|
204
|
+
else
|
|
205
|
+
# Strategy 3: Fall back to SCAN with filtering
|
|
206
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
|
|
207
|
+
indexed_class,
|
|
208
|
+
field,
|
|
209
|
+
:"add_to_#{scope_class_config}_#{index_name}",
|
|
210
|
+
scope_instance: self,
|
|
211
|
+
batch_size: batch_size,
|
|
212
|
+
&progress_block
|
|
213
|
+
)
|
|
214
|
+
end
|
|
158
215
|
end
|
|
159
216
|
end
|
|
160
217
|
end
|
|
@@ -175,7 +232,7 @@ module Familia
|
|
|
175
232
|
scope_class_config = scope_class.config_name
|
|
176
233
|
indexed_class.class_eval do
|
|
177
234
|
method_name = :"add_to_#{scope_class_config}_#{index_name}"
|
|
178
|
-
Familia.
|
|
235
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
179
236
|
|
|
180
237
|
define_method(method_name) do |scope_instance|
|
|
181
238
|
return unless scope_instance
|
|
@@ -209,7 +266,7 @@ module Familia
|
|
|
209
266
|
# employee.guard_unique_company_badge_index!(company)
|
|
210
267
|
#
|
|
211
268
|
method_name = :"guard_unique_#{scope_class_config}_#{index_name}!"
|
|
212
|
-
Familia.
|
|
269
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
213
270
|
|
|
214
271
|
define_method(method_name) do |scope_instance|
|
|
215
272
|
return unless scope_instance
|
|
@@ -228,7 +285,7 @@ module Familia
|
|
|
228
285
|
end
|
|
229
286
|
|
|
230
287
|
method_name = :"remove_from_#{scope_class_config}_#{index_name}"
|
|
231
|
-
Familia.
|
|
288
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
232
289
|
|
|
233
290
|
define_method(method_name) do |scope_instance|
|
|
234
291
|
return unless scope_instance
|
|
@@ -244,7 +301,7 @@ module Familia
|
|
|
244
301
|
end
|
|
245
302
|
|
|
246
303
|
method_name = :"update_in_#{scope_class_config}_#{index_name}"
|
|
247
|
-
Familia.
|
|
304
|
+
Familia.debug("[UniqueIndexGenerators] #{name} method #{method_name}")
|
|
248
305
|
|
|
249
306
|
define_method(method_name) do |scope_instance, old_field_value = nil|
|
|
250
307
|
return unless scope_instance
|
|
@@ -313,15 +370,26 @@ module Familia
|
|
|
313
370
|
# No need to manually create it - Horreum handles this automatically
|
|
314
371
|
|
|
315
372
|
# Generate method to rebuild the class-level index
|
|
316
|
-
indexed_class.define_singleton_method(:"rebuild_#{index_name}") do
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
373
|
+
indexed_class.define_singleton_method(:"rebuild_#{index_name}") do |batch_size: 100, &progress_block|
|
|
374
|
+
if respond_to?(:instances)
|
|
375
|
+
# Strategy 1: Use instances collection (fastest)
|
|
376
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_instances(
|
|
377
|
+
self, # indexed_class (e.g., User)
|
|
378
|
+
field, # e.g., :email
|
|
379
|
+
:"add_to_class_#{index_name}", # e.g., :add_to_class_email_lookup
|
|
380
|
+
batch_size: batch_size,
|
|
381
|
+
&progress_block
|
|
382
|
+
)
|
|
383
|
+
else
|
|
384
|
+
# Strategy 3: Fall back to SCAN
|
|
385
|
+
Familia::Features::Relationships::Indexing::RebuildStrategies.rebuild_via_scan(
|
|
386
|
+
self,
|
|
387
|
+
field,
|
|
388
|
+
:"add_to_class_#{index_name}",
|
|
389
|
+
batch_size: batch_size,
|
|
390
|
+
&progress_block
|
|
391
|
+
)
|
|
392
|
+
end
|
|
325
393
|
end
|
|
326
394
|
end
|
|
327
395
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# lib/familia/features/relationships/indexing.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
require_relative 'indexing_relationship'
|
|
4
6
|
require_relative 'indexing/multi_index_generators'
|
|
5
7
|
require_relative 'indexing/unique_index_generators'
|
|
8
|
+
require_relative 'indexing/rebuild_strategies'
|
|
6
9
|
|
|
7
10
|
module Familia
|
|
8
11
|
module Features
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# lib/familia/features/relationships/indexing_relationship.rb
|
|
2
|
+
#
|
|
1
3
|
# frozen_string_literal: true
|
|
2
4
|
|
|
3
5
|
module Familia
|
|
@@ -29,7 +31,7 @@ module Familia
|
|
|
29
31
|
:scope_class, # Class/Symbol - scope class for instance-scoped indexes (within:)
|
|
30
32
|
:within, # Class/Symbol/nil - within: parameter (nil for class-level, Class for instance-scoped)
|
|
31
33
|
:cardinality, # Symbol - :unique (1:1) or :multi (1:many)
|
|
32
|
-
:query
|
|
34
|
+
:query, # Boolean - whether to generate query methods
|
|
33
35
|
) do
|
|
34
36
|
#
|
|
35
37
|
# Get the normalized config name for the scope class
|