familia 2.0.0.pre15 → 2.0.0.pre17
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/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -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 +42 -42
- 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 +624 -20
- 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 +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -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/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- 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 +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- 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 +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- 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 +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -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 +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- 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 +2 -2
- 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 +10 -10
- 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 +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- 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 +3 -3
- 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/sorted_set_zadd_options_try.rb +625 -0
- 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/field_groups_try.rb +244 -0
- 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 +443 -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 +3 -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 +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -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/horreum/settings_try.rb +2 -0
- 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 +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- 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
- data/try/valkey.conf +26 -0
- metadata +92 -52
- 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/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- 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
@@ -0,0 +1,223 @@
|
|
1
|
+
# lib/familia/connection/handlers.rb
|
2
|
+
|
3
|
+
# Familia
|
4
|
+
#
|
5
|
+
# A family warehouse for your keystore data.
|
6
|
+
#
|
7
|
+
module Familia
|
8
|
+
module Connection
|
9
|
+
# Manages ordered chain of connection handlers
|
10
|
+
#
|
11
|
+
# NOTE: It is important that the last handler in a responsibility chain
|
12
|
+
# either always provides a connection or raises an error. Otherwise the
|
13
|
+
# end result will simply be `nil` without any guidance to the caller.
|
14
|
+
#
|
15
|
+
class ResponsibilityChain
|
16
|
+
def initialize
|
17
|
+
@handlers = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_handler(handler)
|
21
|
+
@handlers << handler
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle(uri)
|
26
|
+
@handlers.each do |handler|
|
27
|
+
connection = handler.handle(uri)
|
28
|
+
if connection
|
29
|
+
Fiber[:familia_connection_handler_class] = handler.class
|
30
|
+
return connection
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# If we get here, no handler provided a connection
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Connection handler base class for Chain of Responsibility pattern.
|
40
|
+
# When no arguments are passed, all behaviour is based on the top
|
41
|
+
# Familia module itself. e.g. Familia.create_dbclient.
|
42
|
+
#
|
43
|
+
# Summary of Behaviors
|
44
|
+
#
|
45
|
+
# | Handler | Transaction | Pipeline | Ad-hoc Commands |
|
46
|
+
# |---------|------------|----------|-----------------|
|
47
|
+
# | **FiberTransaction** | Reentrant (same conn) | Error | Use transaction conn |
|
48
|
+
# | **FiberConnection** | Error | Error | ✓ Allowed |
|
49
|
+
# | **Provider** | ✓ New checkout | ✓ New checkout | ✓ New checkout |
|
50
|
+
# | **Default** | ✓ With guards | ✓ With guards | ✓ Check mode |
|
51
|
+
# | **Create** | ✓ Fresh conn | ✓ Fresh conn | ✓ Fresh conn |
|
52
|
+
#
|
53
|
+
# NOTE: Every subclass must provide values for the @allows_transaction
|
54
|
+
# and @allows_pipelined attributes.
|
55
|
+
#
|
56
|
+
class BaseConnectionHandler
|
57
|
+
@allows_transaction = true
|
58
|
+
@allows_pipelined = true
|
59
|
+
|
60
|
+
class << self
|
61
|
+
attr_reader :allows_transaction, :allows_pipelined
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize(familia_module = nil)
|
65
|
+
@familia_module = familia_module || Familia
|
66
|
+
end
|
67
|
+
|
68
|
+
def handle(uri)
|
69
|
+
raise NotImplementedError, 'Subclasses must implement handle'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Creates new connections directly, with no caching of any kind. If
|
74
|
+
# the make it to here in the chain, it'll create a new connection
|
75
|
+
# every time.
|
76
|
+
#
|
77
|
+
# Fresh connection each time - all operations safe (transactions,
|
78
|
+
# pipelined, ad-hoc)
|
79
|
+
#
|
80
|
+
class CreateConnectionHandler < BaseConnectionHandler
|
81
|
+
@allows_transaction = true
|
82
|
+
@allows_pipelined = true
|
83
|
+
|
84
|
+
def handle(uri)
|
85
|
+
# Create new connection (no module-level caching)
|
86
|
+
parsed_uri = @familia_module.normalize_uri(uri)
|
87
|
+
client = @familia_module.create_dbclient(parsed_uri)
|
88
|
+
Familia.trace :DBCLIENT_DEFAULT, nil, "Created new connection for #{parsed_uri.serverid}"
|
89
|
+
client
|
90
|
+
end
|
91
|
+
end
|
92
|
+
DefaultConnectionHandler = CreateConnectionHandler
|
93
|
+
|
94
|
+
# Delegates to user-defined connection provider
|
95
|
+
#
|
96
|
+
# Provider pattern = full flexibility. Use ad-hoc, operations, whatever you
|
97
|
+
# like. For each connection, choose one and then get another connection.
|
98
|
+
# Rapid-fire sub ms connection pool connection checkouts are all good
|
99
|
+
# and also expected how they are to be used.
|
100
|
+
# This is where connection pools live
|
101
|
+
#
|
102
|
+
class ProviderConnectionHandler < BaseConnectionHandler
|
103
|
+
@allows_transaction = true
|
104
|
+
@allows_pipelined = true
|
105
|
+
|
106
|
+
def handle(uri)
|
107
|
+
return nil unless @familia_module.connection_provider
|
108
|
+
|
109
|
+
@familia_module.trace :DBCLIENT_PROVIDER, nil, 'Using connection provider'
|
110
|
+
|
111
|
+
# Determine the correct URI including logical database if needed
|
112
|
+
if uri.nil? && @familia_module.respond_to?(:logical_database) && @familia_module.logical_database
|
113
|
+
uri = @familia_module.logical_database
|
114
|
+
end
|
115
|
+
|
116
|
+
# Always pass normalized URI with database to provider
|
117
|
+
# Provider MUST return connection already on the correct database
|
118
|
+
parsed_uri = @familia_module.normalize_uri(uri)
|
119
|
+
@familia_module.connection_provider.call(parsed_uri.to_s)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Checks for fiber-local connections with version validation
|
124
|
+
#
|
125
|
+
# Strict Ad-hoc Only. Raise error for transaction, pipeline etc operations.
|
126
|
+
#
|
127
|
+
# # Enforce middleware connection constraints
|
128
|
+
# case request.operation
|
129
|
+
# when :transaction
|
130
|
+
# raise Familia::MiddlewareConnectionError,
|
131
|
+
# "Cannot start transaction on middleware-provided connection. " \
|
132
|
+
# "Middleware connections are for ad-hoc commands only."
|
133
|
+
# when :pipeline
|
134
|
+
# raise Familia::MiddlewareConnectionError,
|
135
|
+
# "Cannot start pipeline on middleware-provided connection. " \
|
136
|
+
# "Middleware connections are for ad-hoc commands only."
|
137
|
+
# when :command, nil
|
138
|
+
# # Ad-hoc commands are fine
|
139
|
+
# conn
|
140
|
+
# else
|
141
|
+
# raise "Unknown operation: #{request.operation}"
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
class FiberConnectionHandler < BaseConnectionHandler
|
145
|
+
@allows_transaction = false
|
146
|
+
@allows_pipelined = false
|
147
|
+
|
148
|
+
def handle(uri)
|
149
|
+
return nil unless Fiber[:familia_connection]
|
150
|
+
|
151
|
+
conn, version = Fiber[:familia_connection]
|
152
|
+
if version == @familia_module.middleware_version
|
153
|
+
@familia_module.trace :DBCLIENT_FIBER, nil, "Using fiber-local connection for #{uri}"
|
154
|
+
conn
|
155
|
+
else
|
156
|
+
# Version mismatch, clear stale connection
|
157
|
+
Fiber[:familia_connection] = nil
|
158
|
+
@familia_module.trace :DBCLIENT_FIBER, nil, 'Cleared stale fiber connection (version mismatch)'
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Checks for fiber-local transaction connections (highest priority for Horreum)
|
165
|
+
#
|
166
|
+
# Key insight: Mark that we're in reentrant mode and also track of
|
167
|
+
# depth. This allows nested transaction calls to be safely reentrant
|
168
|
+
# without breaking Redis's single-level MULTI/EXEC.
|
169
|
+
#
|
170
|
+
# Reentrant transaction - just yield the existing connection
|
171
|
+
# No new MULTI/EXEC, just participate in existing transaction
|
172
|
+
# Fiber[:familia_transaction_depth] ||= 0
|
173
|
+
# Fiber[:familia_transaction_depth] += 1
|
174
|
+
#
|
175
|
+
class FiberTransactionHandler < BaseConnectionHandler
|
176
|
+
@allows_transaction = :reentrant
|
177
|
+
@allows_pipelined = false
|
178
|
+
|
179
|
+
# Singleton pattern for stateless handler
|
180
|
+
@instance = new.freeze
|
181
|
+
|
182
|
+
def self.instance
|
183
|
+
@instance
|
184
|
+
end
|
185
|
+
|
186
|
+
def handle(_uri)
|
187
|
+
return nil unless Fiber[:familia_transaction]
|
188
|
+
|
189
|
+
Familia.trace :DBCLIENT_FIBER_TRANSACTION, nil, 'Using fiber-local transaction connection'
|
190
|
+
Fiber[:familia_transaction]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Checks for a dbclient class instance variable with a cached client instance
|
195
|
+
#
|
196
|
+
# This works on any module, class, or instance that implements has a
|
197
|
+
# dbclient method. From a Horreum model instance, if you call
|
198
|
+
# CachedConnectionHandler.new(self) it'll return self.dbclient or
|
199
|
+
# nil, or you can call CachedConnectionHandler(self.class) and it'll
|
200
|
+
# attempt the same using the model's class.
|
201
|
+
#
|
202
|
+
# +familia_module+ is required.
|
203
|
+
#
|
204
|
+
# CachedConnectionHandler - Single cached connection - block all multi-mode operations
|
205
|
+
#
|
206
|
+
class CachedConnectionHandler < BaseConnectionHandler
|
207
|
+
@allows_transaction = false
|
208
|
+
@allows_pipelined = false
|
209
|
+
|
210
|
+
def initialize(familia_module)
|
211
|
+
@familia_module = familia_module
|
212
|
+
end
|
213
|
+
|
214
|
+
def handle(_uri)
|
215
|
+
dbclient = @familia_module.instance_variable_get(:@dbclient)
|
216
|
+
return nil unless dbclient
|
217
|
+
|
218
|
+
Familia.trace :DBCLIENT_INSTVAL_OVERRIDE, nil, "Using @dbclient from #{@familia_module.class}"
|
219
|
+
dbclient
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# lib/familia/connection/individual_command_proxy.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Connection
|
5
|
+
# Proxy class that executes Redis commands individually instead of in a transaction
|
6
|
+
#
|
7
|
+
# This class intercepts Redis method calls and executes them immediately against
|
8
|
+
# the underlying connection, collecting results as if they were part of a transaction.
|
9
|
+
# Used as a fallback when transaction mode is unavailable but graceful degradation
|
10
|
+
# is preferred over raising an error.
|
11
|
+
#
|
12
|
+
# @example Usage in transaction fallback
|
13
|
+
# conn = dbclient
|
14
|
+
# proxy = IndividualCommandProxy.new(conn)
|
15
|
+
#
|
16
|
+
# proxy.set('key1', 'value1') # Executes immediately
|
17
|
+
# proxy.incr('counter') # Executes immediately
|
18
|
+
# proxy.get('key1') # Executes immediately
|
19
|
+
#
|
20
|
+
# results = proxy.collected_results # => ["OK", 1, "value1"]
|
21
|
+
#
|
22
|
+
class IndividualCommandProxy
|
23
|
+
attr_reader :collected_results
|
24
|
+
|
25
|
+
def initialize(redis_connection)
|
26
|
+
@connection = redis_connection
|
27
|
+
@collected_results = []
|
28
|
+
end
|
29
|
+
|
30
|
+
# Intercepts Redis method calls and executes them immediately
|
31
|
+
#
|
32
|
+
# @param method_name [Symbol] The Redis method being called
|
33
|
+
# @param args [Array] Arguments passed to the Redis method
|
34
|
+
# @param kwargs [Hash] Keyword arguments passed to the Redis method
|
35
|
+
# @param block [Proc] Block passed to the Redis method
|
36
|
+
# @return The result of the Redis command execution
|
37
|
+
#
|
38
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
39
|
+
if @connection.respond_to?(method_name)
|
40
|
+
result = @connection.public_send(method_name, *args, **kwargs, &block)
|
41
|
+
@collected_results << result
|
42
|
+
result
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def respond_to_missing?(method_name, include_private = false)
|
49
|
+
@connection.respond_to?(method_name, include_private) || super
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns debug information about the proxy state
|
53
|
+
#
|
54
|
+
# @return [Hash] Debug information including connection class and result count
|
55
|
+
def debug_info
|
56
|
+
{
|
57
|
+
connection_class: @connection.class.name,
|
58
|
+
results_count: @collected_results.size,
|
59
|
+
results: @collected_results.dup
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# lib/familia/connection/middleware.rb
|
2
|
+
|
3
|
+
require_relative '../../middleware/database_middleware'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Connection
|
7
|
+
module Middleware
|
8
|
+
# @return [Boolean] Whether Database command logging is enabled
|
9
|
+
attr_reader :enable_database_logging
|
10
|
+
|
11
|
+
# @return [Boolean] Whether Database command counter is enabled
|
12
|
+
attr_reader :enable_database_counter
|
13
|
+
|
14
|
+
# @return [Integer] Current middleware version for cache invalidation
|
15
|
+
def middleware_version
|
16
|
+
@middleware_version
|
17
|
+
end
|
18
|
+
|
19
|
+
# Increments the middleware version, invalidating all cached connections
|
20
|
+
def increment_middleware_version!
|
21
|
+
@middleware_version += 1
|
22
|
+
Familia.trace :MIDDLEWARE_VERSION, nil, "Incremented to #{@middleware_version}" if Familia.debug?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sets a versioned fiber-local connection
|
26
|
+
def set_fiber_connection(connection)
|
27
|
+
Fiber[:familia_connection] = [connection, middleware_version]
|
28
|
+
Familia.trace :FIBER_CONNECTION, nil, "Set with version #{middleware_version}" if Familia.debug?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Clears the fiber-local connection
|
32
|
+
def clear_fiber_connection!
|
33
|
+
Fiber[:familia_connection] = nil
|
34
|
+
Familia.trace :FIBER_CONNECTION, nil, 'Cleared' if Familia.debug?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets whether Database command logging is enabled
|
38
|
+
# Registers middleware immediately when enabled
|
39
|
+
def enable_database_logging=(value)
|
40
|
+
@enable_database_logging = value
|
41
|
+
register_middleware_once if value
|
42
|
+
increment_middleware_version! if value
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sets whether Database command counter is enabled
|
46
|
+
# Registers middleware immediately when enabled
|
47
|
+
def enable_database_counter=(value)
|
48
|
+
@enable_database_counter = value
|
49
|
+
register_middleware_once if value
|
50
|
+
increment_middleware_version! if value
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Registers middleware once globally, regardless of when clients are created.
|
56
|
+
# This prevents duplicate middleware registration and ensures all clients get middleware.
|
57
|
+
def register_middleware_once
|
58
|
+
return if @middleware_registered
|
59
|
+
|
60
|
+
if Familia.enable_database_logging
|
61
|
+
DatabaseLogger.logger = Familia.logger
|
62
|
+
RedisClient.register(DatabaseLogger)
|
63
|
+
end
|
64
|
+
|
65
|
+
if Familia.enable_database_counter
|
66
|
+
# NOTE: This middleware uses AtomicFixnum from concurrent-ruby which is
|
67
|
+
# less contentious than Mutex-based counters. Safe for production.
|
68
|
+
RedisClient.register(DatabaseCommandCounter)
|
69
|
+
end
|
70
|
+
|
71
|
+
@middleware_registered = true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Connection
|
5
|
+
# Shared logic for transaction and pipeline operation fallback handling
|
6
|
+
#
|
7
|
+
# Provides configurable fallback behavior when connection handlers don't
|
8
|
+
# support specific operation modes (transaction/pipeline).
|
9
|
+
#
|
10
|
+
module OperationCore
|
11
|
+
# Handles operation fallback based on configured mode
|
12
|
+
#
|
13
|
+
# @param operation_type [Symbol] Either :transaction or :pipeline
|
14
|
+
# @param dbclient_proc [Proc] Lambda that returns the Redis connection
|
15
|
+
# @param handler_class [Class] The connection handler that blocked operation
|
16
|
+
# @param block [Proc] Block containing Redis commands to execute
|
17
|
+
# @return [MultiResult] Result from individual command execution or raises error
|
18
|
+
#
|
19
|
+
def self.handle_fallback(operation_type, dbclient_proc, handler_class, &block)
|
20
|
+
mode = get_operation_mode(operation_type)
|
21
|
+
|
22
|
+
case mode
|
23
|
+
when :strict
|
24
|
+
raise Familia::OperationModeError,
|
25
|
+
"Cannot start #{operation_type} with #{handler_class.name} connection. Use connection pools."
|
26
|
+
when :warn
|
27
|
+
log_fallback_warning(operation_type, handler_class)
|
28
|
+
execute_individual_commands(dbclient_proc, &block)
|
29
|
+
when :permissive
|
30
|
+
execute_individual_commands(dbclient_proc, &block)
|
31
|
+
else
|
32
|
+
# Default to strict mode if invalid setting
|
33
|
+
raise Familia::OperationModeError,
|
34
|
+
"Cannot start #{operation_type} with #{handler_class.name} connection. Use connection pools."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Gets the configured mode for the operation type
|
39
|
+
#
|
40
|
+
# @param operation_type [Symbol] Either :transaction or :pipeline
|
41
|
+
# @return [Symbol] The configured mode (:strict, :warn, :permissive)
|
42
|
+
#
|
43
|
+
def self.get_operation_mode(operation_type)
|
44
|
+
case operation_type
|
45
|
+
when :transaction
|
46
|
+
Familia.transaction_mode
|
47
|
+
when :pipeline
|
48
|
+
Familia.pipeline_mode
|
49
|
+
else
|
50
|
+
:strict
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Logs fallback warning message
|
55
|
+
#
|
56
|
+
# @param operation_type [Symbol] Either :transaction or :pipeline
|
57
|
+
# @param handler_class [Class] The connection handler class
|
58
|
+
#
|
59
|
+
def self.log_fallback_warning(operation_type, handler_class)
|
60
|
+
message = "#{operation_type.capitalize} unavailable with #{handler_class.name}. Using individual commands."
|
61
|
+
|
62
|
+
if Familia.respond_to?(:logger) && Familia.logger
|
63
|
+
Familia.logger.warn message
|
64
|
+
else
|
65
|
+
warn message
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Executes commands individually using a proxy that collects results
|
70
|
+
#
|
71
|
+
# Creates an IndividualCommandProxy that executes each Redis command immediately
|
72
|
+
# instead of queuing them in a transaction or pipeline. Results are collected to
|
73
|
+
# maintain the same interface as normal operations.
|
74
|
+
#
|
75
|
+
# @param dbclient_proc [Proc] Lambda that returns the Redis connection
|
76
|
+
# @param block [Proc] Block containing Redis commands to execute
|
77
|
+
# @return [MultiResult] Result object with collected command results
|
78
|
+
#
|
79
|
+
def self.execute_individual_commands(dbclient_proc, &block)
|
80
|
+
conn = dbclient_proc.call
|
81
|
+
proxy = IndividualCommandProxy.new(conn)
|
82
|
+
|
83
|
+
# Execute the block with the proxy
|
84
|
+
block.call(proxy)
|
85
|
+
|
86
|
+
# Return MultiResult format for consistency
|
87
|
+
results = proxy.collected_results
|
88
|
+
summary_boolean = results.all? { |ret| !ret.is_a?(Exception) }
|
89
|
+
MultiResult.new(summary_boolean, results)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|