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,674 @@
|
|
|
1
|
+
# Optimized Loading Guide
|
|
2
|
+
|
|
3
|
+
> **💡 Quick Reference**
|
|
4
|
+
>
|
|
5
|
+
> Reduce Redis commands by 50-96% for bulk object loading:
|
|
6
|
+
> ```ruby
|
|
7
|
+
> # Single object (50% reduction)
|
|
8
|
+
> user = User.find_by_id(123, check_exists: false)
|
|
9
|
+
>
|
|
10
|
+
> # Bulk loading (96% reduction)
|
|
11
|
+
> users = User.load_multi([123, 456, 789])
|
|
12
|
+
> ```
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Familia's optimized loading provides two complementary strategies to dramatically reduce Redis command overhead when loading objects. These optimizations are particularly valuable for applications loading collections of related objects, processing query results, or operating in high-throughput environments.
|
|
17
|
+
|
|
18
|
+
**Default behavior**: Each object load requires 2 Redis commands (EXISTS + HGETALL)
|
|
19
|
+
|
|
20
|
+
**Optimized approaches**:
|
|
21
|
+
1. **Skip EXISTS check** (`check_exists: false`) - 50% reduction, 1 command per object
|
|
22
|
+
2. **Pipelined bulk loading** (`load_multi`) - Up to 96% reduction, 1 round trip for N objects
|
|
23
|
+
|
|
24
|
+
## Why Optimize Object Loading?
|
|
25
|
+
|
|
26
|
+
**Network Overhead**: Each Redis command incurs network round-trip latency. For 14 objects, default loading requires 28 round trips.
|
|
27
|
+
|
|
28
|
+
**Bulk Operations**: Loading collections of related objects (e.g., metadata for a customer, domains for a team) compounds the overhead.
|
|
29
|
+
|
|
30
|
+
**High Throughput**: APIs serving thousands of requests per second benefit significantly from reduced Redis commands.
|
|
31
|
+
|
|
32
|
+
**Cost Efficiency**: Fewer commands mean lower Redis server load and reduced infrastructure costs in cloud environments.
|
|
33
|
+
|
|
34
|
+
## The Problem
|
|
35
|
+
|
|
36
|
+
Consider loading metadata objects for a customer:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Get metadata IDs from sorted set
|
|
40
|
+
metadata_ids = customer.metadata.rangebyscore(start_time, end_time)
|
|
41
|
+
# => ["id1", "id2", "id3", ..., "id14"] # 14 metadata objects
|
|
42
|
+
|
|
43
|
+
# Traditional approach
|
|
44
|
+
metadata = metadata_ids.map { |id| Metadata.find_by_id(id) }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Redis commands generated**:
|
|
48
|
+
```
|
|
49
|
+
exists metadata:id1:object # Check 1
|
|
50
|
+
hgetall metadata:id1:object # Load 1
|
|
51
|
+
exists metadata:id2:object # Check 2
|
|
52
|
+
hgetall metadata:id2:object # Load 2
|
|
53
|
+
... (repeated 14 times)
|
|
54
|
+
# Total: 28 commands across 28 network round trips
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Optimization 1: Skip EXISTS Check
|
|
60
|
+
|
|
61
|
+
For single object loads or when iterating over collections:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# Default behavior (2 commands)
|
|
65
|
+
user = User.find_by_id(123)
|
|
66
|
+
|
|
67
|
+
# Optimized (1 command)
|
|
68
|
+
user = User.find_by_id(123, check_exists: false)
|
|
69
|
+
|
|
70
|
+
# Still returns nil for non-existent objects
|
|
71
|
+
missing = User.find_by_id(999, check_exists: false) # => nil
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**When to use**:
|
|
75
|
+
- Loading objects from known-to-exist references (sorted set members, etc.)
|
|
76
|
+
- Performance-critical paths where 50% reduction matters
|
|
77
|
+
- Iterating over collections with `.map`
|
|
78
|
+
|
|
79
|
+
**Performance**: 14 objects → 14 commands instead of 28 (50% reduction)
|
|
80
|
+
|
|
81
|
+
### Optimization 2: Pipelined Bulk Loading
|
|
82
|
+
|
|
83
|
+
For loading multiple objects at once:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# Optimized bulk loading (1 round trip)
|
|
87
|
+
users = User.load_multi([123, 456, 789])
|
|
88
|
+
|
|
89
|
+
# With metadata example from above
|
|
90
|
+
metadata = Metadata.load_multi(metadata_ids)
|
|
91
|
+
|
|
92
|
+
# Filter out nils for missing objects
|
|
93
|
+
existing_metadata = Metadata.load_multi(metadata_ids).compact
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**When to use**:
|
|
97
|
+
- Loading collections of related objects
|
|
98
|
+
- Processing query results (ZRANGEBYSCORE, SMEMBERS, etc.)
|
|
99
|
+
- Batch operations
|
|
100
|
+
- Any scenario requiring multiple object lookups
|
|
101
|
+
|
|
102
|
+
**Performance**: 14 objects → 1 pipelined batch with 14 HGETALL commands (96% reduction in round trips)
|
|
103
|
+
|
|
104
|
+
## Detailed Usage
|
|
105
|
+
|
|
106
|
+
### check_exists Parameter
|
|
107
|
+
|
|
108
|
+
The `check_exists` parameter is available on all finder methods:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# find_by_dbkey
|
|
112
|
+
user = User.find_by_dbkey("user:123:object", check_exists: false)
|
|
113
|
+
|
|
114
|
+
# find_by_identifier
|
|
115
|
+
user = User.find_by_identifier(123, check_exists: false)
|
|
116
|
+
|
|
117
|
+
# Aliases (find_by_id, find, load)
|
|
118
|
+
user = User.find_by_id(123, check_exists: false)
|
|
119
|
+
user = User.find(123, check_exists: false)
|
|
120
|
+
user = User.load(123, check_exists: false)
|
|
121
|
+
|
|
122
|
+
# Custom suffix
|
|
123
|
+
session = Session.find_by_identifier('abc123', :session, check_exists: false)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**How it works**:
|
|
127
|
+
|
|
128
|
+
**Safe mode** (`check_exists: true`, default):
|
|
129
|
+
1. Send `EXISTS user:123:object`
|
|
130
|
+
2. If key doesn't exist, return nil immediately
|
|
131
|
+
3. If exists, send `HGETALL user:123:object`
|
|
132
|
+
4. Instantiate object from hash
|
|
133
|
+
|
|
134
|
+
**Optimized mode** (`check_exists: false`):
|
|
135
|
+
1. Send `HGETALL user:123:object` directly
|
|
136
|
+
2. If hash is empty (key doesn't exist), return nil
|
|
137
|
+
3. Otherwise instantiate object from hash
|
|
138
|
+
|
|
139
|
+
**Safety**: Both modes return nil for non-existent keys. Optimized mode detects non-existence via empty hash response.
|
|
140
|
+
|
|
141
|
+
### Pipelined Bulk Loading
|
|
142
|
+
|
|
143
|
+
#### load_multi
|
|
144
|
+
|
|
145
|
+
Load multiple objects by their identifiers:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Basic usage
|
|
149
|
+
users = User.load_multi([123, 456, 789])
|
|
150
|
+
|
|
151
|
+
# Returns array with nils for missing objects
|
|
152
|
+
results = User.load_multi(['id1', 'missing', 'id3'])
|
|
153
|
+
# => [<User:id1>, nil, <User:id3>]
|
|
154
|
+
|
|
155
|
+
# Filter out nils
|
|
156
|
+
existing = User.load_multi(ids).compact
|
|
157
|
+
|
|
158
|
+
# Empty array handling
|
|
159
|
+
User.load_multi([]) # => []
|
|
160
|
+
|
|
161
|
+
# Preserves order
|
|
162
|
+
users = User.load_multi([789, 123, 456])
|
|
163
|
+
users.map(&:user_id) # => [789, 123, 456] (same order)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Parameters**:
|
|
167
|
+
- `identifiers` - Array of identifiers (Strings or Integers)
|
|
168
|
+
- `suffix` - Optional suffix (default: class suffix)
|
|
169
|
+
|
|
170
|
+
**Returns**: Array of objects in same order as input, with nils for non-existent keys
|
|
171
|
+
|
|
172
|
+
#### load_multi_by_keys
|
|
173
|
+
|
|
174
|
+
Load objects by full dbkeys (lower-level variant):
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# When you already have full keys
|
|
178
|
+
keys = [
|
|
179
|
+
"user:123:object",
|
|
180
|
+
"user:456:object",
|
|
181
|
+
"user:789:object"
|
|
182
|
+
]
|
|
183
|
+
users = User.load_multi_by_keys(keys)
|
|
184
|
+
|
|
185
|
+
# Mixed existing and non-existent keys
|
|
186
|
+
keys = ["user:123:object", "user:missing:object"]
|
|
187
|
+
results = User.load_multi_by_keys(keys)
|
|
188
|
+
# => [<User:123>, nil]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**When to use**: When working directly with dbkeys rather than identifiers (rare).
|
|
192
|
+
|
|
193
|
+
#### load_batch Alias
|
|
194
|
+
|
|
195
|
+
`load_batch` is an alias for `load_multi`:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
users = User.load_batch([123, 456, 789])
|
|
199
|
+
# Identical to load_multi
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Handling Edge Cases
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
# Nil identifiers
|
|
206
|
+
results = User.load_multi(['id1', nil, 'id3'])
|
|
207
|
+
# => [<User:id1>, nil, <User:id3>]
|
|
208
|
+
|
|
209
|
+
# Empty string identifiers
|
|
210
|
+
results = User.load_multi(['id1', '', 'id3'])
|
|
211
|
+
# => [<User:id1>, nil, <User:id3>]
|
|
212
|
+
|
|
213
|
+
# All missing
|
|
214
|
+
results = User.load_multi(['missing1', 'missing2'])
|
|
215
|
+
results.compact # => []
|
|
216
|
+
|
|
217
|
+
# Mixed with compact
|
|
218
|
+
existing = User.load_multi(ids).compact
|
|
219
|
+
# Only non-nil objects
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Performance Comparison
|
|
223
|
+
|
|
224
|
+
### Single Object Loading
|
|
225
|
+
|
|
226
|
+
| Method | Commands | Round Trips | Use Case |
|
|
227
|
+
|--------|----------|-------------|----------|
|
|
228
|
+
| `find_by_id(id)` (default) | 2 | 2 | Safe, defensive code |
|
|
229
|
+
| `find_by_id(id, check_exists: false)` | 1 | 1 | Performance-critical |
|
|
230
|
+
| `load_multi([id])` | 1 | 1 | Bulk API consistency |
|
|
231
|
+
|
|
232
|
+
### Bulk Loading (14 Objects)
|
|
233
|
+
|
|
234
|
+
| Method | Commands | Round Trips | Improvement |
|
|
235
|
+
|--------|----------|-------------|-------------|
|
|
236
|
+
| `ids.map { \|id\| find(id) }` | 28 | 28 | Baseline |
|
|
237
|
+
| `ids.map { \|id\| find(id, check_exists: false) }` | 14 | 14 | 50% reduction |
|
|
238
|
+
| `load_multi(ids)` | 14 | 1 | 96% reduction |
|
|
239
|
+
|
|
240
|
+
### Real-World Example
|
|
241
|
+
|
|
242
|
+
Loading customer metadata (your use case):
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
# Get metadata IDs from sorted set (1 command)
|
|
246
|
+
metadata_ids = customer.metadata.rangebyscore(start_time, end_time)
|
|
247
|
+
# => 14 metadata IDs
|
|
248
|
+
|
|
249
|
+
# ❌ Traditional approach: 28 commands, 28 round trips
|
|
250
|
+
metadata = metadata_ids.map { |id| Metadata.find_by_id(id) }
|
|
251
|
+
|
|
252
|
+
# ✅ Optimized approach: 14 commands, 14 round trips (50% reduction)
|
|
253
|
+
metadata = metadata_ids.map { |id| Metadata.find_by_id(id, check_exists: false) }
|
|
254
|
+
|
|
255
|
+
# ✅✅ Best approach: 14 commands, 1 round trip (96% reduction)
|
|
256
|
+
metadata = Metadata.load_multi(metadata_ids).compact
|
|
257
|
+
|
|
258
|
+
# Total commands for full operation:
|
|
259
|
+
# Traditional: 1 (ZRANGEBYSCORE) + 28 (loading) = 29 commands
|
|
260
|
+
# Optimized: 1 (ZRANGEBYSCORE) + 1 (pipelined batch) = 2 commands
|
|
261
|
+
# Improvement: 93% reduction
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Best Practices
|
|
265
|
+
|
|
266
|
+
### 1. Use load_multi for Bulk Operations
|
|
267
|
+
|
|
268
|
+
**Always prefer** `load_multi` when loading multiple objects:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
# ❌ Avoid
|
|
272
|
+
domain_ids = team.domains.members
|
|
273
|
+
domains = domain_ids.map { |id| Domain.find_by_id(id) }
|
|
274
|
+
|
|
275
|
+
# ✅ Better
|
|
276
|
+
domain_ids = team.domains.members
|
|
277
|
+
domains = Domain.load_multi(domain_ids).compact
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 2. Use check_exists: false for Trusted References
|
|
281
|
+
|
|
282
|
+
When loading objects from known-to-exist references:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# Objects from sorted set members
|
|
286
|
+
participant_ids = event.participants.members
|
|
287
|
+
participants = participant_ids.map { |id|
|
|
288
|
+
User.find_by_id(id, check_exists: false)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Even better with load_multi
|
|
292
|
+
participants = User.load_multi(participant_ids).compact
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 3. Keep Default Behavior for Defensive Code
|
|
296
|
+
|
|
297
|
+
Use default `check_exists: true` when:
|
|
298
|
+
- Loading from user input
|
|
299
|
+
- Defensive/paranoid code paths
|
|
300
|
+
- Single object lookups where optimization doesn't matter
|
|
301
|
+
- Initial development before optimization phase
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# User input - keep safe mode
|
|
305
|
+
user = User.find_by_id(params[:user_id])
|
|
306
|
+
|
|
307
|
+
# Internal lookup - optimize
|
|
308
|
+
user = User.find_by_id(session.user_id, check_exists: false)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 4. Compact Results Appropriately
|
|
312
|
+
|
|
313
|
+
Handle nils based on your requirements:
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
# When all objects should exist (raise on missing)
|
|
317
|
+
users = User.load_multi(ids)
|
|
318
|
+
if users.any?(&:nil?)
|
|
319
|
+
raise "Missing users: #{ids.zip(users).select { |_, u| u.nil? }}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# When missing objects are acceptable
|
|
323
|
+
existing_users = User.load_multi(ids).compact
|
|
324
|
+
|
|
325
|
+
# When you need to track which are missing
|
|
326
|
+
results = ids.zip(User.load_multi(ids))
|
|
327
|
+
results.each do |id, user|
|
|
328
|
+
if user.nil?
|
|
329
|
+
logger.warn "User #{id} not found"
|
|
330
|
+
else
|
|
331
|
+
process_user(user)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### 5. Measure Before Optimizing
|
|
337
|
+
|
|
338
|
+
Profile your application to identify bottlenecks:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# Add timing to measure impact
|
|
342
|
+
require 'benchmark'
|
|
343
|
+
|
|
344
|
+
ids = (1..100).to_a
|
|
345
|
+
|
|
346
|
+
# Traditional
|
|
347
|
+
traditional_time = Benchmark.realtime do
|
|
348
|
+
users = ids.map { |id| User.find_by_id(id) }
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Optimized
|
|
352
|
+
optimized_time = Benchmark.realtime do
|
|
353
|
+
users = User.load_multi(ids)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
puts "Traditional: #{traditional_time}s"
|
|
357
|
+
puts "Optimized: #{optimized_time}s"
|
|
358
|
+
puts "Speedup: #{(traditional_time / optimized_time).round(1)}x"
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Implementation Details
|
|
362
|
+
|
|
363
|
+
### Empty Hash Detection
|
|
364
|
+
|
|
365
|
+
When `check_exists: false`, non-existent keys are detected via empty hash:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Non-existent key
|
|
369
|
+
hash = redis.hgetall("user:missing:object")
|
|
370
|
+
# => {} # Empty hash indicates key doesn't exist
|
|
371
|
+
|
|
372
|
+
# Existing key with no fields (edge case)
|
|
373
|
+
redis.hset("user:empty:object", "placeholder", "")
|
|
374
|
+
redis.hdel("user:empty:object", "placeholder")
|
|
375
|
+
hash = redis.hgetall("user:empty:object")
|
|
376
|
+
# => {} # Also empty, but key technically exists
|
|
377
|
+
|
|
378
|
+
# Both cases safely return nil
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Note**: In practice, Familia objects always have fields, so empty hashes reliably indicate non-existent keys.
|
|
382
|
+
|
|
383
|
+
### Pipelining vs Individual Commands
|
|
384
|
+
|
|
385
|
+
**Individual commands**:
|
|
386
|
+
```ruby
|
|
387
|
+
# Each command is a separate round trip
|
|
388
|
+
ids.each do |id|
|
|
389
|
+
key = "user:#{id}:object"
|
|
390
|
+
redis.hgetall(key) # Round trip 1, 2, 3, ...
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Pipelined commands**:
|
|
395
|
+
```ruby
|
|
396
|
+
# All commands in single round trip
|
|
397
|
+
redis.pipelined do |pipeline|
|
|
398
|
+
ids.each do |id|
|
|
399
|
+
key = "user:#{id}:object"
|
|
400
|
+
pipeline.hgetall(key) # Queued locally
|
|
401
|
+
end
|
|
402
|
+
end # Single round trip with all commands
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Field Deserialization
|
|
406
|
+
|
|
407
|
+
All optimized methods use the same deserialization logic as standard loading:
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
# All field types are properly handled
|
|
411
|
+
user = User.load_multi([123]).first
|
|
412
|
+
user.age # Integer field correctly deserialized
|
|
413
|
+
user.active # Boolean field correctly deserialized
|
|
414
|
+
user.metadata # Hash field correctly deserialized
|
|
415
|
+
user.tags # Array field correctly deserialized
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Technical details**:
|
|
419
|
+
- Uses `initialize_with_keyword_args_deserialize_value` internally
|
|
420
|
+
- JSON deserialization for all field values
|
|
421
|
+
- Proper type preservation (Integer, Boolean, Hash, Array, nil)
|
|
422
|
+
|
|
423
|
+
## Migration Guide
|
|
424
|
+
|
|
425
|
+
### Identifying Optimization Opportunities
|
|
426
|
+
|
|
427
|
+
Look for these patterns in your codebase:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
# Pattern 1: Mapping over collection of IDs
|
|
431
|
+
ids.map { |id| Model.find_by_id(id) }
|
|
432
|
+
ids.map { |id| Model.find(id) }
|
|
433
|
+
ids.map { |id| Model.load(id) }
|
|
434
|
+
|
|
435
|
+
# Pattern 2: Loading from sorted set members
|
|
436
|
+
member_ids = sorted_set.members
|
|
437
|
+
members = member_ids.map { |id| Model.find(id) }
|
|
438
|
+
|
|
439
|
+
# Pattern 3: Loading from set members
|
|
440
|
+
tag_ids = set.members
|
|
441
|
+
tags = tag_ids.map { |id| Tag.find(id) }
|
|
442
|
+
|
|
443
|
+
# Pattern 4: Processing query results
|
|
444
|
+
user_ids = redis.zrangebyscore("users:active", start_score, end_score)
|
|
445
|
+
users = user_ids.map { |id| User.find(id) }
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Step-by-Step Migration
|
|
449
|
+
|
|
450
|
+
**Step 1**: Identify bulk loading patterns
|
|
451
|
+
```bash
|
|
452
|
+
# Search your codebase
|
|
453
|
+
grep -r "\.map.*find_by_id" app/
|
|
454
|
+
grep -r "\.map.*\.find(" app/
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Step 2**: Replace with `load_multi`
|
|
458
|
+
```ruby
|
|
459
|
+
# Before
|
|
460
|
+
domains = domain_ids.map { |id| Domain.find(id) }
|
|
461
|
+
|
|
462
|
+
# After
|
|
463
|
+
domains = Domain.load_multi(domain_ids).compact
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Step 3**: Profile the change
|
|
467
|
+
```ruby
|
|
468
|
+
# Add logging temporarily
|
|
469
|
+
start = Time.now
|
|
470
|
+
domains = Domain.load_multi(domain_ids).compact
|
|
471
|
+
duration = Time.now - start
|
|
472
|
+
Rails.logger.info "Loaded #{domains.size} domains in #{duration}s"
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Step 4**: Deploy and monitor
|
|
476
|
+
- Check error rates remain stable
|
|
477
|
+
- Monitor Redis command counts
|
|
478
|
+
- Verify response times improve
|
|
479
|
+
|
|
480
|
+
### Backwards Compatibility
|
|
481
|
+
|
|
482
|
+
All changes are fully backwards compatible:
|
|
483
|
+
|
|
484
|
+
```ruby
|
|
485
|
+
# Existing code continues to work
|
|
486
|
+
user = User.find_by_id(123) # Still works, still safe
|
|
487
|
+
|
|
488
|
+
# New optional parameter
|
|
489
|
+
user = User.find_by_id(123, check_exists: false) # Opt-in optimization
|
|
490
|
+
|
|
491
|
+
# New methods
|
|
492
|
+
users = User.load_multi(ids) # New method, doesn't break existing code
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Common Patterns
|
|
496
|
+
|
|
497
|
+
### Pattern 1: Loading Related Objects
|
|
498
|
+
|
|
499
|
+
```ruby
|
|
500
|
+
class Team < Familia::Horreum
|
|
501
|
+
identifier_field :team_id
|
|
502
|
+
field :team_id, :name
|
|
503
|
+
sorted_set :member_ids # Stores user IDs with scores
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Efficient member loading
|
|
507
|
+
def load_team_members(team)
|
|
508
|
+
member_ids = team.member_ids.members
|
|
509
|
+
User.load_multi(member_ids).compact
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# With sorting by score
|
|
513
|
+
def load_recent_members(team, limit: 10)
|
|
514
|
+
member_ids = team.member_ids.revrange(0, limit - 1)
|
|
515
|
+
User.load_multi(member_ids).compact
|
|
516
|
+
end
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Pattern 2: Filtered Loading
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
# Load and filter in one pass
|
|
523
|
+
def load_active_users(user_ids)
|
|
524
|
+
User.load_multi(user_ids).compact.select(&:active?)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Load with transformation
|
|
528
|
+
def load_user_emails(user_ids)
|
|
529
|
+
User.load_multi(user_ids).compact.map(&:email)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Load with stats
|
|
533
|
+
def load_with_stats(user_ids)
|
|
534
|
+
users = User.load_multi(user_ids)
|
|
535
|
+
{
|
|
536
|
+
found: users.compact.size,
|
|
537
|
+
missing: users.count(&:nil?),
|
|
538
|
+
users: users.compact
|
|
539
|
+
}
|
|
540
|
+
end
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Pattern 3: Batch Processing
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
# Process in batches to avoid memory issues
|
|
547
|
+
def process_all_users(batch_size: 100)
|
|
548
|
+
user_ids = User.instances.members # Get all user IDs
|
|
549
|
+
|
|
550
|
+
user_ids.each_slice(batch_size) do |batch_ids|
|
|
551
|
+
users = User.load_multi(batch_ids).compact
|
|
552
|
+
|
|
553
|
+
users.each do |user|
|
|
554
|
+
process_user(user)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Pattern 4: Multi-Model Loading
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
# Load related objects across different models
|
|
564
|
+
def load_dashboard_data(user_id)
|
|
565
|
+
user = User.find_by_id(user_id, check_exists: false)
|
|
566
|
+
|
|
567
|
+
# Load user's teams and domains in parallel
|
|
568
|
+
team_ids = user.team_ids.members
|
|
569
|
+
domain_ids = user.domain_ids.members
|
|
570
|
+
|
|
571
|
+
teams = Team.load_multi(team_ids).compact
|
|
572
|
+
domains = Domain.load_multi(domain_ids).compact
|
|
573
|
+
|
|
574
|
+
{
|
|
575
|
+
user: user,
|
|
576
|
+
teams: teams,
|
|
577
|
+
domains: domains
|
|
578
|
+
}
|
|
579
|
+
end
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## Troubleshooting
|
|
583
|
+
|
|
584
|
+
### Issue: Unexpected nils in Results
|
|
585
|
+
|
|
586
|
+
**Problem**: `load_multi` returns more nils than expected
|
|
587
|
+
|
|
588
|
+
**Causes**:
|
|
589
|
+
1. Objects genuinely don't exist
|
|
590
|
+
2. Wrong identifier field being used
|
|
591
|
+
3. Suffix mismatch
|
|
592
|
+
|
|
593
|
+
**Solution**:
|
|
594
|
+
```ruby
|
|
595
|
+
# Debug which objects are missing
|
|
596
|
+
ids = [1, 2, 3]
|
|
597
|
+
results = User.load_multi(ids)
|
|
598
|
+
missing_ids = ids.zip(results).select { |_, obj| obj.nil? }.map(&:first)
|
|
599
|
+
puts "Missing: #{missing_ids}"
|
|
600
|
+
|
|
601
|
+
# Check if keys exist
|
|
602
|
+
missing_ids.each do |id|
|
|
603
|
+
key = User.dbkey(id)
|
|
604
|
+
exists = Familia.redis.exists(key)
|
|
605
|
+
puts "#{key}: #{exists}"
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Verify correct suffix
|
|
609
|
+
User.suffix # Check what suffix the class uses
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Issue: Performance Not Improving
|
|
613
|
+
|
|
614
|
+
**Problem**: `load_multi` doesn't seem faster
|
|
615
|
+
|
|
616
|
+
**Causes**:
|
|
617
|
+
1. Small dataset (overhead of pipelining)
|
|
618
|
+
2. Local Redis instance (network latency minimal)
|
|
619
|
+
3. Not actually using `load_multi`
|
|
620
|
+
|
|
621
|
+
**Solution**:
|
|
622
|
+
```ruby
|
|
623
|
+
# Benchmark with realistic dataset
|
|
624
|
+
require 'benchmark'
|
|
625
|
+
|
|
626
|
+
# Create test data
|
|
627
|
+
ids = (1..100).map do |i|
|
|
628
|
+
user = User.new(user_id: i, name: "User #{i}")
|
|
629
|
+
user.save
|
|
630
|
+
i
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Compare approaches
|
|
634
|
+
Benchmark.bm(20) do |x|
|
|
635
|
+
x.report("traditional:") { ids.map { |id| User.find(id) } }
|
|
636
|
+
x.report("check_exists:false:") { ids.map { |id| User.find(id, check_exists: false) } }
|
|
637
|
+
x.report("load_multi:") { User.load_multi(ids) }
|
|
638
|
+
end
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Issue: Order Not Preserved
|
|
642
|
+
|
|
643
|
+
**Problem**: Results appear in wrong order
|
|
644
|
+
|
|
645
|
+
**Cause**: Using `compact` changes indices
|
|
646
|
+
|
|
647
|
+
**Solution**:
|
|
648
|
+
```ruby
|
|
649
|
+
# ❌ Loses position information
|
|
650
|
+
ids = [1, 2, 3]
|
|
651
|
+
users = User.load_multi(ids).compact # [<User:1>, <User:3>] if 2 is missing
|
|
652
|
+
|
|
653
|
+
# ✅ Preserve positions with zip
|
|
654
|
+
ids.zip(User.load_multi(ids)).each do |id, user|
|
|
655
|
+
if user
|
|
656
|
+
puts "Processing user #{id}"
|
|
657
|
+
process_user(user)
|
|
658
|
+
else
|
|
659
|
+
puts "User #{id} not found"
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# ✅ Or track original indices
|
|
664
|
+
users_with_ids = User.load_multi(ids).map.with_index do |user, idx|
|
|
665
|
+
[ids[idx], user]
|
|
666
|
+
end
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
## See Also
|
|
670
|
+
|
|
671
|
+
- [Core Field System](core-field-system.md) - Understanding Familia's field types
|
|
672
|
+
- [Relationships Guide](feature-relationships.md) - Loading related objects
|
|
673
|
+
- [Time Utilities](time-utilities.md) - For score-based queries with timestamps
|
|
674
|
+
- [Implementation Guide](implementation.md) - Advanced Familia internals
|