familia 2.0.0.pre18 → 2.0.0.pre21

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 (370) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +4 -9
  3. data/.github/workflows/code-smells.yml +64 -3
  4. data/.pre-commit-config.yaml +8 -6
  5. data/.reek.yml +10 -9
  6. data/.rubocop.yml +4 -0
  7. data/CHANGELOG.rst +205 -88
  8. data/CLAUDE.md +62 -10
  9. data/Gemfile +3 -3
  10. data/Gemfile.lock +27 -62
  11. data/README.md +39 -0
  12. data/bin/try +16 -0
  13. data/bin/tryouts +16 -0
  14. data/changelog.d/20251105_flexible_external_identifier_format.rst +66 -0
  15. data/changelog.d/20251107_112554_delano_179_participation_asymmetry.rst +44 -0
  16. data/changelog.d/20251107_213121_delano_fix_thread_safety_races_011CUumCP492Twxm4NLt2FvL.rst +20 -0
  17. data/changelog.d/20251107_fix_participates_in_symbol_resolution.rst +91 -0
  18. data/changelog.d/20251107_optimized_redis_exists_checks.rst +94 -0
  19. data/changelog.d/20251108_frozen_string_literal_pragma.rst +44 -0
  20. data/docs/1106-participates_in-bidirectional-solution.md +129 -0
  21. data/docs/guides/encryption.md +486 -0
  22. data/docs/guides/feature-encrypted-fields.md +123 -7
  23. data/docs/guides/feature-expiration.md +177 -133
  24. data/docs/guides/feature-external-identifiers.md +415 -443
  25. data/docs/guides/feature-object-identifiers.md +400 -269
  26. data/docs/guides/feature-quantization.md +120 -6
  27. data/docs/guides/feature-relationships-indexing.md +318 -0
  28. data/docs/guides/feature-relationships-methods.md +146 -604
  29. data/docs/guides/feature-relationships-participation.md +263 -0
  30. data/docs/guides/feature-relationships.md +118 -136
  31. data/docs/guides/feature-system-devs.md +176 -693
  32. data/docs/guides/feature-system.md +119 -6
  33. data/docs/guides/feature-transient-fields.md +81 -0
  34. data/docs/guides/field-system.md +778 -0
  35. data/docs/guides/index.md +32 -15
  36. data/docs/guides/logging.md +187 -0
  37. data/docs/guides/optimized-loading.md +674 -0
  38. data/docs/guides/thread-safety-monitoring.md +61 -0
  39. data/docs/guides/{time-utilities.md → time-literals.md} +12 -12
  40. data/docs/migrating/v2.0.0-pre19.md +197 -0
  41. data/docs/migrating/v2.0.0-pre22.md +241 -0
  42. data/docs/overview.md +7 -9
  43. data/docs/reference/api-technical.md +267 -320
  44. data/examples/autoloader/mega_customer/features/deprecated_fields.rb +2 -0
  45. data/examples/autoloader/mega_customer/safe_dump_fields.rb +2 -0
  46. data/examples/autoloader/mega_customer.rb +2 -0
  47. data/examples/datatype_standalone.rb +282 -0
  48. data/examples/encrypted_fields.rb +2 -1
  49. data/examples/json_usage_patterns.rb +2 -0
  50. data/examples/relationships.rb +3 -0
  51. data/examples/safe_dump.rb +2 -1
  52. data/examples/sampling_demo.rb +53 -0
  53. data/examples/single_connection_transaction_confusions.rb +2 -1
  54. data/familia.gemspec +2 -1
  55. data/lib/familia/base.rb +2 -0
  56. data/lib/familia/connection/behavior.rb +254 -0
  57. data/lib/familia/connection/handlers.rb +97 -0
  58. data/lib/familia/connection/individual_command_proxy.rb +2 -0
  59. data/lib/familia/connection/middleware.rb +34 -24
  60. data/lib/familia/connection/operation_core.rb +3 -1
  61. data/lib/familia/connection/operations.rb +2 -0
  62. data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +4 -2
  63. data/lib/familia/connection/transaction_core.rb +75 -9
  64. data/lib/familia/connection.rb +21 -5
  65. data/lib/familia/data_type/class_methods.rb +3 -1
  66. data/lib/familia/data_type/connection.rb +153 -7
  67. data/lib/familia/data_type/database_commands.rb +9 -4
  68. data/lib/familia/data_type/serialization.rb +10 -4
  69. data/lib/familia/data_type/settings.rb +2 -0
  70. data/lib/familia/data_type/types/counter.rb +2 -0
  71. data/lib/familia/data_type/types/hashkey.rb +8 -6
  72. data/lib/familia/data_type/types/listkey.rb +2 -0
  73. data/lib/familia/data_type/types/lock.rb +2 -0
  74. data/lib/familia/data_type/types/sorted_set.rb +2 -0
  75. data/lib/familia/data_type/types/stringkey.rb +2 -0
  76. data/lib/familia/data_type/types/unsorted_set.rb +2 -0
  77. data/lib/familia/data_type.rb +2 -0
  78. data/lib/familia/encryption/encrypted_data.rb +4 -2
  79. data/lib/familia/encryption/manager.rb +2 -0
  80. data/lib/familia/encryption/provider.rb +2 -0
  81. data/lib/familia/encryption/providers/aes_gcm_provider.rb +2 -0
  82. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +2 -0
  83. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +2 -0
  84. data/lib/familia/encryption/registry.rb +2 -0
  85. data/lib/familia/encryption/request_cache.rb +2 -0
  86. data/lib/familia/encryption.rb +9 -2
  87. data/lib/familia/errors.rb +53 -14
  88. data/lib/familia/features/autoloader.rb +2 -0
  89. data/lib/familia/features/encrypted_fields/concealed_string.rb +2 -0
  90. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +4 -0
  91. data/lib/familia/features/encrypted_fields.rb +2 -2
  92. data/lib/familia/features/expiration/extensions.rb +11 -11
  93. data/lib/familia/features/expiration.rb +29 -21
  94. data/lib/familia/features/external_identifier.rb +33 -7
  95. data/lib/familia/features/object_identifier.rb +2 -0
  96. data/lib/familia/features/quantization.rb +3 -1
  97. data/lib/familia/features/relationships/README.md +3 -1
  98. data/lib/familia/features/relationships/collection_operations.rb +2 -0
  99. data/lib/familia/features/relationships/indexing/multi_index_generators.rb +177 -47
  100. data/lib/familia/features/relationships/indexing/rebuild_strategies.rb +479 -0
  101. data/lib/familia/features/relationships/indexing/unique_index_generators.rb +203 -63
  102. data/lib/familia/features/relationships/indexing.rb +40 -42
  103. data/lib/familia/features/relationships/indexing_relationship.rb +17 -5
  104. data/lib/familia/features/relationships/participation/participant_methods.rb +131 -14
  105. data/lib/familia/features/relationships/participation/rebuild_strategies.md +41 -0
  106. data/lib/familia/features/relationships/participation/target_methods.rb +6 -6
  107. data/lib/familia/features/relationships/participation.rb +155 -69
  108. data/lib/familia/features/relationships/participation_membership.rb +69 -0
  109. data/lib/familia/features/relationships/participation_relationship.rb +34 -6
  110. data/lib/familia/features/relationships/score_encoding.rb +2 -0
  111. data/lib/familia/features/relationships.rb +5 -3
  112. data/lib/familia/features/safe_dump.rb +2 -0
  113. data/lib/familia/features/transient_fields/redacted_string.rb +2 -0
  114. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +2 -0
  115. data/lib/familia/features/transient_fields/transient_field_type.rb +5 -3
  116. data/lib/familia/features/transient_fields.rb +2 -0
  117. data/lib/familia/features.rb +2 -0
  118. data/lib/familia/field_type.rb +5 -2
  119. data/lib/familia/horreum/connection.rb +28 -36
  120. data/lib/familia/horreum/database_commands.rb +131 -10
  121. data/lib/familia/horreum/definition.rb +18 -7
  122. data/lib/familia/horreum/management.rb +233 -57
  123. data/lib/familia/horreum/persistence.rb +314 -122
  124. data/lib/familia/horreum/related_fields.rb +2 -0
  125. data/lib/familia/horreum/serialization.rb +26 -4
  126. data/lib/familia/horreum/settings.rb +2 -0
  127. data/lib/familia/horreum/utils.rb +2 -8
  128. data/lib/familia/horreum.rb +46 -13
  129. data/lib/familia/identifier_extractor.rb +2 -0
  130. data/lib/familia/instrumentation.rb +156 -0
  131. data/lib/familia/json_serializer.rb +2 -0
  132. data/lib/familia/logging.rb +94 -37
  133. data/lib/familia/refinements/dear_json.rb +2 -0
  134. data/lib/familia/refinements/stylize_words.rb +2 -14
  135. data/lib/familia/refinements/time_literals.rb +2 -0
  136. data/lib/familia/refinements.rb +2 -0
  137. data/lib/familia/secure_identifier.rb +10 -2
  138. data/lib/familia/settings.rb +9 -7
  139. data/lib/familia/thread_safety/instrumented_mutex.rb +166 -0
  140. data/lib/familia/thread_safety/monitor.rb +328 -0
  141. data/lib/familia/utils.rb +13 -0
  142. data/lib/familia/verifiable_identifier.rb +3 -1
  143. data/lib/familia/version.rb +3 -1
  144. data/lib/familia.rb +31 -4
  145. data/lib/middleware/database_command_counter.rb +152 -0
  146. data/lib/middleware/database_logger.rb +325 -129
  147. data/lib/multi_result.rb +2 -0
  148. data/try/edge_cases/empty_identifiers_try.rb +2 -0
  149. data/try/edge_cases/hash_symbolization_try.rb +2 -0
  150. data/try/edge_cases/json_serialization_try.rb +2 -0
  151. data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +4 -0
  152. data/try/edge_cases/race_conditions_try.rb +4 -0
  153. data/try/edge_cases/reserved_keywords_try.rb +4 -0
  154. data/try/edge_cases/string_coercion_try.rb +6 -4
  155. data/try/edge_cases/ttl_side_effects_try.rb +4 -0
  156. data/try/features/encrypted_fields/aad_protection_try.rb +4 -0
  157. data/try/features/encrypted_fields/concealed_string_core_try.rb +4 -0
  158. data/try/features/encrypted_fields/context_isolation_try.rb +4 -0
  159. data/try/features/encrypted_fields/encrypted_fields_core_try.rb +33 -0
  160. data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +4 -0
  161. data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +4 -0
  162. data/try/features/encrypted_fields/encrypted_fields_security_try.rb +4 -0
  163. data/try/features/encrypted_fields/error_conditions_try.rb +4 -0
  164. data/try/features/encrypted_fields/fresh_key_derivation_try.rb +4 -0
  165. data/try/features/encrypted_fields/fresh_key_try.rb +4 -0
  166. data/try/features/encrypted_fields/key_rotation_try.rb +4 -0
  167. data/try/features/encrypted_fields/memory_security_try.rb +4 -0
  168. data/try/features/encrypted_fields/missing_current_key_version_try.rb +4 -0
  169. data/try/features/encrypted_fields/nonce_uniqueness_try.rb +4 -0
  170. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +4 -0
  171. data/try/features/encrypted_fields/thread_safety_try.rb +4 -0
  172. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +4 -0
  173. data/try/features/encryption/config_persistence_try.rb +4 -0
  174. data/try/features/encryption/core_try.rb +4 -0
  175. data/try/features/encryption/instance_variable_scope_try.rb +4 -0
  176. data/try/features/encryption/module_loading_try.rb +4 -0
  177. data/try/features/encryption/providers/aes_gcm_provider_try.rb +4 -0
  178. data/try/features/encryption/providers/xchacha20_poly1305_provider_try.rb +4 -0
  179. data/try/features/encryption/roundtrip_validation_try.rb +4 -0
  180. data/try/features/encryption/secure_memory_handling_try.rb +4 -0
  181. data/try/features/expiration/expiration_try.rb +5 -1
  182. data/try/features/external_identifier/external_identifier_try.rb +171 -8
  183. data/try/features/feature_dependencies_try.rb +2 -0
  184. data/try/features/feature_improvements_try.rb +2 -0
  185. data/try/features/field_groups_try.rb +2 -0
  186. data/try/features/object_identifier/object_identifier_integration_try.rb +12 -9
  187. data/try/features/object_identifier/object_identifier_try.rb +2 -0
  188. data/try/features/quantization/quantization_try.rb +4 -0
  189. data/try/features/real_feature_integration_try.rb +2 -0
  190. data/try/features/relationships/indexing_commands_verification_try.rb +2 -0
  191. data/try/features/relationships/indexing_rebuild_try.rb +600 -0
  192. data/try/features/relationships/indexing_try.rb +30 -4
  193. data/try/features/relationships/participation_bidirectional_try.rb +242 -0
  194. data/try/features/relationships/participation_commands_verification_spec.rb +4 -0
  195. data/try/features/relationships/participation_commands_verification_try.rb +2 -0
  196. data/try/features/relationships/participation_performance_improvements_try.rb +11 -9
  197. data/try/features/relationships/participation_reverse_index_try.rb +15 -13
  198. data/try/features/relationships/participation_target_class_resolution_try.rb +209 -0
  199. data/try/features/relationships/participation_unresolved_target_try.rb +109 -0
  200. data/try/features/relationships/relationships_api_changes_try.rb +6 -4
  201. data/try/features/relationships/relationships_edge_cases_try.rb +4 -0
  202. data/try/features/relationships/relationships_performance_minimal_try.rb +4 -0
  203. data/try/features/relationships/relationships_performance_simple_try.rb +4 -0
  204. data/try/features/relationships/relationships_performance_try.rb +4 -0
  205. data/try/features/relationships/relationships_performance_working_try.rb +4 -0
  206. data/try/features/relationships/relationships_try.rb +6 -4
  207. data/try/features/safe_dump/safe_dump_advanced_try.rb +4 -0
  208. data/try/features/safe_dump/safe_dump_try.rb +4 -0
  209. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  210. data/try/features/transient_fields/refresh_reset_try.rb +3 -0
  211. data/try/features/transient_fields/simple_refresh_test.rb +3 -0
  212. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  213. data/try/features/transient_fields/transient_fields_core_try.rb +4 -0
  214. data/try/features/transient_fields/transient_fields_integration_try.rb +4 -0
  215. data/try/integration/connection/fiber_context_preservation_try.rb +7 -3
  216. data/try/integration/connection/handler_constraints_try.rb +4 -0
  217. data/try/integration/connection/isolated_dbclient_try.rb +4 -0
  218. data/try/integration/connection/middleware_reconnect_try.rb +2 -0
  219. data/try/integration/connection/operation_mode_guards_try.rb +5 -1
  220. data/try/integration/connection/pipeline_fallback_integration_try.rb +15 -12
  221. data/try/integration/connection/pools_try.rb +4 -0
  222. data/try/integration/connection/responsibility_chain_tracking_try.rb +4 -0
  223. data/try/integration/connection/transaction_fallback_integration_try.rb +4 -0
  224. data/try/integration/connection/transaction_mode_permissive_try.rb +4 -0
  225. data/try/integration/connection/transaction_mode_strict_try.rb +4 -0
  226. data/try/integration/connection/transaction_mode_warn_try.rb +4 -0
  227. data/try/integration/connection/transaction_modes_try.rb +4 -0
  228. data/try/integration/conventional_inheritance_try.rb +4 -0
  229. data/try/integration/create_method_try.rb +26 -22
  230. data/try/integration/cross_component_try.rb +4 -0
  231. data/try/integration/data_types/datatype_pipelines_try.rb +108 -0
  232. data/try/integration/data_types/datatype_transactions_try.rb +251 -0
  233. data/try/integration/database_consistency_try.rb +4 -0
  234. data/try/integration/familia_extended_try.rb +4 -0
  235. data/try/integration/familia_members_methods_try.rb +4 -0
  236. data/try/integration/models/customer_safe_dump_try.rb +9 -1
  237. data/try/integration/models/customer_try.rb +4 -0
  238. data/try/integration/models/datatype_base_try.rb +4 -0
  239. data/try/integration/models/familia_object_try.rb +5 -1
  240. data/try/integration/persistence_operations_try.rb +166 -10
  241. data/try/integration/relationships_persistence_round_trip_try.rb +17 -14
  242. data/try/integration/save_methods_consistency_try.rb +241 -0
  243. data/try/integration/scenarios_try.rb +4 -0
  244. data/try/integration/secure_identifier_try.rb +4 -0
  245. data/try/integration/transaction_safety_core_try.rb +176 -0
  246. data/try/integration/transaction_safety_workflow_try.rb +291 -0
  247. data/try/integration/verifiable_identifier_try.rb +4 -0
  248. data/try/investigation/pipeline_routing/README.md +228 -0
  249. data/try/performance/benchmarks_try.rb +4 -0
  250. data/try/performance/transaction_safety_benchmark_try.rb +238 -0
  251. data/try/support/benchmarks/deserialization_benchmark.rb +3 -1
  252. data/try/support/benchmarks/deserialization_correctness_test.rb +3 -1
  253. data/try/support/debugging/cache_behavior_tracer.rb +4 -0
  254. data/try/support/debugging/debug_aad_process.rb +3 -0
  255. data/try/support/debugging/debug_concealed_internal.rb +3 -0
  256. data/try/support/debugging/debug_concealed_reveal.rb +3 -0
  257. data/try/support/debugging/debug_context_aad.rb +3 -0
  258. data/try/support/debugging/debug_context_simple.rb +3 -0
  259. data/try/support/debugging/debug_cross_context.rb +3 -0
  260. data/try/support/debugging/debug_database_load.rb +3 -0
  261. data/try/support/debugging/debug_encrypted_json_check.rb +3 -0
  262. data/try/support/debugging/debug_encrypted_json_step_by_step.rb +3 -0
  263. data/try/support/debugging/debug_exists_lifecycle.rb +3 -0
  264. data/try/support/debugging/debug_field_decrypt.rb +3 -0
  265. data/try/support/debugging/debug_fresh_cross_context.rb +3 -0
  266. data/try/support/debugging/debug_load_path.rb +3 -0
  267. data/try/support/debugging/debug_method_definition.rb +3 -0
  268. data/try/support/debugging/debug_method_resolution.rb +3 -0
  269. data/try/support/debugging/debug_minimal.rb +3 -0
  270. data/try/support/debugging/debug_provider.rb +3 -0
  271. data/try/support/debugging/debug_secure_behavior.rb +3 -0
  272. data/try/support/debugging/debug_string_class.rb +3 -0
  273. data/try/support/debugging/debug_test.rb +3 -0
  274. data/try/support/debugging/debug_test_design.rb +3 -0
  275. data/try/support/debugging/encryption_method_tracer.rb +4 -0
  276. data/try/support/debugging/provider_diagnostics.rb +4 -0
  277. data/try/support/helpers/test_cleanup.rb +4 -0
  278. data/try/support/helpers/test_helpers.rb +5 -0
  279. data/try/support/memory/memory_basic_test.rb +4 -0
  280. data/try/support/memory/memory_detailed_test.rb +4 -0
  281. data/try/support/memory/memory_search_for_string.rb +4 -0
  282. data/try/support/memory/test_actual_redactedstring_protection.rb +4 -0
  283. data/try/support/prototypes/atomic_saves_v1_context_proxy.rb +4 -0
  284. data/try/support/prototypes/atomic_saves_v2_connection_switching.rb +4 -0
  285. data/try/support/prototypes/atomic_saves_v3_connection_pool.rb +4 -0
  286. data/try/support/prototypes/atomic_saves_v4.rb +4 -0
  287. data/try/support/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -0
  288. data/try/support/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  289. data/try/support/prototypes/pooling/configurable_stress_test.rb +4 -0
  290. data/try/support/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -0
  291. data/try/support/prototypes/pooling/lib/connection_pool_metrics.rb +4 -0
  292. data/try/support/prototypes/pooling/lib/connection_pool_stress_test.rb +4 -0
  293. data/try/support/prototypes/pooling/lib/connection_pool_threading_models.rb +4 -0
  294. data/try/support/prototypes/pooling/lib/visualize_stress_results.rb +4 -2
  295. data/try/support/prototypes/pooling/pool_siege.rb +4 -2
  296. data/try/support/prototypes/pooling/run_stress_tests.rb +4 -2
  297. data/try/thread_safety/README.md +496 -0
  298. data/try/thread_safety/class_connection_chain_race_try.rb +265 -0
  299. data/try/thread_safety/connection_chain_race_try.rb +148 -0
  300. data/try/thread_safety/encryption_manager_cache_race_try.rb +166 -0
  301. data/try/thread_safety/feature_registry_race_try.rb +226 -0
  302. data/try/thread_safety/fiber_pipeline_isolation_try.rb +235 -0
  303. data/try/thread_safety/fiber_transaction_isolation_try.rb +208 -0
  304. data/try/thread_safety/field_registration_race_try.rb +222 -0
  305. data/try/thread_safety/logger_initialization_race_try.rb +170 -0
  306. data/try/thread_safety/middleware_registration_race_try.rb +154 -0
  307. data/try/thread_safety/module_config_race_try.rb +175 -0
  308. data/try/thread_safety/secure_identifier_cache_race_try.rb +226 -0
  309. data/try/unit/core/autoloader_try.rb +4 -0
  310. data/try/unit/core/base_enhancements_try.rb +4 -0
  311. data/try/unit/core/connection_try.rb +4 -0
  312. data/try/unit/core/errors_try.rb +4 -0
  313. data/try/unit/core/extensions_try.rb +4 -0
  314. data/try/unit/core/familia_logger_try.rb +2 -0
  315. data/try/unit/core/familia_try.rb +4 -0
  316. data/try/unit/core/middleware_sampling_try.rb +335 -0
  317. data/try/unit/core/middleware_test_helpers_bug_try.rb +58 -0
  318. data/try/unit/core/middleware_thread_safety_try.rb +245 -0
  319. data/try/unit/core/middleware_try.rb +4 -0
  320. data/try/unit/core/settings_try.rb +4 -0
  321. data/try/unit/core/time_utils_try.rb +4 -0
  322. data/try/unit/core/tools_try.rb +4 -0
  323. data/try/unit/core/utils_try.rb +37 -0
  324. data/try/unit/data_types/boolean_try.rb +5 -1
  325. data/try/unit/data_types/counter_try.rb +4 -0
  326. data/try/unit/data_types/datatype_base_try.rb +4 -0
  327. data/try/unit/data_types/hash_try.rb +4 -0
  328. data/try/unit/data_types/list_try.rb +4 -0
  329. data/try/unit/data_types/lock_try.rb +4 -0
  330. data/try/unit/data_types/sorted_set_try.rb +4 -0
  331. data/try/unit/data_types/sorted_set_zadd_options_try.rb +4 -0
  332. data/try/unit/data_types/string_try.rb +5 -1
  333. data/try/unit/data_types/unsortedset_try.rb +4 -0
  334. data/try/unit/familia_resolve_class_try.rb +116 -0
  335. data/try/unit/horreum/auto_indexing_on_save_try.rb +36 -16
  336. data/try/unit/horreum/automatic_index_validation_try.rb +255 -0
  337. data/try/unit/horreum/base_try.rb +5 -1
  338. data/try/unit/horreum/class_methods_try.rb +6 -2
  339. data/try/unit/horreum/commands_try.rb +4 -0
  340. data/try/unit/horreum/defensive_initialization_try.rb +4 -0
  341. data/try/unit/horreum/destroy_related_fields_cleanup_try.rb +4 -0
  342. data/try/unit/horreum/enhanced_conflict_handling_try.rb +4 -0
  343. data/try/unit/horreum/field_categories_try.rb +4 -0
  344. data/try/unit/horreum/field_definition_try.rb +4 -0
  345. data/try/unit/horreum/initialization_try.rb +5 -1
  346. data/try/unit/horreum/json_type_preservation_try.rb +2 -0
  347. data/try/unit/horreum/optimized_loading_try.rb +156 -0
  348. data/try/unit/horreum/relations_try.rb +8 -4
  349. data/try/unit/horreum/serialization_persistent_fields_try.rb +4 -0
  350. data/try/unit/horreum/serialization_try.rb +6 -2
  351. data/try/unit/horreum/settings_try.rb +4 -0
  352. data/try/unit/horreum/unique_index_edge_cases_try.rb +380 -0
  353. data/try/unit/horreum/unique_index_guard_validation_try.rb +283 -0
  354. data/try/unit/middleware/database_command_counter_methods_try.rb +139 -0
  355. data/try/unit/middleware/database_logger_methods_try.rb +251 -0
  356. data/try/unit/refinements/dear_json_array_methods_try.rb +4 -0
  357. data/try/unit/refinements/dear_json_hash_methods_try.rb +4 -0
  358. data/try/unit/refinements/time_literals_numeric_methods_try.rb +4 -0
  359. data/try/unit/refinements/time_literals_string_methods_try.rb +4 -0
  360. data/try/unit/thread_safety_monitor_try.rb +149 -0
  361. metadata +81 -14
  362. data/.github/workflows/code-quality.yml +0 -138
  363. data/docs/archive/FAMILIA_RELATIONSHIPS.md +0 -210
  364. data/docs/archive/FAMILIA_TECHNICAL.md +0 -823
  365. data/docs/archive/FAMILIA_UPDATE.md +0 -226
  366. data/docs/archive/README.md +0 -64
  367. data/docs/archive/api-reference.md +0 -333
  368. data/docs/guides/core-field-system.md +0 -806
  369. data/docs/guides/implementation.md +0 -276
  370. data/docs/guides/security-model.md +0 -183
