familia 2.0.0.pre15 → 2.0.0.pre16
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/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_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 +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +75 -43
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
data/lib/familia/connection.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
# lib/familia/connection.rb
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
3
|
+
require_relative 'connection/handlers'
|
4
|
+
require_relative 'connection/middleware'
|
5
|
+
require_relative 'connection/operations'
|
6
|
+
require_relative 'connection/individual_command_proxy'
|
7
|
+
require_relative 'connection/operation_core'
|
8
|
+
require_relative 'connection/transaction_core'
|
9
|
+
require_relative 'connection/pipeline_core'
|
5
10
|
|
6
11
|
# Familia
|
7
12
|
#
|
@@ -9,7 +14,8 @@ require_relative 'multi_result'
|
|
9
14
|
#
|
10
15
|
module Familia
|
11
16
|
@uri = URI.parse 'redis://127.0.0.1:6379'
|
12
|
-
@
|
17
|
+
@middleware_registered = false
|
18
|
+
@middleware_version = 0
|
13
19
|
|
14
20
|
# The Connection module provides Database connection management for Familia.
|
15
21
|
# It allows easy setup and access to Database clients across different URIs
|
@@ -18,222 +24,86 @@ module Familia
|
|
18
24
|
# @return [URI] The default URI for Database connections
|
19
25
|
attr_reader :uri
|
20
26
|
|
21
|
-
# @return [Hash] A hash of Database clients, keyed by server ID
|
22
|
-
attr_reader :database_clients
|
23
|
-
|
24
|
-
# @return [Boolean] Whether Database command logging is enabled
|
25
|
-
attr_accessor :enable_database_logging
|
26
|
-
|
27
|
-
# @return [Boolean] Whether Database command counter is enabled
|
28
|
-
attr_accessor :enable_database_counter
|
29
|
-
|
30
27
|
# @return [Proc] A callable that provides Database connections
|
31
|
-
|
28
|
+
# The provider should accept a URI string and return a Redis connection
|
29
|
+
# already connected to the correct database specified in the URI.
|
30
|
+
#
|
31
|
+
# @example Setting a connection provider
|
32
|
+
# Familia.connection_provider = ->(uri) do
|
33
|
+
# pool = ConnectionPool.new { Redis.new(url: uri) }
|
34
|
+
# pool.with { |conn| conn }
|
35
|
+
# end
|
36
|
+
attr_reader :connection_provider
|
32
37
|
|
33
|
-
#
|
34
|
-
|
38
|
+
# Sets the connection provider and bumps middleware version
|
39
|
+
def connection_provider=(provider)
|
40
|
+
@connection_provider = provider
|
41
|
+
increment_middleware_version! if provider
|
42
|
+
@connection_chain = nil # Force rebuild of chain
|
43
|
+
end
|
35
44
|
|
36
45
|
# Sets the default URI for Database connections.
|
37
46
|
#
|
38
47
|
# NOTE: uri is not a property of the Settings module b/c it's not
|
39
48
|
# configured in class defintions like default_expiration or logical DB index.
|
40
49
|
#
|
41
|
-
# @param
|
42
|
-
# @example
|
43
|
-
# Familia.uri = 'redis://localhost:6379'
|
50
|
+
# @param uri [String, URI] The new default URI
|
51
|
+
# @example Familia.uri = 'redis://localhost:6379'
|
44
52
|
def uri=(uri)
|
45
53
|
@uri = normalize_uri(uri)
|
46
54
|
end
|
47
55
|
alias url uri
|
48
56
|
alias url= uri=
|
49
57
|
|
50
|
-
#
|
58
|
+
# Creates a new Database connection instance.
|
59
|
+
#
|
60
|
+
# This method always creates a fresh connection and does not use caching.
|
61
|
+
# Each call returns a new Redis client instance that you are responsible
|
62
|
+
# for managing and closing when done.
|
51
63
|
#
|
52
64
|
# @param uri [String, URI, nil] The URI of the Database server to connect to.
|
53
|
-
# If nil, uses the default URI from
|
54
|
-
# @return [Redis]
|
65
|
+
# If nil, uses the default URI from Familia.uri.
|
66
|
+
# @return [Redis] A new Database client connection.
|
55
67
|
# @raise [ArgumentError] If no URI is specified.
|
56
|
-
#
|
57
|
-
#
|
58
|
-
|
68
|
+
#
|
69
|
+
# @example Creating a new connection
|
70
|
+
# client = Familia.create_dbclient('redis://localhost:6379')
|
71
|
+
# client.ping
|
72
|
+
# client.close
|
73
|
+
#
|
74
|
+
def create_dbclient(uri = nil)
|
59
75
|
parsed_uri = normalize_uri(uri)
|
60
76
|
|
61
|
-
|
62
|
-
|
63
|
-
RedisClient.register(DatabaseLogger)
|
64
|
-
end
|
65
|
-
|
66
|
-
if Familia.enable_database_counter
|
67
|
-
# NOTE: This middleware uses AtommicFixnum from concurrent-ruby which is
|
68
|
-
# less contentious than Mutex-based counters. Safe for
|
69
|
-
RedisClient.register(DatabaseCommandCounter)
|
70
|
-
end
|
77
|
+
# Register middleware only once, globally
|
78
|
+
register_middleware_once
|
71
79
|
|
72
80
|
Redis.new(parsed_uri.conf)
|
73
81
|
end
|
82
|
+
alias connect create_dbclient # backwards compatibility
|
83
|
+
alias isolated_dbclient create_dbclient # matches with_isolated_dbclient api
|
74
84
|
|
75
|
-
|
76
|
-
parsed_uri = normalize_uri(uri)
|
77
|
-
serverid = parsed_uri.serverid
|
78
|
-
|
79
|
-
# Close the existing connection if it exists
|
80
|
-
@database_clients[serverid].close if @database_clients.key?(serverid)
|
81
|
-
@database_clients.delete(serverid)
|
82
|
-
|
83
|
-
connect(parsed_uri)
|
84
|
-
end
|
85
|
-
|
86
|
-
# Retrieves a Database connection from the appropriate pool.
|
85
|
+
# Retrieves a Database connection using the Chain of Responsibility pattern.
|
87
86
|
# Handles DB selection automatically based on the URI.
|
88
87
|
#
|
89
88
|
# @return [Redis] The Database client for the specified URI
|
90
|
-
# @example
|
91
|
-
# Familia.dbclient('redis://localhost:6379/1')
|
89
|
+
# @example Familia.dbclient('redis://localhost:6379/1')
|
92
90
|
# Familia.dbclient(2) # Use DB 2 with default server
|
93
91
|
def dbclient(uri = nil)
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
# Second priority: Connection provider
|
98
|
-
if connection_provider
|
99
|
-
# Always pass normalized URI with database to provider
|
100
|
-
# Provider MUST return connection already on the correct database
|
101
|
-
parsed_uri = normalize_uri(uri)
|
102
|
-
client = connection_provider.call(parsed_uri.to_s)
|
103
|
-
|
104
|
-
# In debug mode, verify the provider honored the contract
|
105
|
-
if Familia.debug? && client.respond_to?(:client)
|
106
|
-
current_db = client.connection[:db]
|
107
|
-
expected_db = parsed_uri.db || 0
|
108
|
-
Familia.ld "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
|
109
|
-
if current_db != expected_db
|
110
|
-
Familia.warn "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
return client
|
115
|
-
end
|
116
|
-
|
117
|
-
# Third priority: Fallback behavior or error
|
118
|
-
raise Familia::NoConnectionAvailable, 'No connection available.' if connection_required
|
119
|
-
|
120
|
-
# Legacy behavior: create connection
|
121
|
-
parsed_uri = normalize_uri(uri)
|
122
|
-
serverid = parsed_uri.serverid
|
123
|
-
|
124
|
-
@database_clients[serverid] ||= connect(parsed_uri)
|
125
|
-
end
|
126
|
-
|
127
|
-
# Executes Database commands atomically within a transaction (MULTI/EXEC).
|
128
|
-
#
|
129
|
-
# Database transactions queue commands and execute them atomically as a single unit.
|
130
|
-
# All commands succeed together or all fail together, ensuring data consistency.
|
131
|
-
#
|
132
|
-
# @yield [Redis] The Database transaction connection
|
133
|
-
# @return [Array] Results of all commands executed in the transaction
|
134
|
-
#
|
135
|
-
# @example Basic transaction usage
|
136
|
-
# Familia.transaction do |trans|
|
137
|
-
# trans.set("key1", "value1")
|
138
|
-
# trans.incr("counter")
|
139
|
-
# trans.lpush("list", "item")
|
140
|
-
# end
|
141
|
-
# # Returns: ["OK", 2, 1] - results of all commands
|
142
|
-
#
|
143
|
-
# @note **Comparison of Database batch operations:**
|
144
|
-
#
|
145
|
-
# | Feature | Multi/Exec | Pipeline |
|
146
|
-
# |-----------------|-----------------|-----------------|
|
147
|
-
# | Atomicity | Yes | No |
|
148
|
-
# | Performance | Good | Better |
|
149
|
-
# | Error handling | All-or-nothing | Per-command |
|
150
|
-
# | Use case | Data consistency| Bulk operations |
|
151
|
-
#
|
152
|
-
def transaction(&)
|
153
|
-
block_result = nil
|
154
|
-
result = dbclient.multi do |conn|
|
155
|
-
Fiber[:familia_transaction] = conn
|
156
|
-
begin
|
157
|
-
block_result = yield(conn)
|
158
|
-
ensure
|
159
|
-
Fiber[:familia_transaction] = nil # cleanup reference
|
160
|
-
end
|
161
|
-
end
|
162
|
-
# Return the multi result which contains the transaction results
|
163
|
-
result
|
92
|
+
@connection_chain ||= build_connection_chain
|
93
|
+
@connection_chain.handle(uri)
|
164
94
|
end
|
165
|
-
alias multi transaction
|
166
95
|
|
167
|
-
#
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
# @return [Array] Results of all commands executed in the pipeline
|
175
|
-
#
|
176
|
-
# @example Basic pipeline usage
|
177
|
-
# Familia.pipeline do |pipe|
|
178
|
-
# pipe.set("key1", "value1")
|
179
|
-
# pipe.incr("counter")
|
180
|
-
# pipe.lpush("list", "item")
|
181
|
-
# end
|
182
|
-
# # Returns: ["OK", 2, 1] - results of all commands
|
183
|
-
#
|
184
|
-
# @example Error handling - commands succeed/fail independently
|
185
|
-
# results = Familia.pipeline do |conn|
|
186
|
-
# conn.set("valid_key", "value") # This will succeed
|
187
|
-
# conn.incr("string_key") # This will fail (wrong type)
|
188
|
-
# conn.set("another_key", "value2") # This will still succeed
|
189
|
-
# end
|
190
|
-
# # Returns: ["OK", Redis::CommandError, "OK"]
|
191
|
-
# # Notice how the error doesn't prevent other commands from executing
|
192
|
-
#
|
193
|
-
# @example Contrast with transaction behavior
|
194
|
-
# results = Familia.transaction do |conn|
|
195
|
-
# conn.set("inventory:item1", 100)
|
196
|
-
# conn.incr("invalid_key") # Fails, rolls back everything
|
197
|
-
# conn.set("inventory:item2", 200) # Won't be applied
|
198
|
-
# end
|
199
|
-
# # Result: neither item1 nor item2 are set due to the error
|
200
|
-
#
|
201
|
-
def pipeline(&)
|
202
|
-
block_result = nil
|
203
|
-
result = dbclient.pipelined do |conn|
|
204
|
-
Fiber[:familia_pipeline] = conn
|
205
|
-
begin
|
206
|
-
block_result = yield(conn)
|
207
|
-
ensure
|
208
|
-
Fiber[:familia_pipeline] = nil # cleanup reference
|
209
|
-
end
|
210
|
-
end
|
211
|
-
# Return the pipeline result which contains the command results
|
212
|
-
result
|
96
|
+
# Builds the connection chain with handlers in priority order
|
97
|
+
def build_connection_chain
|
98
|
+
ResponsibilityChain.new
|
99
|
+
.add_handler(Familia::Connection::FiberTransactionHandler.new)
|
100
|
+
.add_handler(FiberConnectionHandler.new)
|
101
|
+
.add_handler(ProviderConnectionHandler.new)
|
102
|
+
.add_handler(CreateConnectionHandler.new)
|
213
103
|
end
|
214
104
|
|
215
|
-
# Provides explicit access to a Database connection.
|
216
|
-
#
|
217
|
-
# This method is useful when you need direct access to a connection
|
218
|
-
# for operations not covered by other methods. The connection is
|
219
|
-
# properly managed and returned to the pool (if using connection_provider).
|
220
|
-
#
|
221
|
-
# @yield [Redis] A Database connection
|
222
|
-
# @return The result of the block
|
223
|
-
#
|
224
|
-
# @example Using with_connection for custom operations
|
225
|
-
# Familia.with_connection do |conn|
|
226
|
-
# conn.set("custom_key", "value")
|
227
|
-
# conn.expire("custom_key", 3600)
|
228
|
-
# end
|
229
|
-
#
|
230
|
-
def with_connection(&)
|
231
|
-
yield dbclient
|
232
|
-
end
|
233
|
-
|
234
|
-
private
|
235
|
-
|
236
105
|
# Normalizes various URI formats to a consistent URI object
|
106
|
+
# Made public so handlers can use it
|
237
107
|
def normalize_uri(uri)
|
238
108
|
case uri
|
239
109
|
when Integer
|
@@ -250,5 +120,9 @@ module Familia
|
|
250
120
|
raise ArgumentError, "Invalid URI type: #{uri.class.name}"
|
251
121
|
end
|
252
122
|
end
|
123
|
+
|
124
|
+
# Extend self with submodules to make their methods available as module methods
|
125
|
+
include Familia::Connection::Middleware
|
126
|
+
include Familia::Connection::Operations
|
253
127
|
end
|
254
128
|
end
|
@@ -1,56 +1,58 @@
|
|
1
1
|
# lib/familia/data_type/commands.rb
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
3
|
+
module Familia
|
4
|
+
class DataType
|
5
|
+
# Must be included in all DataType classes to provide Valkey/Redis
|
6
|
+
# commands. The class must have a dbkey method.
|
7
|
+
module Commands
|
8
|
+
def move(logical_database)
|
9
|
+
dbclient.move dbkey, logical_database
|
10
|
+
end
|
11
|
+
|
12
|
+
def rename(newkey)
|
13
|
+
dbclient.rename dbkey, newkey
|
14
|
+
end
|
15
|
+
|
16
|
+
def renamenx(newkey)
|
17
|
+
dbclient.renamenx dbkey, newkey
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
dbclient.type dbkey
|
22
|
+
end
|
23
|
+
|
24
|
+
# Deletes the entire dbkey
|
25
|
+
# @return [Boolean] true if the key was deleted, false otherwise
|
26
|
+
def delete!
|
27
|
+
Familia.trace :DELETE!, nil, uri if Familia.debug?
|
28
|
+
ret = dbclient.del dbkey
|
29
|
+
ret.positive?
|
30
|
+
end
|
31
|
+
alias clear delete!
|
32
|
+
|
33
|
+
def exists?
|
34
|
+
dbclient.exists(dbkey) && !size.zero?
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_expiration
|
38
|
+
dbclient.ttl dbkey
|
39
|
+
end
|
40
|
+
|
41
|
+
def expire(sec)
|
42
|
+
dbclient.expire dbkey, sec.to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
def expireat(unixtime)
|
46
|
+
dbclient.expireat dbkey, unixtime
|
47
|
+
end
|
48
|
+
|
49
|
+
def persist
|
50
|
+
dbclient.persist dbkey
|
51
|
+
end
|
52
|
+
|
53
|
+
def echo(*args)
|
54
|
+
dbclient.echo "[#{self.class}] #{args.join(' ')} (#{opts&.fetch(:class, '<no opts>')})"
|
55
|
+
end
|
54
56
|
end
|
55
57
|
end
|
56
58
|
end
|
@@ -1,127 +1,128 @@
|
|
1
1
|
# lib/familia/data_type/serialization.rb
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
3
|
+
module Familia
|
4
|
+
class DataType
|
5
|
+
module Serialization
|
6
|
+
# Serializes a value for storage in the database.
|
7
|
+
#
|
8
|
+
# @param val [Object] The value to be serialized.
|
9
|
+
# @param strict_values [Boolean] Whether to enforce strict value
|
10
|
+
# serialization (default: true).
|
11
|
+
# @return [String, nil] The serialized representation of the value, or nil
|
12
|
+
# if serialization fails.
|
13
|
+
#
|
14
|
+
# @note When a class option is specified, it uses that class's
|
15
|
+
# serialization method. Otherwise, it relies on Familia.distinguisher for
|
16
|
+
# serialization.
|
17
|
+
#
|
18
|
+
# @example With a class option
|
19
|
+
# serialize_value(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
|
20
|
+
#
|
21
|
+
# @example Without a class option
|
22
|
+
# serialize_value(123) #=> "123"
|
23
|
+
# serialize_value("hello") #=> "hello"
|
24
|
+
#
|
25
|
+
# @raise [Familia::NotDistinguishableError] If serialization fails under strict
|
26
|
+
# mode.
|
27
|
+
#
|
28
|
+
def serialize_value(val, strict_values: true)
|
29
|
+
prepared = nil
|
29
30
|
|
30
|
-
|
31
|
+
Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}>" if Familia.debug?
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
if opts[:class]
|
34
|
+
prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
|
35
|
+
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
if prepared.nil?
|
39
|
+
# Enforce strict values when no class option is specified
|
40
|
+
prepared = Familia.distinguisher(val, strict_values: true)
|
41
|
+
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
if Familia.debug?
|
45
|
+
Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>"
|
46
|
+
end
|
42
47
|
|
43
|
-
|
44
|
-
|
45
|
-
caller(1..1)
|
48
|
+
Familia.warn "[#{self.class}#serialize_value] nil returned for #{opts[:class]}##{name}" if prepared.nil?
|
49
|
+
prepared
|
46
50
|
end
|
47
51
|
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
# Deserializes multiple values from Valkey/Redis, removing nil values.
|
53
|
+
#
|
54
|
+
# @param values [Array<String>] The values to deserialize.
|
55
|
+
# @return [Array<Object>] Deserialized objects, with nil values removed.
|
56
|
+
#
|
57
|
+
# @see #deserialize_values_with_nil
|
58
|
+
#
|
59
|
+
def deserialize_values(*values)
|
60
|
+
# Avoid using compact! here. Using compact! as the last expression in the
|
61
|
+
# method can unintentionally return nil if no changes are made, which is
|
62
|
+
# not desirable. Instead, use compact to ensure the method returns the
|
63
|
+
# expected value.
|
64
|
+
deserialize_values_with_nil(*values).compact
|
65
|
+
end
|
51
66
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
#
|
61
|
-
# method
|
62
|
-
#
|
63
|
-
#
|
64
|
-
deserialize_values_with_nil(*values)
|
65
|
-
|
67
|
+
# Deserializes multiple values from Valkey/Redis, preserving nil values.
|
68
|
+
#
|
69
|
+
# @param values [Array<String>] The values to deserialize.
|
70
|
+
# @return [Array<Object, nil>] Deserialized objects, including nil values.
|
71
|
+
#
|
72
|
+
# @raise [Familia::Problem] If the specified class doesn't respond to the
|
73
|
+
# load method.
|
74
|
+
#
|
75
|
+
# @note This method attempts to deserialize each value using the specified
|
76
|
+
# class's load method. If deserialization fails for a value, it's
|
77
|
+
# replaced with nil.
|
78
|
+
#
|
79
|
+
def deserialize_values_with_nil(*values)
|
80
|
+
Familia.ld "deserialize_values: (#{@opts}) #{values}"
|
81
|
+
return [] if values.empty?
|
82
|
+
return values.flatten unless @opts[:class]
|
66
83
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
# @return [Array<Object, nil>] Deserialized objects, including nil values.
|
71
|
-
#
|
72
|
-
# @raise [Familia::Problem] If the specified class doesn't respond to the
|
73
|
-
# load method.
|
74
|
-
#
|
75
|
-
# @note This method attempts to deserialize each value using the specified
|
76
|
-
# class's load method. If deserialization fails for a value, it's
|
77
|
-
# replaced with nil.
|
78
|
-
#
|
79
|
-
def deserialize_values_with_nil(*values)
|
80
|
-
Familia.ld "deserialize_values: (#{@opts}) #{values}"
|
81
|
-
return [] if values.empty?
|
82
|
-
return values.flatten unless @opts[:class]
|
84
|
+
unless @opts[:class].respond_to?(load_method)
|
85
|
+
raise Familia::Problem, "No such method: #{@opts[:class]}##{load_method}"
|
86
|
+
end
|
83
87
|
|
84
|
-
|
85
|
-
|
86
|
-
end
|
88
|
+
values.collect! do |obj|
|
89
|
+
next if obj.nil?
|
87
90
|
|
88
|
-
|
89
|
-
|
91
|
+
val = @opts[:class].send load_method, obj
|
92
|
+
Familia.ld "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
|
90
93
|
|
91
|
-
|
92
|
-
|
94
|
+
val
|
95
|
+
rescue StandardError => e
|
96
|
+
Familia.info val
|
97
|
+
Familia.info "Parse error for #{dbkey} (#{load_method}): #{e.message}"
|
98
|
+
Familia.info e.backtrace
|
99
|
+
nil
|
100
|
+
end
|
93
101
|
|
94
|
-
|
95
|
-
rescue StandardError => e
|
96
|
-
Familia.info val
|
97
|
-
Familia.info "Parse error for #{dbkey} (#{load_method}): #{e.message}"
|
98
|
-
Familia.info e.backtrace
|
99
|
-
nil
|
102
|
+
values
|
100
103
|
end
|
101
104
|
|
102
|
-
|
103
|
-
|
105
|
+
# Deserializes a single value from the database.
|
106
|
+
#
|
107
|
+
# @param val [String, nil] The value to deserialize.
|
108
|
+
# @return [Object, nil] The deserialized object, the default value if
|
109
|
+
# val is nil, or nil if deserialization fails.
|
110
|
+
#
|
111
|
+
# @note If no class option is specified, the original value is
|
112
|
+
# returned unchanged.
|
113
|
+
#
|
114
|
+
# NOTE: Currently only the DataType class uses this method. Horreum
|
115
|
+
# fields are a newer addition and don't support the full range of
|
116
|
+
# deserialization options that DataType supports. It uses serialize_value
|
117
|
+
# for serialization since everything becomes a string in Valkey.
|
118
|
+
#
|
119
|
+
def deserialize_value(val)
|
120
|
+
return @opts[:default] if val.nil?
|
121
|
+
return val unless @opts[:class]
|
104
122
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
# @return [Object, nil] The deserialized object, the default value if
|
109
|
-
# val is nil, or nil if deserialization fails.
|
110
|
-
#
|
111
|
-
# @note If no class option is specified, the original value is
|
112
|
-
# returned unchanged.
|
113
|
-
#
|
114
|
-
# NOTE: Currently only the DataType class uses this method. Horreum
|
115
|
-
# fields are a newer addition and don't support the full range of
|
116
|
-
# deserialization options that DataType supports. It uses serialize_value
|
117
|
-
# for serialization since everything becomes a string in Valkey.
|
118
|
-
#
|
119
|
-
def deserialize_value(val)
|
120
|
-
return @opts[:default] if val.nil?
|
121
|
-
return val unless @opts[:class]
|
122
|
-
|
123
|
-
ret = deserialize_values val
|
124
|
-
ret&.first # return the object or nil
|
123
|
+
ret = deserialize_values val
|
124
|
+
ret&.first # return the object or nil
|
125
|
+
end
|
125
126
|
end
|
126
127
|
end
|
127
128
|
end
|