familia 2.0.0.pre17 → 2.0.0.pre19
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/CHANGELOG.rst +118 -6
- data/CLAUDE.md +43 -11
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +52 -0
- data/bin/irb +1 -1
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/core-field-system.md +48 -26
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +2 -1
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +9 -6
- data/lib/familia/data_type/serialization.rb +9 -5
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +45 -44
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +151 -65
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +130 -11
- data/lib/familia/horreum/definition.rb +8 -38
- data/lib/familia/horreum/management.rb +38 -27
- data/lib/familia/horreum/persistence.rb +191 -67
- data/lib/familia/horreum/serialization.rb +94 -73
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +41 -18
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +268 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +118 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +5 -5
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +2 -2
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +34 -5
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +5 -5
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +4 -4
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +2 -2
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +13 -13
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +23 -23
- data/try/integration/cross_component_try.rb +1 -1
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +6 -2
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +2 -2
- data/try/{core → integration}/persistence_operations_try.rb +163 -11
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +2 -2
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +2 -2
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +33 -17
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/{horreum → unit/horreum}/base_try.rb +4 -4
- data/try/{horreum → unit/horreum}/class_methods_try.rb +3 -3
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +3 -3
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +5 -5
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +6 -6
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +147 -126
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
data/lib/familia/horreum.rb
CHANGED
@@ -51,7 +51,6 @@ module Familia
|
|
51
51
|
include Familia::Base
|
52
52
|
include Familia::Horreum::Persistence
|
53
53
|
include Familia::Horreum::Serialization
|
54
|
-
include Familia::Horreum::Connection
|
55
54
|
include Familia::Horreum::DatabaseCommands
|
56
55
|
include Familia::Horreum::Settings
|
57
56
|
include Familia::Horreum::Utils
|
@@ -226,23 +225,43 @@ module Familia
|
|
226
225
|
# Default values are intentionally NOT set here
|
227
226
|
end
|
228
227
|
|
229
|
-
# Implementing classes can define an init method to do any
|
230
|
-
#
|
231
|
-
#
|
228
|
+
# Implementing classes can define an init method to do any additional
|
229
|
+
# initialization. Notice that this is called AFTER fields are set from
|
230
|
+
# kwargs, so kwargs have been consumed and are no longer available.
|
231
|
+
#
|
232
|
+
# IMPORTANT: Use ||= in init to apply defaults without overriding:
|
233
|
+
# def init
|
234
|
+
# @email ||= email # Preserves value already set
|
235
|
+
# @status ||= 'pending' # Applies default if nil
|
236
|
+
# end
|
237
|
+
#
|
232
238
|
init
|
233
239
|
end
|
234
240
|
|
235
|
-
#
|
236
|
-
#
|
241
|
+
# Initialization method called at the end of initialize
|
242
|
+
#
|
243
|
+
# Override this method to apply defaults, run validations, or setup
|
244
|
+
# callbacks. It's recommended to call super as other modules like
|
245
|
+
# features can also override init.
|
246
|
+
#
|
247
|
+
# IMPORTANT: The init method receieves no arguments. By the time this runs,
|
248
|
+
# all arguments to initialize have already been consumed and used to set
|
249
|
+
# fields. Use the ||= operator to preserve values already set:
|
237
250
|
#
|
238
|
-
#
|
251
|
+
# def init(email: nil, user_id: nil, **kwargs)
|
252
|
+
# @email ||= email # Preserves value from new()
|
253
|
+
# @user_id ||= user_id # Preserves value from new()
|
254
|
+
# @created_at ||= Familia.now # Applies default if not set
|
239
255
|
#
|
240
|
-
# Example
|
241
|
-
#
|
242
|
-
#
|
256
|
+
# # Example of additional initialization logic
|
257
|
+
# validate_email_format if @email
|
258
|
+
# setup_callbacks
|
243
259
|
# end
|
244
|
-
|
245
|
-
|
260
|
+
#
|
261
|
+
# @return [void]
|
262
|
+
#
|
263
|
+
def init
|
264
|
+
# Default no-op - override in subclasses
|
246
265
|
end
|
247
266
|
|
248
267
|
# Sets up related Database objects for the instance
|
@@ -299,7 +318,9 @@ module Familia
|
|
299
318
|
|
300
319
|
def initialize_with_keyword_args_deserialize_value(**fields)
|
301
320
|
# Deserialize Database string values back to their original types
|
302
|
-
deserialized_fields = fields.
|
321
|
+
deserialized_fields = fields.each_with_object({}) do |(field_name, value), hsh|
|
322
|
+
hsh[field_name] = deserialize_value(value, field_name: field_name)
|
323
|
+
end
|
303
324
|
initialize_with_keyword_args(**deserialized_fields)
|
304
325
|
end
|
305
326
|
|
@@ -308,7 +329,7 @@ module Familia
|
|
308
329
|
#
|
309
330
|
# This method is part of horreum.rb rather than serialization.rb because it
|
310
331
|
# operates solely on the provided values and doesn't query Database or other
|
311
|
-
# external sources. That's why it's called "
|
332
|
+
# external sources. That's why it's called "naive" refresh: it assumes
|
312
333
|
# the provided values are correct and updates the object accordingly.
|
313
334
|
#
|
314
335
|
# @see #refresh!
|
@@ -316,8 +337,8 @@ module Familia
|
|
316
337
|
# @param fields [Hash] A hash of field names and their new values to update
|
317
338
|
# the object with.
|
318
339
|
# @return [Array] The list of field names that were updated.
|
319
|
-
def
|
320
|
-
Familia.ld "[
|
340
|
+
def naive_refresh(**fields)
|
341
|
+
Familia.ld "[naive_refresh] #{self.class} #{dbkey} #{fields.keys}"
|
321
342
|
initialize_with_keyword_args_deserialize_value(**fields)
|
322
343
|
end
|
323
344
|
|
@@ -401,8 +422,10 @@ module Familia
|
|
401
422
|
self.class.fields.filter_map do |field|
|
402
423
|
# Database will give us field names as strings back, but internally
|
403
424
|
# we use symbols. So we check for both.
|
404
|
-
|
405
|
-
|
425
|
+
# Use fetch with default to avoid || operator which skips false values
|
426
|
+
value = fields.fetch(field.to_sym) { fields[field.to_s] }
|
427
|
+
# Check for nil explicitly to allow false and 0 values
|
428
|
+
unless value.nil?
|
406
429
|
# Use the mapped method name, not the field name
|
407
430
|
method_name = self.class.field_method_map[field] || field
|
408
431
|
send(:"#{method_name}=", value)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# lib/familia/identifier_extractor.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
# IdentifierExtractor - Extracts identifiers from Familia objects for storage
|
5
|
+
#
|
6
|
+
# This module provides a focused mechanism for converting object references
|
7
|
+
# into Redis-storable strings. It handles two primary cases:
|
8
|
+
#
|
9
|
+
# 1. Class references: Customer → "Customer"
|
10
|
+
# 2. Familia::Base instances: customer_obj → customer_obj.identifier
|
11
|
+
#
|
12
|
+
# This is primarily used by DataType serialization when storing object
|
13
|
+
# references in Redis data structures (lists, sets, zsets). It extracts
|
14
|
+
# the identifier rather than serializing the entire object.
|
15
|
+
#
|
16
|
+
# @example With class_zset
|
17
|
+
# class Customer < Familia::Horreum
|
18
|
+
# class_zset :instances, class: self
|
19
|
+
# end
|
20
|
+
# # When adding: Customer.instances.add(customer_obj)
|
21
|
+
# # Stores: customer_obj.identifier (e.g., "customer_123")
|
22
|
+
#
|
23
|
+
module IdentifierExtractor
|
24
|
+
# Extracts a Redis-storable identifier from a Familia object or class.
|
25
|
+
#
|
26
|
+
# @param value [Object] The value to extract an identifier from
|
27
|
+
# @return [String] The extracted identifier or class name
|
28
|
+
# @raise [Familia::NotDistinguishableError] If value is not a Class or Familia::Base
|
29
|
+
#
|
30
|
+
def identifier_extractor(value, strict_values: true)
|
31
|
+
case value
|
32
|
+
when ::Symbol, ::String, ::Integer, ::Float
|
33
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'simple_value' if Familia.debug?
|
34
|
+
# DataTypes (lists, sets, zsets) can store simple values directly
|
35
|
+
# Convert to string for Redis storage
|
36
|
+
value.to_s
|
37
|
+
|
38
|
+
when Class
|
39
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'class' if Familia.debug?
|
40
|
+
value.name
|
41
|
+
|
42
|
+
when Familia::Base
|
43
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_instance' if Familia.debug?
|
44
|
+
value.identifier
|
45
|
+
|
46
|
+
else
|
47
|
+
# Check if value's class inherits from Familia::Base
|
48
|
+
if value.class.ancestors.member?(Familia::Base)
|
49
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'base_ancestor' if Familia.debug?
|
50
|
+
value.identifier
|
51
|
+
else
|
52
|
+
Familia.trace :IDENTIFIER_EXTRACTOR, nil, 'error' if Familia.debug?
|
53
|
+
raise Familia::NotDistinguishableError, value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
extend IdentifierExtractor
|
60
|
+
end
|
data/lib/familia/logging.rb
CHANGED
@@ -3,151 +3,307 @@
|
|
3
3
|
require 'pathname'
|
4
4
|
require 'logger'
|
5
5
|
|
6
|
+
# Familia - Logbook
|
7
|
+
#
|
6
8
|
module Familia
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
9
|
+
# Custom Logger subclass with TRACE level support.
|
10
|
+
#
|
11
|
+
# FamiliaLogger extends Ruby's standard Logger with a TRACE level for
|
12
|
+
# extremely detailed debugging output. The TRACE level is numerically
|
13
|
+
# equal to DEBUG (0) but distinguishes itself via a thread-local marker
|
14
|
+
# that the LogFormatter uses to output 'T' instead of 'D'.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# logger = Familia::FamiliaLogger.new($stderr)
|
18
|
+
# logger.level = Familia::FamiliaLogger::TRACE
|
19
|
+
# logger.trace "Detailed trace message"
|
20
|
+
# # => T, 10-05 20:43:09.843 pid:123 [456/789]: Detailed trace message
|
21
|
+
#
|
22
|
+
# @example With progname
|
23
|
+
# logger.trace("MyApp") { "Trace with progname" }
|
24
|
+
#
|
25
|
+
# @see Familia::LogFormatter
|
26
|
+
#
|
27
|
+
class FamiliaLogger < Logger
|
28
|
+
# TRACE severity level (numerically equal to DEBUG=0).
|
29
|
+
#
|
30
|
+
# Uses the same numeric level as DEBUG but signals via thread-local
|
31
|
+
# marker to output 'T' prefix instead of 'D'. This approach works
|
32
|
+
# around Logger's limitation with negative severity values.
|
33
|
+
#
|
34
|
+
# Standard Logger levels: DEBUG=0, INFO=1, WARN=2, ERROR=3, FATAL=4, UNKNOWN=5
|
35
|
+
TRACE = 0
|
36
|
+
|
37
|
+
# Log a TRACE level message.
|
38
|
+
#
|
39
|
+
# This method behaves like the standard Logger methods (debug, info, etc.)
|
40
|
+
# but outputs with a 'T' severity letter when used with LogFormatter.
|
41
|
+
#
|
42
|
+
# @param progname [String, nil] Program name to include in log output
|
43
|
+
# @yield Block that returns the message to log (lazy evaluation)
|
44
|
+
# @return [true] Always returns true
|
45
|
+
#
|
46
|
+
# @example Simple message
|
47
|
+
# logger.trace("Entering complex calculation")
|
48
|
+
#
|
49
|
+
# @example With block for lazy evaluation
|
50
|
+
# logger.trace { "Expensive: #{expensive_debug_info}" }
|
51
|
+
#
|
52
|
+
# @example With progname
|
53
|
+
# logger.trace("MyApp") { "Application trace" }
|
54
|
+
#
|
55
|
+
# @note Sets Fiber[:familia_trace_mode] during execution to
|
56
|
+
# signal LogFormatter to output 'T' instead of 'D'
|
57
|
+
#
|
58
|
+
def trace(progname = nil, &)
|
59
|
+
# Store marker in thread-local to signal this is TRACE not DEBUG
|
60
|
+
# Track whether we set the flag to avoid clearing it in nested calls
|
61
|
+
was_already_tracing = Fiber[:familia_trace_mode]
|
62
|
+
Fiber[:familia_trace_mode] = true
|
63
|
+
add(TRACE, nil, progname, &)
|
64
|
+
ensure
|
65
|
+
# Only clear the flag if we set it (not already tracing)
|
66
|
+
Fiber[:familia_trace_mode] = false unless was_already_tracing
|
67
|
+
end
|
26
68
|
end
|
27
69
|
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
70
|
+
# Custom formatter for Familia logger output.
|
71
|
+
#
|
72
|
+
# LogFormatter produces structured log output with severity letters,
|
73
|
+
# timestamps, process/thread/fiber IDs, and the log message.
|
31
74
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# A custom log level for trace messages, typically used for very detailed
|
35
|
-
# debugging information.
|
75
|
+
# Output format:
|
76
|
+
# SEVERITY, MM-DD HH:MM:SS.mmm pid:PID [THREAD_ID/FIBER_ID]: MESSAGE
|
36
77
|
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# Familia::Refinements::LoggerTrace is used.
|
78
|
+
# @example Output
|
79
|
+
# I, 10-05 20:43:09.843 pid:12345 [67890/54321]: Connection established
|
80
|
+
# T, 10-05 20:43:10.123 pid:12345 [67890/54321]: [LOAD] redis -> user:123
|
41
81
|
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
82
|
+
# Severity letters:
|
83
|
+
# T = TRACE (when Fiber[:familia_trace_mode] is set, or level 0 when not using FamiliaLogger)
|
84
|
+
# D = DEBUG
|
85
|
+
# I = INFO
|
86
|
+
# W = WARN
|
87
|
+
# E = ERROR
|
88
|
+
# F = FATAL
|
89
|
+
# U = UNKNOWN
|
45
90
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
91
|
+
# @example Use with FamiliaLogger for TRACE support
|
92
|
+
# logger = Familia::FamiliaLogger.new($stderr)
|
93
|
+
# logger.formatter = Familia::LogFormatter.new
|
94
|
+
# logger.trace("Trace message") # => T, ...
|
95
|
+
# logger.debug("Debug message") # => D, ...
|
49
96
|
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
97
|
+
# @example Use with standard Logger (level 0 becomes 'T')
|
98
|
+
# logger = Logger.new($stderr)
|
99
|
+
# logger.formatter = Familia::LogFormatter.new
|
100
|
+
# logger.debug("Debug message") # => T, ... (because DEBUG=0)
|
53
101
|
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
102
|
+
# @note When used with FamiliaLogger, checks Fiber[:familia_trace_mode] to
|
103
|
+
# distinguish TRACE from DEBUG. When used with standard Logger, treats
|
104
|
+
# level 0 as TRACE since DEBUG and TRACE share the same numeric level.
|
57
105
|
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
106
|
+
# @see FamiliaLogger#trace
|
107
|
+
#
|
108
|
+
class LogFormatter < Logger::Formatter
|
109
|
+
# Severity string to letter mapping.
|
110
|
+
#
|
111
|
+
# Maps severity string labels to single-letter codes for compact output.
|
112
|
+
# Note: TRACE is handled via Fiber check in #call for FamiliaLogger.
|
113
|
+
SEVERITY_LETTERS = {
|
114
|
+
'DEBUG' => 'D',
|
115
|
+
'INFO' => 'I',
|
116
|
+
'WARN' => 'W',
|
117
|
+
'ERROR' => 'E',
|
118
|
+
'FATAL' => 'F',
|
119
|
+
'UNKNOWN' => 'U',
|
120
|
+
'ANY' => 'T' # ANY is Logger's label for severity < 0, treat as TRACE
|
121
|
+
}.freeze
|
122
|
+
|
123
|
+
# Format a log message with severity, timestamp, and context.
|
124
|
+
#
|
125
|
+
# @param severity [String] Severity label (e.g., "INFO", "DEBUG", "UNKNOWN")
|
126
|
+
# @param datetime [Time] Timestamp of the log message
|
127
|
+
# @param _progname [String] Program name (unused, kept for Logger compatibility)
|
128
|
+
# @param msg [String] The log message
|
129
|
+
# @return [String] Formatted log line with newline
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
# formatter = Familia::LogFormatter.new
|
133
|
+
# formatter.call("INFO", Time.now, nil, "Test message")
|
134
|
+
# # => "I, 10-05 20:43:09.843 pid:12345 [67890/54321]: Test message\n"
|
135
|
+
#
|
136
|
+
def call(severity, datetime, _progname, msg)
|
137
|
+
# Check if we're in trace mode (TRACE uses same level as DEBUG but marks itself)
|
138
|
+
# FamiliaLogger sets Fiber[:familia_trace_mode] when trace() is called
|
139
|
+
severity_letter = if Fiber[:familia_trace_mode]
|
140
|
+
'T'
|
141
|
+
else
|
142
|
+
SEVERITY_LETTERS.fetch(severity, severity[0])
|
143
|
+
end
|
144
|
+
|
145
|
+
utc_datetime = datetime.utc.strftime('%H:%M:%S.%3N')
|
146
|
+
|
147
|
+
"#{severity_letter}, #{utc_datetime} #{msg}\n"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# The Logging module provides logging capabilities for Familia.
|
152
|
+
#
|
153
|
+
# Familia uses a custom FamiliaLogger that extends the standard Ruby Logger
|
154
|
+
# with a TRACE level for detailed debugging output.
|
155
|
+
#
|
156
|
+
# == Log Levels (from most to least verbose):
|
157
|
+
# - TRACE: Extremely detailed debugging (controlled by FAMILIA_TRACE env var)
|
158
|
+
# - DEBUG: Detailed debugging information
|
159
|
+
# - INFO: General informational messages
|
160
|
+
# - WARN: Warning messages
|
161
|
+
# - ERROR: Error messages
|
162
|
+
# - FATAL: Fatal errors that cause termination
|
61
163
|
#
|
62
164
|
# == Usage:
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
# TRACE = 0
|
73
|
-
#
|
74
|
-
# def trace(progname = nil, &block)
|
75
|
-
# add(TRACE, nil, progname, &block)
|
76
|
-
# end
|
77
|
-
# end
|
78
|
-
# end
|
79
|
-
#
|
80
|
-
# using Familia::Refinements::LoggerTrace
|
81
|
-
#
|
82
|
-
# logger = Logger.new(STDOUT)
|
83
|
-
# logger.trace("This is a trace message")
|
84
|
-
# logger.debug("This is a debug message")
|
85
|
-
# logger.info("This is an info message")
|
86
|
-
# logger.warn("This is a warning message")
|
87
|
-
# logger.error("This is an error message")
|
88
|
-
# logger.fatal("This is a fatal message")
|
89
|
-
#
|
90
|
-
# In this example, the Familia::Refinements::LoggerTrace module is defined with a refinement
|
91
|
-
# for the Logger class. The TRACE constant and trace method are added to the Logger
|
92
|
-
# class within the refinement. The `using` keyword is used to apply the refinement
|
93
|
-
# in the scope where it's needed.
|
94
|
-
#
|
95
|
-
# == Conditions:
|
96
|
-
# The trace method and TRACE log level are only available if the Familia::Refinements::LoggerTrace
|
97
|
-
# module is used with the `using` keyword. Without this, the Logger class will not
|
98
|
-
# have the trace method or the TRACE log level.
|
99
|
-
#
|
100
|
-
# == Minimum Ruby Version:
|
101
|
-
# This module requires Ruby 2.0.0 or later to use refinements.
|
165
|
+
# # Use default logger
|
166
|
+
# Familia.info "Connection established"
|
167
|
+
# Familia.warn "Cache miss"
|
168
|
+
#
|
169
|
+
# # Set custom logger
|
170
|
+
# Familia.logger = Logger.new('familia.log')
|
171
|
+
#
|
172
|
+
# # Trace-level debugging (requires FAMILIA_TRACE=true)
|
173
|
+
# Familia.trace :LOAD, redis_client, "user:123", "from cache"
|
102
174
|
#
|
103
175
|
module Logging
|
104
|
-
|
105
|
-
|
106
|
-
#
|
107
|
-
|
176
|
+
# Get the logger instance, initializing with defaults if not yet set
|
177
|
+
#
|
178
|
+
# @return [FamiliaLogger] the logger instance
|
179
|
+
#
|
180
|
+
# @example Set a custom logger
|
181
|
+
# Familia.logger = Logger.new('familia.log')
|
182
|
+
#
|
183
|
+
# @example Use the default logger
|
184
|
+
# Familia.logger.info "Connection established"
|
185
|
+
#
|
186
|
+
def logger
|
187
|
+
@logger ||= FamiliaLogger.new($stderr).tap do |log|
|
188
|
+
log.progname = name
|
189
|
+
log.formatter = LogFormatter.new
|
190
|
+
end
|
191
|
+
end
|
108
192
|
|
109
|
-
|
110
|
-
|
193
|
+
# Set a custom logger instance.
|
194
|
+
#
|
195
|
+
# Allows replacing the default FamiliaLogger with any Logger-compatible
|
196
|
+
# object. Useful for integrating with application logging frameworks.
|
197
|
+
#
|
198
|
+
# @param new_logger [Logger] The logger to use
|
199
|
+
# @return [Logger] The logger that was set
|
200
|
+
#
|
201
|
+
# @example Use Rails logger
|
202
|
+
# Familia.logger = Rails.logger
|
203
|
+
#
|
204
|
+
# @example Custom file logger
|
205
|
+
# Familia.logger = Logger.new('familia.log').tap do |log|
|
206
|
+
# log.level = Logger::INFO
|
207
|
+
# end
|
208
|
+
#
|
209
|
+
def logger=(new_logger)
|
210
|
+
@logger = new_logger
|
111
211
|
end
|
112
212
|
|
113
|
-
|
114
|
-
|
213
|
+
# Log an informational message.
|
214
|
+
#
|
215
|
+
# @param msg [String] The message to log
|
216
|
+
# @return [true]
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# Familia.info "Redis connection established"
|
220
|
+
#
|
221
|
+
def info(msg)
|
222
|
+
logger.info(msg)
|
115
223
|
end
|
116
224
|
|
117
|
-
|
118
|
-
|
225
|
+
# Log a warning message.
|
226
|
+
#
|
227
|
+
# @param msg [String] The message to log
|
228
|
+
# @return [true]
|
229
|
+
#
|
230
|
+
# @example
|
231
|
+
# Familia.warn "Cache miss for key: user:123"
|
232
|
+
#
|
233
|
+
def warn(msg)
|
234
|
+
logger.warn(msg)
|
235
|
+
end
|
119
236
|
|
120
|
-
|
237
|
+
# Log a debug message (only when Familia.debug? is true).
|
238
|
+
#
|
239
|
+
# Short for "log debug". Only outputs when FAMILIA_DEBUG environment
|
240
|
+
# variable is set to '1' or 'true'.
|
241
|
+
#
|
242
|
+
# @param msg [String] The message to log
|
243
|
+
# @return [true, nil] Returns true if logged, nil if debug disabled
|
244
|
+
#
|
245
|
+
# @example
|
246
|
+
# Familia.ld "Cache lookup for user:123"
|
247
|
+
# # Only outputs when FAMILIA_DEBUG=true
|
248
|
+
#
|
249
|
+
def ld(msg)
|
250
|
+
logger.debug(msg) if Familia.debug?
|
121
251
|
end
|
122
252
|
|
123
|
-
|
124
|
-
|
253
|
+
# Log an error message.
|
254
|
+
#
|
255
|
+
# Short for "log error".
|
256
|
+
#
|
257
|
+
# @param msg [String] The message to log
|
258
|
+
# @return [true]
|
259
|
+
#
|
260
|
+
# @example
|
261
|
+
# Familia.le "Failed to deserialize value: #{e.message}"
|
262
|
+
#
|
263
|
+
def le(msg)
|
264
|
+
logger.error(msg)
|
125
265
|
end
|
126
266
|
|
127
|
-
# Logs a trace message for debugging
|
267
|
+
# Logs a structured trace message for debugging Familia operations.
|
268
|
+
#
|
269
|
+
# This method only executes when both FAMILIA_TRACE and FAMILIA_DEBUG
|
270
|
+
# environment variables are enabled.
|
128
271
|
#
|
129
272
|
# @param label [Symbol] A label for the trace message (e.g., :EXPAND,
|
130
273
|
# :FROMREDIS, :LOAD, :EXISTS).
|
131
|
-
# @param instance_id
|
132
|
-
# @param ident [String] An identifier or key related to the operation being
|
133
|
-
#
|
134
|
-
# @param extra_context [Array<String>, String, nil] Any extra details to include.
|
135
|
-
#
|
136
|
-
# @example Familia.trace :LOAD, Familia.dbclient(uri), objkey if Familia.debug?
|
274
|
+
# @param instance_id [Object] The object instance being traced (e.g., Redis client)
|
275
|
+
# @param ident [String] An identifier or key related to the operation being traced
|
276
|
+
# @param extra_context [String, nil] Any extra details to include
|
137
277
|
#
|
138
278
|
# @return [nil]
|
139
279
|
#
|
140
|
-
# @
|
141
|
-
#
|
142
|
-
#
|
143
|
-
#
|
280
|
+
# @example
|
281
|
+
# Familia.trace :LOAD, redis_client, "user:123", "from cache"
|
282
|
+
# # Output: T, 10-05 20:43:09.843 pid:123 [456/789]: [LOAD] #<Redis> -> user:123 <-from cache
|
283
|
+
#
|
284
|
+
# @note Controlled by FAMILIA_TRACE environment variable (set to '1', 'true', or 'yes')
|
285
|
+
# @note The instance_id can be a Redis client, Redis::Future, or nil
|
144
286
|
#
|
145
287
|
def trace(label, instance_id = nil, ident = nil, extra_context = nil)
|
146
|
-
return unless Familia
|
288
|
+
return unless trace_enabled? && Familia.debug?
|
289
|
+
|
290
|
+
ident_str = ident.nil? ? '<nil>' : ident.to_s
|
291
|
+
logger.trace format('[%s] %s -> %s <-%s', label, instance_id, ident_str, extra_context)
|
292
|
+
end
|
147
293
|
|
148
|
-
|
149
|
-
|
150
|
-
|
294
|
+
private
|
295
|
+
|
296
|
+
# Check if trace logging is enabled via FAMILIA_TRACE environment variable.
|
297
|
+
#
|
298
|
+
# Trace logging is enabled when FAMILIA_TRACE is set to '1', 'true',
|
299
|
+
# or 'yes' (case-insensitive). Checks the environment variable on every
|
300
|
+
# call to support dynamic changes in test environments.
|
301
|
+
#
|
302
|
+
# @return [Boolean] true if trace logging is enabled
|
303
|
+
# @api private
|
304
|
+
#
|
305
|
+
def trace_enabled?
|
306
|
+
%w[1 true yes].include?(ENV.fetch('FAMILIA_TRACE', 'false').downcase)
|
151
307
|
end
|
152
308
|
end
|
153
309
|
end
|
data/lib/familia/refinements.rb
CHANGED
data/lib/familia/settings.rb
CHANGED
@@ -11,7 +11,7 @@ module Familia
|
|
11
11
|
@encryption_keys = nil
|
12
12
|
@current_key_version = nil
|
13
13
|
@encryption_personalization = 'FamilialMatters'.freeze
|
14
|
-
@
|
14
|
+
@pipelined_mode = :warn
|
15
15
|
|
16
16
|
# Familia::Settings
|
17
17
|
#
|
@@ -118,24 +118,24 @@ module Familia
|
|
118
118
|
#
|
119
119
|
# @example Setting pipeline mode
|
120
120
|
# Familia.configure do |config|
|
121
|
-
# config.
|
121
|
+
# config.pipelined_mode = :permissive
|
122
122
|
# end
|
123
123
|
#
|
124
|
-
def
|
124
|
+
def pipelined_mode(val = nil)
|
125
125
|
if val
|
126
126
|
unless [:strict, :warn, :permissive].include?(val)
|
127
127
|
raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
|
128
128
|
end
|
129
|
-
@
|
129
|
+
@pipelined_mode = val
|
130
130
|
end
|
131
|
-
@
|
131
|
+
@pipelined_mode || :warn # default to warn mode
|
132
132
|
end
|
133
133
|
|
134
|
-
def
|
134
|
+
def pipelined_mode=(val)
|
135
135
|
unless [:strict, :warn, :permissive].include?(val)
|
136
136
|
raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
|
137
137
|
end
|
138
|
-
@
|
138
|
+
@pipelined_mode = val
|
139
139
|
end
|
140
140
|
|
141
141
|
# Configure Familia settings
|
data/lib/familia/version.rb
CHANGED
data/lib/familia.rb
CHANGED
@@ -39,7 +39,7 @@ module Familia
|
|
39
39
|
using Refinements::StylizeWords
|
40
40
|
|
41
41
|
class << self
|
42
|
-
attr_accessor :debug
|
42
|
+
attr_accessor :debug
|
43
43
|
attr_reader :members
|
44
44
|
|
45
45
|
def included(member)
|
@@ -139,7 +139,7 @@ module Familia
|
|
139
139
|
require_relative 'familia/connection'
|
140
140
|
require_relative 'familia/settings'
|
141
141
|
require_relative 'familia/utils'
|
142
|
-
require_relative 'familia/
|
142
|
+
require_relative 'familia/identifier_extractor'
|
143
143
|
require_relative 'familia/json_serializer'
|
144
144
|
|
145
145
|
extend SecureIdentifier
|