@@ -0,0 +1,139 @@
1
+ # try/unit/middleware/database_command_counter_methods_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Test DatabaseCommandCounter non-command methods and utilities
6
+ #
7
+ # This test file covers utility methods, configuration methods,
8
+ # and state management methods that don't involve command execution.
9
+ #
10
+ # Covers:
11
+ # - Counter methods (count, reset, increment, count_commands)
12
+ # - Configuration (skip_commands)
13
+ # - Utility methods (skip_command?)
14
+ # - Thread safety of atomic counter
15
+
16
+ require_relative '../../support/helpers/test_helpers'
17
+ require 'concurrent-ruby'
18
+
19
+ @original_count = DatabaseCommandCounter.count
20
+
21
+ # Clear initial state
22
+ DatabaseCommandCounter.reset
23
+
24
+ ## count returns the current command count
25
+ DatabaseCommandCounter.reset
26
+ DatabaseCommandCounter.count
27
+ #=> 0
28
+
29
+ ## reset sets count to zero
30
+ DatabaseCommandCounter.increment
31
+ DatabaseCommandCounter.increment
32
+ DatabaseCommandCounter.reset
33
+ DatabaseCommandCounter.count
34
+ #=> 0
35
+
36
+ ## reset returns zero
37
+ result = DatabaseCommandCounter.reset
38
+ result
39
+ #=> 0
40
+
41
+ ## increment increases the count by 1
42
+ DatabaseCommandCounter.reset
43
+ DatabaseCommandCounter.increment
44
+ DatabaseCommandCounter.count
45
+ #=> 1
46
+
47
+ ## increment returns the new count
48
+ DatabaseCommandCounter.reset
49
+ result = DatabaseCommandCounter.increment
50
+ result
51
+ #=> 1
52
+
53
+ ## increment can be called multiple times
54
+ DatabaseCommandCounter.reset
55
+ 3.times { DatabaseCommandCounter.increment }
56
+ DatabaseCommandCounter.count
57
+ #=> 3
58
+
59
+ ## skip_commands returns a Set with default skipped commands
60
+ DatabaseCommandCounter.skip_commands.class
61
+ #=> Set
62
+
63
+ ## skip_commands includes SELECT by default
64
+ DatabaseCommandCounter.skip_commands.include?("SELECT")
65
+ #=> true
66
+
67
+ ## skip_commands is frozen for immutability
68
+ DatabaseCommandCounter.skip_commands.frozen?
69
+ #=> true
70
+
71
+ ## skip_command? returns true for commands in skip_commands
72
+ DatabaseCommandCounter.skip_command?(["SELECT", "0"])
73
+ #=> true
74
+
75
+ ## skip_command? returns false for commands not in skip_commands
76
+ DatabaseCommandCounter.skip_command?(["SET", "key", "value"])
77
+ #=> false
78
+
79
+ ## skip_command? handles command array properly
80
+ DatabaseCommandCounter.skip_command?(["GET", "key"])
81
+ #=> false
82
+
83
+ ## skip_command? is case insensitive
84
+ DatabaseCommandCounter.skip_command?(["select", "0"])
85
+ #=> true
86
+
87
+ ## count_commands captures count difference in block
88
+ DatabaseCommandCounter.reset
89
+ initial_count = DatabaseCommandCounter.count
90
+ commands_executed = DatabaseCommandCounter.count_commands do
91
+ 5.times { DatabaseCommandCounter.increment }
92
+ end
93
+ commands_executed
94
+ #=> 5
95
+
96
+ ## count_commands works with empty block
97
+ DatabaseCommandCounter.reset
98
+ commands_executed = DatabaseCommandCounter.count_commands do
99
+ # No commands
100
+ end
101
+ commands_executed
102
+ #=> 0
103
+
104
+ ## count_commands captures count difference with existing count
105
+ DatabaseCommandCounter.reset
106
+ 5.times { DatabaseCommandCounter.increment } # Existing commands
107
+ commands_executed = DatabaseCommandCounter.count_commands do
108
+ 3.times { DatabaseCommandCounter.increment } # New commands in block
109
+ end
110
+ commands_executed
111
+ #=> 3
112
+
113
+ ## Thread safety: increment is thread-safe with concurrent access
114
+ DatabaseCommandCounter.reset
115
+ threads = 10.times.map do |i|
116
+ Thread.new do
117
+ 10.times { DatabaseCommandCounter.increment }
118
+ end
119
+ end
120
+
121
+ threads.each(&:join)
122
+ DatabaseCommandCounter.count
123
+ #=> 100
124
+
125
+ ## Thread safety: count_commands is thread-safe
126
+ DatabaseCommandCounter.reset
127
+ result = DatabaseCommandCounter.count_commands do
128
+ threads = 5.times.map do
129
+ Thread.new do
130
+ 10.times { DatabaseCommandCounter.increment }
131
+ end
132
+ end
133
+ threads.each(&:join)
134
+ end
135
+ result
136
+ #=> 50
137
+
138
+ # Restore original state (if needed)
139
+ DatabaseCommandCounter.reset
@@ -0,0 +1,251 @@
1
+ # try/unit/middleware/database_logger_methods_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # Test DatabaseLogger non-command methods and utilities
6
+ #
7
+ # This test file covers utility methods, configuration methods,
8
+ # and state management methods that don't involve command execution.
9
+ #
10
+ # Covers:
11
+ # - Configuration getters/setters (logger, max_commands, structured_logging, sample_rate)
12
+ # - Command storage methods (clear_commands, append_command, capture_commands)
13
+ # - Utility methods (index, now_in_μs, should_log?)
14
+ # - State introspection (commands, process_start)
15
+ # - Thread safety of command storage
16
+
17
+ require_relative '../../support/helpers/test_helpers'
18
+ require 'logger'
19
+ require 'stringio'
20
+ require 'concurrent-ruby'
21
+
22
+ @original_logger = DatabaseLogger.logger
23
+ @original_max_commands = DatabaseLogger.max_commands
24
+ @original_structured_logging = DatabaseLogger.structured_logging
25
+ @original_sample_rate = DatabaseLogger.sample_rate
26
+
27
+ # Clear initial state
28
+ DatabaseLogger.clear_commands
29
+ DatabaseLogger.logger = nil
30
+ DatabaseLogger.max_commands = 10_000
31
+ DatabaseLogger.structured_logging = false
32
+ DatabaseLogger.sample_rate = nil
33
+
34
+ ## logger getter returns the current logger instance
35
+ test_logger = Logger.new(StringIO.new)
36
+ DatabaseLogger.logger = test_logger
37
+ DatabaseLogger.logger == test_logger
38
+ #=> true
39
+
40
+ ## logger can be set to nil
41
+ DatabaseLogger.logger = nil
42
+ DatabaseLogger.logger
43
+ #=> nil
44
+
45
+ ## max_commands getter returns the current max commands value
46
+ DatabaseLogger.max_commands = 5_000
47
+ DatabaseLogger.max_commands
48
+ #=> 5000
49
+
50
+ ## max_commands can be set to different values
51
+ DatabaseLogger.max_commands = 1_000
52
+ DatabaseLogger.max_commands
53
+ #=> 1000
54
+
55
+ ## structured_logging getter returns the current structured logging mode
56
+ DatabaseLogger.structured_logging = true
57
+ DatabaseLogger.structured_logging
58
+ #=> true
59
+
60
+ ## structured_logging can be toggled
61
+ DatabaseLogger.structured_logging = false
62
+ DatabaseLogger.structured_logging
63
+ #=> false
64
+
65
+ ## sample_rate getter returns the current sample rate
66
+ DatabaseLogger.sample_rate = 0.5
67
+ DatabaseLogger.sample_rate
68
+ #=> 0.5
69
+
70
+ ## sample_rate can be set to nil
71
+ DatabaseLogger.sample_rate = nil
72
+ DatabaseLogger.sample_rate
73
+ #=> nil
74
+
75
+ ## commands getter returns the captured commands array
76
+ DatabaseLogger.clear_commands
77
+ commands = DatabaseLogger.commands
78
+ commands.class
79
+ #=> Concurrent::Array
80
+
81
+ ## commands array is initially empty after clear
82
+ DatabaseLogger.clear_commands
83
+ DatabaseLogger.commands.empty?
84
+ #=> true
85
+
86
+ ## process_start returns a float timestamp
87
+ DatabaseLogger.process_start.class
88
+ #=> Float
89
+
90
+ ## process_start is frozen
91
+ DatabaseLogger.process_start.frozen?
92
+ #=> true
93
+
94
+ ## clear_commands returns nil and empties the commands array
95
+ DatabaseLogger.instance_variable_get(:@commands) << "test"
96
+ result = DatabaseLogger.clear_commands
97
+ [result, DatabaseLogger.commands.empty?]
98
+ #=> [nil, true]
99
+
100
+ ## index returns the current count of commands
101
+ DatabaseLogger.clear_commands
102
+ DatabaseLogger.index
103
+ #=> 0
104
+
105
+ ## index increases as commands are added
106
+ DatabaseLogger.clear_commands
107
+ msg = DatabaseLogger::CommandMessage.new("TEST", 100, 0.001)
108
+ DatabaseLogger.append_command(msg)
109
+ DatabaseLogger.index
110
+ #=> 1
111
+
112
+ ## append_command adds a message to the commands array
113
+ DatabaseLogger.clear_commands
114
+ msg = DatabaseLogger::CommandMessage.new("SET key value", 500, 0.002)
115
+ result = DatabaseLogger.append_command(msg)
116
+ [DatabaseLogger.commands.size, result.last == msg]
117
+ #=> [1, true]
118
+
119
+ ## append_command respects max_commands limit by shifting oldest
120
+ DatabaseLogger.max_commands = 3
121
+ DatabaseLogger.clear_commands
122
+ msg1 = DatabaseLogger::CommandMessage.new("CMD1", 100, 0.001)
123
+ msg2 = DatabaseLogger::CommandMessage.new("CMD2", 200, 0.002)
124
+ msg3 = DatabaseLogger::CommandMessage.new("CMD3", 300, 0.003)
125
+ msg4 = DatabaseLogger::CommandMessage.new("CMD4", 400, 0.004)
126
+
127
+ DatabaseLogger.append_command(msg1)
128
+ DatabaseLogger.append_command(msg2)
129
+ DatabaseLogger.append_command(msg3)
130
+ DatabaseLogger.append_command(msg4)
131
+
132
+ [DatabaseLogger.commands.size, DatabaseLogger.commands.first == msg2]
133
+ #=> [3, true]
134
+
135
+ ## now_in_μs returns current time in microseconds
136
+ time1 = DatabaseLogger.now_in_μs
137
+ sleep(0.001)
138
+ time2 = DatabaseLogger.now_in_μs
139
+ time2 > time1
140
+ #=> true
141
+
142
+ ## now_in_microseconds is an alias for now_in_μs
143
+ DatabaseLogger.method(:now_in_microseconds) == DatabaseLogger.method(:now_in_μs)
144
+ #=> true
145
+
146
+ ## capture_commands yields and returns captured commands
147
+ DatabaseLogger.clear_commands
148
+ commands = DatabaseLogger.capture_commands do
149
+ msg = DatabaseLogger::CommandMessage.new("GET key", 150, 0.001)
150
+ DatabaseLogger.append_command(msg)
151
+ msg2 = DatabaseLogger::CommandMessage.new("SET key2 value", 200, 0.002)
152
+ DatabaseLogger.append_command(msg2)
153
+ end
154
+ commands.size
155
+ #=> 2
156
+
157
+ ## capture_commands clears commands before capturing
158
+ DatabaseLogger.clear_commands
159
+ msg_before = DatabaseLogger::CommandMessage.new("BEFORE", 100, 0.001)
160
+ DatabaseLogger.append_command(msg_before)
161
+
162
+ commands = DatabaseLogger.capture_commands do
163
+ msg = DatabaseLogger::CommandMessage.new("DURING", 150, 0.002)
164
+ DatabaseLogger.append_command(msg)
165
+ end
166
+
167
+ [commands.size, commands.first.command]
168
+ #=> [1, "DURING"]
169
+
170
+ ## capture_commands returns array snapshot, not live reference
171
+ DatabaseLogger.clear_commands
172
+ commands = DatabaseLogger.capture_commands do
173
+ msg = DatabaseLogger::CommandMessage.new("TEST", 100, 0.001)
174
+ DatabaseLogger.append_command(msg)
175
+ end
176
+
177
+ # Add another command after capture
178
+ msg2 = DatabaseLogger::CommandMessage.new("AFTER", 200, 0.002)
179
+ DatabaseLogger.append_command(msg2)
180
+
181
+ [commands.size, DatabaseLogger.commands.size]
182
+ #=> [1, 2]
183
+
184
+ ## should_log? returns true when sample_rate is nil
185
+ DatabaseLogger.sample_rate = nil
186
+ DatabaseLogger.logger = Logger.new(StringIO.new)
187
+ DatabaseLogger.should_log?
188
+ #=> true
189
+
190
+ ## should_log? returns false when logger is nil regardless of sample_rate
191
+ DatabaseLogger.sample_rate = 1.0
192
+ DatabaseLogger.logger = nil
193
+ DatabaseLogger.should_log?
194
+ #=> false
195
+
196
+ ## should_log? uses atomic counter for thread-safe sampling
197
+ DatabaseLogger.sample_rate = 0.5 # 50% sampling
198
+ DatabaseLogger.logger = Logger.new(StringIO.new)
199
+ DatabaseLogger.instance_variable_set(:@sample_counter, Concurrent::AtomicFixnum.new(0))
200
+
201
+ # Test deterministic sampling pattern
202
+ results = 10.times.map { DatabaseLogger.should_log? }
203
+ results.count(true)
204
+ #=> 5
205
+
206
+ ## should_log? sampling is deterministic and consistent
207
+ DatabaseLogger.sample_rate = 0.25 # 25% sampling (every 4th)
208
+ DatabaseLogger.instance_variable_set(:@sample_counter, Concurrent::AtomicFixnum.new(0))
209
+
210
+ results = 12.times.map { DatabaseLogger.should_log? }
211
+ results.count(true)
212
+ #=> 3
213
+
214
+ ## CommandMessage can be created with command, duration, timeline
215
+ msg = DatabaseLogger::CommandMessage.new("SET key value", 1500, 0.123456)
216
+ [msg.command, msg.μs, msg.timeline]
217
+ #=> ["SET key value", 1500, 0.123456]
218
+
219
+ ## CommandMessage.inspect formats nicely
220
+ msg = DatabaseLogger::CommandMessage.new("GET key", 2500, 1.234567)
221
+ msg.inspect
222
+ #=> "1.234567 2500μs > GET key"
223
+
224
+ ## CommandMessage.to_a returns deconstructed array
225
+ msg = DatabaseLogger::CommandMessage.new("DEL key1 key2", 750, 2.345678)
226
+ msg.to_a
227
+ #=> ["DEL key1 key2", 750, 2.345678]
228
+
229
+ ## Thread safety: append_command is thread-safe with concurrent access
230
+ # Reset max_commands to allow all 100 commands
231
+ DatabaseLogger.max_commands = 1000
232
+ DatabaseLogger.clear_commands
233
+ threads = 10.times.map do |i|
234
+ Thread.new do
235
+ 10.times do |j|
236
+ msg = DatabaseLogger::CommandMessage.new("THREAD#{i}_CMD#{j}", 100, 0.001)
237
+ DatabaseLogger.append_command(msg)
238
+ end
239
+ end
240
+ end
241
+
242
+ threads.each(&:join)
243
+ DatabaseLogger.commands.size
244
+ #=> 100
245
+
246
+ # Restore original state
247
+ DatabaseLogger.logger = @original_logger
248
+ DatabaseLogger.max_commands = @original_max_commands
249
+ DatabaseLogger.structured_logging = @original_structured_logging
250
+ DatabaseLogger.sample_rate = @original_sample_rate
251
+ DatabaseLogger.clear_commands
@@ -1,3 +1,7 @@
1
+ # try/unit/refinements/dear_json_array_methods_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/refinements/dear_json_array_methods_try.rb
2
6
 
