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
@@ -1,7 +1,12 @@
1
1
  # lib/familia/connection.rb
2
2
 
3
- require_relative '../../lib/middleware/database_middleware'
4
- require_relative 'multi_result'
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
- @database_clients = {}
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
- attr_accessor :connection_provider
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
- # @return [Boolean] Whether to require external connections (no fallback)
34
- attr_accessor :connection_required
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 v [String, URI] The new default URI
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
- # Establishes a connection to a Database server.
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 `@database_clients` or `Familia.uri`.
54
- # @return [Redis] The connected Database client.
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
- # @example
57
- # Familia.connect('redis://localhost:6379')
58
- def connect(uri = nil)
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
- if Familia.enable_database_logging
62
- DatabaseLogger.logger = Familia.logger
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
- def reconnect(uri = nil)
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
- # First priority: Thread-local connection (middleware pattern)
95
- return Thread.current[:familia_connection] if Thread.current.key?(:familia_connection)
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
- # Executes Database commands in a pipeline for improved performance.
168
- #
169
- # Pipelines send multiple commands without waiting for individual responses,
170
- # reducing network round-trips. Commands execute independently and can
171
- # succeed or fail without affecting other commands in the pipeline.
172
- #
173
- # @yield [Redis] The Database pipeline connection
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
- class Familia::DataType
4
- # Must be included in all DataType classes to provide Redis
5
- # commands. The class must have a dbkey method.
6
- module Commands
7
- def move(logical_database)
8
- dbclient.move dbkey, logical_database
9
- end
10
-
11
- def rename(newkey)
12
- dbclient.rename dbkey, newkey
13
- end
14
-
15
- def renamenx(newkey)
16
- dbclient.renamenx dbkey, newkey
17
- end
18
-
19
- def type
20
- dbclient.type dbkey
21
- end
22
-
23
- # Deletes the entire dbkey
24
- # @return [Boolean] true if the key was deleted, false otherwise
25
- def delete!
26
- Familia.trace :DELETE!, dbclient, uri, caller(1..1) if Familia.debug?
27
- ret = dbclient.del dbkey
28
- ret.positive?
29
- end
30
- alias clear delete!
31
-
32
- def exists?
33
- dbclient.exists(dbkey) && !size.zero?
34
- end
35
-
36
- def current_expiration
37
- dbclient.ttl dbkey
38
- end
39
-
40
- def expire(sec)
41
- dbclient.expire dbkey, sec.to_i
42
- end
43
-
44
- def expireat(unixtime)
45
- dbclient.expireat dbkey, unixtime
46
- end
47
-
48
- def persist
49
- dbclient.persist dbkey
50
- end
51
-
52
- def echo(meth, trace)
53
- dbclient.echo "[#{self.class}##{meth}] #{trace} (#{@opts[:class]}#)"
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
- class Familia::DataType
4
- module Serialization
5
- # Serializes a value for storage in Redis.
6
- #
7
- # @param val [Object] The value to be serialized.
8
- # @param strict_values [Boolean] Whether to enforce strict value
9
- # serialization (default: true).
10
- # @return [String, nil] The serialized representation of the value, or nil
11
- # if serialization fails.
12
- #
13
- # @note When a class option is specified, it uses that class's
14
- # serialization method. Otherwise, it relies on Familia.distinguisher for
15
- # serialization.
16
- #
17
- # @example With a class option
18
- # serialize_value(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
19
- #
20
- # @example Without a class option
21
- # serialize_value(123) #=> "123"
22
- # serialize_value("hello") #=> "hello"
23
- #
24
- # @raise [Familia::HighRiskFactor] If serialization fails under strict
25
- # mode.
26
- #
27
- def serialize_value(val, strict_values: true)
28
- prepared = nil
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
- Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
31
+ Familia.trace :TOREDIS, nil, "#{val}<#{val.class}|#{opts[:class]}>" if Familia.debug?
31
32
 
32
- if opts[:class]
33
- prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
34
- Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
35
- end
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
- if prepared.nil?
38
- # Enforce strict values when no class option is specified
39
- prepared = Familia.distinguisher(val, strict_values: true)
40
- Familia.ld " from <#{val.class}> => <#{prepared.class}>"
41
- end
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
- if Familia.debug?
44
- Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>",
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
- Familia.warn "[#{self.class}#serialize_value] nil returned for #{opts[:class]}##{name}" if prepared.nil?
49
- prepared
50
- end
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
- # Deserializes multiple values from 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
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
- # Deserializes multiple values from 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]
84
+ unless @opts[:class].respond_to?(load_method)
85
+ raise Familia::Problem, "No such method: #{@opts[:class]}##{load_method}"
86
+ end
83
87
 
84
- unless @opts[:class].respond_to?(load_method)
85
- raise Familia::Problem, "No such method: #{@opts[:class]}##{load_method}"
86
- end
88
+ values.collect! do |obj|
89
+ next if obj.nil?
87
90
 
88
- values.collect! do |obj|
89
- next if obj.nil?
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
- val = @opts[:class].send load_method, obj
92
- Familia.ld "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
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
- 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
102
+ values
100
103
  end
101
104
 
102
- values
103
- end
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
- # 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]
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
@@ -1,7 +1,7 @@
1
1
  # lib/familia/data_type/types/counter.rb
2
2
 
3
3
  module Familia
4
- class Counter < String
4
+ class Counter < StringKey
5
5
  def initialize(*args)
6
6
  super
7
7
  @opts[:default] ||= 0