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.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/code-quality.yml +138 -0
  3. data/.github/workflows/code-smellage.yml +145 -0
  4. data/.github/workflows/docs.yml +31 -8
  5. data/.gitignore +1 -1
  6. data/.pre-commit-config.yaml +7 -1
  7. data/.reek.yml +98 -0
  8. data/.rubocop.yml +48 -10
  9. data/.talismanrc +9 -0
  10. data/.yardopts +18 -13
  11. data/CHANGELOG.rst +64 -4
  12. data/CLAUDE.md +1 -1
  13. data/Gemfile +6 -5
  14. data/Gemfile.lock +99 -23
  15. data/LICENSE.txt +1 -1
  16. data/README.md +285 -85
  17. data/changelog.d/README.md +2 -2
  18. data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
  19. data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
  20. data/docs/archive/FAMILIA_UPDATE.md +3 -3
  21. data/docs/archive/README.md +3 -2
  22. data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
  23. data/docs/conf.py +29 -0
  24. data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
  25. data/docs/guides/feature-encrypted-fields.md +785 -0
  26. data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
  27. data/docs/guides/feature-external-identifiers.md +637 -0
  28. data/docs/guides/feature-object-identifiers.md +435 -0
  29. data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
  30. data/docs/guides/feature-relationships-methods.md +684 -0
  31. data/docs/guides/feature-relationships.md +200 -0
  32. data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
  33. data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
  34. data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
  35. data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
  36. data/docs/guides/index.md +176 -0
  37. data/docs/guides/{Security-Model.md → security-model.md} +1 -1
  38. data/docs/migrating/v2.0.0-pre.md +1 -1
  39. data/docs/migrating/v2.0.0-pre11.md +2 -2
  40. data/docs/migrating/v2.0.0-pre12.md +2 -2
  41. data/docs/migrating/v2.0.0-pre5.md +33 -12
  42. data/docs/migrating/v2.0.0-pre6.md +2 -2
  43. data/docs/migrating/v2.0.0-pre7.md +8 -8
  44. data/docs/overview.md +623 -19
  45. data/docs/reference/api-technical.md +1365 -0
  46. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
  47. data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
  48. data/examples/autoloader/mega_customer.rb +3 -1
  49. data/examples/encrypted_fields.rb +378 -0
  50. data/examples/json_usage_patterns.rb +144 -0
  51. data/examples/relationships.rb +13 -13
  52. data/examples/safe_dump.rb +6 -6
  53. data/examples/single_connection_transaction_confusions.rb +379 -0
  54. data/lib/familia/base.rb +49 -10
  55. data/lib/familia/connection/handlers.rb +223 -0
  56. data/lib/familia/connection/individual_command_proxy.rb +64 -0
  57. data/lib/familia/connection/middleware.rb +75 -0
  58. data/lib/familia/connection/operation_core.rb +93 -0
  59. data/lib/familia/connection/operations.rb +277 -0
  60. data/lib/familia/connection/pipeline_core.rb +87 -0
  61. data/lib/familia/connection/transaction_core.rb +100 -0
  62. data/lib/familia/connection.rb +60 -186
  63. data/lib/familia/data_type/commands.rb +53 -51
  64. data/lib/familia/data_type/serialization.rb +108 -107
  65. data/lib/familia/data_type/types/counter.rb +1 -1
  66. data/lib/familia/data_type/types/hashkey.rb +13 -10
  67. data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
  68. data/lib/familia/data_type/types/lock.rb +3 -2
  69. data/lib/familia/data_type/types/sorted_set.rb +26 -15
  70. data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
  71. data/lib/familia/data_type/types/unsorted_set.rb +20 -27
  72. data/lib/familia/data_type.rb +75 -47
  73. data/lib/familia/distinguisher.rb +85 -0
  74. data/lib/familia/encryption/encrypted_data.rb +15 -24
  75. data/lib/familia/encryption/manager.rb +6 -4
  76. data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
  77. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
  78. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
  79. data/lib/familia/encryption/request_cache.rb +7 -7
  80. data/lib/familia/encryption.rb +2 -3
  81. data/lib/familia/errors.rb +9 -3
  82. data/lib/familia/features/autoloader.rb +30 -12
  83. data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
  84. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
  85. data/lib/familia/features/encrypted_fields.rb +66 -64
  86. data/lib/familia/features/expiration/extensions.rb +1 -1
  87. data/lib/familia/features/expiration.rb +31 -26
  88. data/lib/familia/features/external_identifier.rb +9 -12
  89. data/lib/familia/features/object_identifier.rb +56 -19
  90. data/lib/familia/features/quantization.rb +16 -21
  91. data/lib/familia/features/relationships/README.md +97 -0
  92. data/lib/familia/features/relationships/collection_operations.rb +104 -0
  93. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
  94. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
  95. data/lib/familia/features/relationships/indexing.rb +176 -256
  96. data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
  97. data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
  98. data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
  99. data/lib/familia/features/relationships/participation.rb +656 -0
  100. data/lib/familia/features/relationships/participation_relationship.rb +31 -0
  101. data/lib/familia/features/relationships/score_encoding.rb +20 -20
  102. data/lib/familia/features/relationships.rb +65 -266
  103. data/lib/familia/features/safe_dump.rb +127 -130
  104. data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
  105. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
  106. data/lib/familia/features/transient_fields.rb +3 -5
  107. data/lib/familia/features.rb +4 -13
  108. data/lib/familia/field_type.rb +24 -4
  109. data/lib/familia/horreum/core/connection.rb +229 -26
  110. data/lib/familia/horreum/core/database_commands.rb +27 -17
  111. data/lib/familia/horreum/core/serialization.rb +40 -20
  112. data/lib/familia/horreum/core/utils.rb +2 -1
  113. data/lib/familia/horreum/shared/settings.rb +2 -1
  114. data/lib/familia/horreum/subclass/definition.rb +33 -45
  115. data/lib/familia/horreum/subclass/management.rb +72 -24
  116. data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
  117. data/lib/familia/horreum.rb +196 -114
  118. data/lib/familia/json_serializer.rb +0 -1
  119. data/lib/familia/logging.rb +11 -114
  120. data/lib/familia/refinements/dear_json.rb +122 -0
  121. data/lib/familia/refinements/logger_trace.rb +20 -17
  122. data/lib/familia/refinements/stylize_words.rb +65 -0
  123. data/lib/familia/refinements/time_literals.rb +60 -52
  124. data/lib/familia/refinements.rb +2 -1
  125. data/lib/familia/secure_identifier.rb +60 -28
  126. data/lib/familia/settings.rb +83 -7
  127. data/lib/familia/utils.rb +5 -87
  128. data/lib/familia/verifiable_identifier.rb +4 -4
  129. data/lib/familia/version.rb +1 -1
  130. data/lib/familia.rb +72 -14
  131. data/lib/middleware/database_middleware.rb +56 -14
  132. data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
  133. data/try/configuration/scenarios_try.rb +1 -1
  134. data/try/connection/fiber_context_preservation_try.rb +250 -0
  135. data/try/connection/handler_constraints_try.rb +59 -0
  136. data/try/connection/operation_mode_guards_try.rb +208 -0
  137. data/try/connection/pipeline_fallback_integration_try.rb +128 -0
  138. data/try/connection/responsibility_chain_tracking_try.rb +72 -0
  139. data/try/connection/transaction_fallback_integration_try.rb +288 -0
  140. data/try/connection/transaction_mode_permissive_try.rb +153 -0
  141. data/try/connection/transaction_mode_strict_try.rb +98 -0
  142. data/try/connection/transaction_mode_warn_try.rb +131 -0
  143. data/try/connection/transaction_modes_try.rb +249 -0
  144. data/try/core/autoloader_try.rb +120 -2
  145. data/try/core/connection_try.rb +7 -7
  146. data/try/core/conventional_inheritance_try.rb +130 -0
  147. data/try/core/create_method_try.rb +15 -23
  148. data/try/core/database_consistency_try.rb +10 -10
  149. data/try/core/errors_try.rb +8 -11
  150. data/try/core/familia_extended_try.rb +2 -2
  151. data/try/core/familia_members_methods_try.rb +76 -0
  152. data/try/core/isolated_dbclient_try.rb +165 -0
  153. data/try/core/middleware_try.rb +16 -16
  154. data/try/core/persistence_operations_try.rb +4 -4
  155. data/try/core/pools_try.rb +42 -26
  156. data/try/core/secure_identifier_try.rb +28 -24
  157. data/try/core/time_utils_try.rb +10 -10
  158. data/try/core/tools_try.rb +1 -1
  159. data/try/core/utils_try.rb +2 -2
  160. data/try/data_types/boolean_try.rb +4 -4
  161. data/try/data_types/datatype_base_try.rb +0 -2
  162. data/try/data_types/list_try.rb +10 -10
  163. data/try/data_types/sorted_set_try.rb +5 -5
  164. data/try/data_types/string_try.rb +12 -12
  165. data/try/data_types/unsortedset_try.rb +33 -0
  166. data/try/debugging/cache_behavior_tracer.rb +7 -7
  167. data/try/debugging/debug_aad_process.rb +1 -1
  168. data/try/debugging/debug_concealed_internal.rb +1 -1
  169. data/try/debugging/debug_cross_context.rb +1 -1
  170. data/try/debugging/debug_fresh_cross_context.rb +1 -1
  171. data/try/debugging/encryption_method_tracer.rb +10 -10
  172. data/try/edge_cases/hash_symbolization_try.rb +1 -1
  173. data/try/edge_cases/ttl_side_effects_try.rb +1 -1
  174. data/try/encryption/config_persistence_try.rb +2 -2
  175. data/try/encryption/encryption_core_try.rb +19 -19
  176. data/try/encryption/instance_variable_scope_try.rb +1 -1
  177. data/try/encryption/module_loading_try.rb +2 -2
  178. data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
  179. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
  180. data/try/encryption/secure_memory_handling_try.rb +1 -1
  181. data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
  182. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
  183. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
  184. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
  185. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
  186. data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
  187. data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
  188. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
  189. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
  190. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
  191. data/try/features/external_identifier/external_identifier_try.rb +1 -1
  192. data/try/features/feature_dependencies_try.rb +3 -3
  193. data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
  194. data/try/features/object_identifier/object_identifier_try.rb +10 -0
  195. data/try/features/quantization/quantization_try.rb +1 -1
  196. data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
  197. data/try/features/relationships/indexing_try.rb +433 -0
  198. data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
  199. data/try/features/relationships/participation_commands_verification_try.rb +105 -0
  200. data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
  201. data/try/features/relationships/participation_reverse_index_try.rb +196 -0
  202. data/try/features/relationships/relationships_api_changes_try.rb +72 -71
  203. data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
  204. data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
  205. data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
  206. data/try/features/relationships/relationships_performance_try.rb +20 -20
  207. data/try/features/relationships/relationships_try.rb +27 -38
  208. data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
  209. data/try/features/transient_fields/refresh_reset_try.rb +1 -1
  210. data/try/features/transient_fields/simple_refresh_test.rb +1 -1
  211. data/try/helpers/test_cleanup.rb +86 -0
  212. data/try/helpers/test_helpers.rb +3 -3
  213. data/try/horreum/base_try.rb +3 -2
  214. data/try/horreum/commands_try.rb +1 -1
  215. data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
  216. data/try/horreum/initialization_try.rb +11 -7
  217. data/try/horreum/relations_try.rb +21 -13
  218. data/try/horreum/serialization_try.rb +12 -11
  219. data/try/integration/cross_component_try.rb +3 -3
  220. data/try/memory/memory_basic_test.rb +1 -1
  221. data/try/memory/memory_docker_ruby_dump.sh +1 -1
  222. data/try/models/customer_safe_dump_try.rb +1 -1
  223. data/try/models/customer_try.rb +8 -10
  224. data/try/models/datatype_base_try.rb +3 -3
  225. data/try/models/familia_object_try.rb +9 -8
  226. data/try/performance/benchmarks_try.rb +2 -2
  227. data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
  228. data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
  229. data/try/prototypes/atomic_saves_v4.rb +1 -1
  230. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
  231. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  232. data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
  233. data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
  234. data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
  235. data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
  236. data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
  237. data/try/prototypes/pooling/pool_siege.rb +11 -11
  238. data/try/prototypes/pooling/run_stress_tests.rb +7 -7
  239. data/try/refinements/dear_json_array_methods_try.rb +53 -0
  240. data/try/refinements/dear_json_hash_methods_try.rb +54 -0
  241. data/try/refinements/logger_trace_methods_try.rb +44 -0
  242. data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
  243. data/try/refinements/time_literals_string_methods_try.rb +80 -0
  244. metadata +75 -43
  245. data/.rubocop_todo.yml +0 -208
  246. data/docs/connection_pooling.md +0 -192
  247. data/docs/guides/Connection-Pooling-Guide.md +0 -437
  248. data/docs/guides/Encrypted-Fields-Overview.md +0 -101
  249. data/docs/guides/Feature-System-Autoloading.md +0 -198
  250. data/docs/guides/Home.md +0 -116
  251. data/docs/guides/Relationships-Guide.md +0 -737
  252. data/docs/guides/relationships-methods.md +0 -266
  253. data/docs/reference/auditing_database_commands.rb +0 -228
  254. data/examples/permissions.rb +0 -240
  255. data/lib/familia/features/relationships/cascading.rb +0 -437
  256. data/lib/familia/features/relationships/membership.rb +0 -497
  257. data/lib/familia/features/relationships/permission_management.rb +0 -264
  258. data/lib/familia/features/relationships/querying.rb +0 -615
  259. data/lib/familia/features/relationships/redis_operations.rb +0 -274
  260. data/lib/familia/features/relationships/tracking.rb +0 -418
  261. data/lib/familia/refinements/snake_case.rb +0 -40
  262. data/lib/familia/validation/command_recorder.rb +0 -336
  263. data/lib/familia/validation/expectations.rb +0 -519
  264. data/lib/familia/validation/validation_helpers.rb +0 -443
  265. data/lib/familia/validation/validator.rb +0 -412
  266. data/lib/familia/validation.rb +0 -140
  267. data/try/data_types/set_try.rb +0 -33
  268. data/try/features/relationships/categorical_permissions_try.rb +0 -515
  269. data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
  270. data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
  271. data/try/validation/atomic_operations_try.rb.disabled +0 -320
  272. data/try/validation/command_validation_try.rb.disabled +0 -207
  273. data/try/validation/performance_validation_try.rb.disabled +0 -324
  274. 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