3
7
  require_relative '../../support/helpers/test_helpers'
@@ -1,3 +1,7 @@
1
+ # try/unit/refinements/dear_json_hash_methods_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/refinements/dear_json_hash_methods_try.rb
2
6
 
3
7
  require_relative '../../support/helpers/test_helpers'
@@ -1,3 +1,7 @@
1
+ # try/unit/refinements/time_literals_numeric_methods_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/refinements/time_literals_numeric_methods_try.rb
2
6
 
3
7
  require_relative '../../support/helpers/test_helpers'
@@ -1,3 +1,7 @@
1
+ # try/unit/refinements/time_literals_string_methods_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
1
5
  # try/refinements/time_literals_string_methods_try.rb
2
6
 
3
7
  require_relative '../../support/helpers/test_helpers'
@@ -0,0 +1,149 @@
1
+ # try/unit/thread_safety_monitor_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../support/helpers/test_helpers'
6
+
7
+ # Thread safety monitoring tests
8
+ #
9
+ # Tests basic monitoring functionality without complex concurrency
10
+
11
+ # setup
12
+ Familia.stop_monitoring! if Familia.thread_safety_monitor.enabled
13
+ Familia.thread_safety_monitor.reset_metrics
14
+
15
+ ## Monitor starts and stops correctly
16
+ Familia.start_monitoring!
17
+ started = Familia.thread_safety_monitor.enabled
18
+ Familia.stop_monitoring!
19
+ stopped = !Familia.thread_safety_monitor.enabled
20
+ started && stopped
21
+ #=> true
22
+
23
+ ## Monitor tracks contentions
24
+ Familia.start_monitoring!
25
+ Familia.thread_safety_monitor.record_contention('test_location')
26
+ Familia.thread_safety_monitor.record_contention('test_location')
27
+ report = Familia.thread_safety_report
28
+ report[:summary][:mutex_contentions]
29
+ #=> 2
30
+
31
+ ## Monitor tracks different locations
32
+ Familia.thread_safety_monitor.reset_metrics
33
+ Familia.thread_safety_monitor.record_contention('location_a')
34
+ Familia.thread_safety_monitor.record_contention('location_b')
35
+ Familia.thread_safety_monitor.record_contention('location_a')
36
+ report = Familia.thread_safety_report
37
+ report[:hot_spots].size
38
+ #=> 2
39
+
40
+ ## Monitor tracks race conditions
41
+ Familia.thread_safety_monitor.reset_metrics
42
+ Familia.thread_safety_monitor.record_race_condition('test', 'details')
43
+ report = Familia.thread_safety_report
44
+ report[:summary][:race_detections]
45
+ #=> 1
46
+
47
+ ## Monitor exports metrics
48
+ Familia.thread_safety_monitor.reset_metrics
49
+ Familia.thread_safety_monitor.record_contention('test')
50
+ metrics = Familia.thread_safety_metrics
51
+ metrics['familia.thread_safety.mutex_contentions']
52
+ #=> 1
53
+
54
+ ## Health score starts at maximum
55
+ Familia.thread_safety_monitor.reset_metrics
56
+ report = Familia.thread_safety_report
57
+ report[:health]
58
+ #=> 100
59
+
60
+ ## InstrumentedMutex is used for connection chain
61
+ conn_mutex = Familia.instance_variable_get(:@connection_chain_mutex)
62
+ conn_mutex.is_a?(Familia::ThreadSafety::InstrumentedMutex)
63
+ #=> true
64
+
65
+ ## InstrumentedMutex tracks basic operations
66
+ mutex = Familia::ThreadSafety::InstrumentedMutex.new('test')
67
+ mutex.synchronize { 'work' }
68
+ stats = mutex.stats
69
+ stats[:lock_count]
70
+ #=> 1
71
+
72
+ ## Monitor time_critical_section works
73
+ Familia.thread_safety_monitor.reset_metrics
74
+ result = Familia.thread_safety_monitor.time_critical_section('test') { 37 }
75
+ result
76
+ #=> 37
77
+
78
+ ## Critical sections are tracked
79
+ report = Familia.thread_safety_report
80
+ report[:summary][:critical_sections]
81
+ #=> 1
82
+
83
+ ## Monitor uses microsecond timing for precision
84
+ Familia.thread_safety_monitor.reset_metrics
85
+ start_μs = Familia.now_in_μs
86
+ Familia.thread_safety_monitor.time_critical_section('timing_test') do
87
+ sleep 0.005 # 5ms sleep
88
+ end
89
+ end_μs = Familia.now_in_μs
90
+ duration_μs = end_μs - start_μs
91
+ # Should be at least 5000 microseconds (5ms)
92
+ duration_μs >= 5000
93
+ #=> true
94
+
95
+ ## Wait time tracking uses microsecond precision
96
+ mutex = Familia::ThreadSafety::InstrumentedMutex.new('timing_mutex')
97
+ Familia.thread_safety_monitor.reset_metrics
98
+
99
+ # Create intentional contention with timing
100
+ t1_ready = false
101
+ t2_ready = false
102
+
103
+ t1 = Thread.new do
104
+ mutex.synchronize do
105
+ t1_ready = true
106
+ sleep 0.01 while !t2_ready # Wait for t2 to be waiting
107
+ sleep 0.005 # Hold lock for 5ms
108
+ end
109
+ end
110
+
111
+ t2 = Thread.new do
112
+ sleep 0.001 while !t1_ready # Wait for t1 to acquire lock
113
+ t2_ready = true
114
+ mutex.synchronize { 'got lock' }
115
+ end
116
+
117
+ t1.join
118
+ t2.join
119
+
120
+ stats = mutex.stats
121
+ # Should show contention occurred due to intentional delay
122
+ stats[:contention_count] > 0
123
+ #=> true
124
+
125
+ ## Monitor preserves microsecond precision
126
+ Familia.thread_safety_monitor.reset_metrics
127
+ Familia.thread_safety_monitor.record_contention('precision_test', 1500) # 1500μs = 1.5ms
128
+
129
+ report = Familia.thread_safety_report
130
+ hot_spot = report[:hot_spots].first
131
+ # Should preserve microsecond precision, not convert to seconds
132
+ hot_spot[:avg_wait_μs]
133
+ #=> 1500
134
+
135
+ ## Critical section timing uses microseconds
136
+ Familia.thread_safety_monitor.reset_metrics
137
+ result = Familia.thread_safety_monitor.time_critical_section('precision_timing') do
138
+ sleep 0.003 # 3ms
139
+ 37
140
+ end
141
+
142
+ report = Familia.thread_safety_report
143
+ section_perf = report[:section_performance].first
144
+ # Total time should be in microseconds (≥3000μs for 3ms sleep)
145
+ section_perf[:total_time_μs] >= 3000
146
+ #=> true
147
+
148
+ # teardown
149
+ Familia.stop_monitoring!