neo4j-ruby-driver 6.0.3.alpha.0 → 6.2.1.beta.2

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 (348) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +113 -411
  3. data/{ruby → lib}/neo4j/driver/access_mode.rb +2 -1
  4. data/lib/neo4j/driver/auth_token_managers.rb +31 -0
  5. data/lib/neo4j/driver/auth_tokens.rb +41 -0
  6. data/lib/neo4j/driver/auto_commit_retries_mode.rb +18 -0
  7. data/lib/neo4j/driver/bolt/bolt_version.rb +67 -0
  8. data/lib/neo4j/driver/bolt/connection.rb +1091 -0
  9. data/lib/neo4j/driver/bolt/handshake.rb +236 -0
  10. data/lib/neo4j/driver/bolt/message/failure.rb +139 -0
  11. data/lib/neo4j/driver/bolt/message/ignored.rb +27 -0
  12. data/lib/neo4j/driver/bolt/message/record.rb +30 -0
  13. data/lib/neo4j/driver/bolt/message/success.rb +34 -0
  14. data/lib/neo4j/driver/bolt/message.rb +112 -0
  15. data/lib/neo4j/driver/bolt/pool.rb +255 -0
  16. data/lib/neo4j/driver/bolt/protocol/base.rb +146 -0
  17. data/lib/neo4j/driver/bolt/protocol/v3.rb +43 -0
  18. data/lib/neo4j/driver/bolt/protocol/v4.rb +24 -0
  19. data/lib/neo4j/driver/bolt/protocol/v43.rb +28 -0
  20. data/lib/neo4j/driver/bolt/protocol/v44.rb +23 -0
  21. data/lib/neo4j/driver/bolt/protocol/v5.rb +27 -0
  22. data/lib/neo4j/driver/bolt/protocol/v51.rb +24 -0
  23. data/lib/neo4j/driver/bolt/protocol/v52.rb +28 -0
  24. data/lib/neo4j/driver/bolt/protocol/v53.rb +27 -0
  25. data/lib/neo4j/driver/bolt/protocol/v54.rb +14 -0
  26. data/lib/neo4j/driver/bolt/protocol/v55.rb +17 -0
  27. data/lib/neo4j/driver/bolt/protocol/v56.rb +14 -0
  28. data/lib/neo4j/driver/bolt/protocol/v57.rb +31 -0
  29. data/lib/neo4j/driver/bolt/protocol/v58.rb +15 -0
  30. data/lib/neo4j/driver/bolt/protocol/v6.rb +43 -0
  31. data/lib/neo4j/driver/bolt/protocol/v61.rb +21 -0
  32. data/lib/neo4j/driver/bolt/protocol_version_handler.rb +39 -0
  33. data/lib/neo4j/driver/bolt/record_buffer.rb +143 -0
  34. data/lib/neo4j/driver/bolt/stream_handler.rb +53 -0
  35. data/lib/neo4j/driver/bolt/tls_config.rb +129 -0
  36. data/lib/neo4j/driver/bolt/wire.rb +321 -0
  37. data/lib/neo4j/driver/bookmark.rb +15 -0
  38. data/lib/neo4j/driver/bookmark_managers.rb +39 -0
  39. data/lib/neo4j/driver/client_certificate.rb +19 -0
  40. data/lib/neo4j/driver/client_certificate_manager.rb +37 -0
  41. data/lib/neo4j/driver/client_certificate_managers.rb +15 -0
  42. data/lib/neo4j/driver/client_certificates.rb +14 -0
  43. data/lib/neo4j/driver/direct/connection_provider.rb +262 -0
  44. data/lib/neo4j/driver/driver.rb +268 -0
  45. data/lib/neo4j/driver/eager_result.rb +10 -0
  46. data/lib/neo4j/driver/exceptions/neo4j_exception.rb +16 -7
  47. data/lib/neo4j/driver/exceptions/no_such_record_exception.rb +1 -1
  48. data/lib/neo4j/driver/exceptions/protocol_exception.rb +0 -5
  49. data/lib/neo4j/driver/exceptions/security_retryable_exception.rb +14 -0
  50. data/lib/neo4j/driver/exceptions/transaction_terminated_exception.rb +13 -0
  51. data/lib/neo4j/driver/graph_database.rb +29 -0
  52. data/lib/neo4j/driver/internal/clock.rb +20 -0
  53. data/lib/neo4j/driver/internal/clock_adapter.rb +21 -0
  54. data/lib/neo4j/driver/internal/default_bookmark_manager.rb +54 -0
  55. data/lib/neo4j/driver/internal/deprecator.rb +9 -5
  56. data/lib/neo4j/driver/internal/driver_factory.rb +128 -0
  57. data/lib/neo4j/driver/internal/duration_normalizer.rb +14 -39
  58. data/lib/neo4j/driver/internal/extras.rb +23 -0
  59. data/lib/neo4j/driver/internal/home_db_cache.rb +57 -0
  60. data/lib/neo4j/driver/internal/internal_auth_token_manager.rb +37 -0
  61. data/lib/neo4j/driver/internal/metrics.rb +31 -0
  62. data/lib/neo4j/driver/internal/security/static_auth_token_manager.rb +25 -0
  63. data/lib/neo4j/driver/internal/validator.rb +16 -2
  64. data/lib/neo4j/driver/net.rb +29 -0
  65. data/lib/neo4j/driver/packstream/markers.rb +49 -0
  66. data/lib/neo4j/driver/packstream/packer.rb +335 -0
  67. data/lib/neo4j/driver/packstream/structure.rb +27 -0
  68. data/lib/neo4j/driver/packstream/unpacker.rb +170 -0
  69. data/lib/neo4j/driver/record.rb +43 -0
  70. data/lib/neo4j/driver/result.rb +282 -0
  71. data/lib/neo4j/driver/routing/load_balancer.rb +706 -0
  72. data/lib/neo4j/driver/routing/routed_connection.rb +132 -0
  73. data/lib/neo4j/driver/routing/routing_table.rb +127 -0
  74. data/lib/neo4j/driver/routing/server_address.rb +25 -0
  75. data/lib/neo4j/driver/routing_control.rb +13 -0
  76. data/lib/neo4j/driver/session.rb +577 -0
  77. data/lib/neo4j/driver/summary/database_info.rb +25 -0
  78. data/lib/neo4j/driver/summary/gql_notification.rb +38 -0
  79. data/lib/neo4j/driver/summary/gql_status_object.rb +35 -0
  80. data/lib/neo4j/driver/summary/notification.rb +46 -0
  81. data/lib/neo4j/driver/summary/plan.rb +23 -0
  82. data/lib/neo4j/driver/summary/profile.rb +28 -0
  83. data/lib/neo4j/driver/summary/query.rb +19 -0
  84. data/lib/neo4j/driver/summary/query_type.rb +5 -1
  85. data/lib/neo4j/driver/summary/result_summary.rb +215 -0
  86. data/lib/neo4j/driver/summary/server_info.rb +33 -0
  87. data/lib/neo4j/driver/summary/summary_counters.rb +72 -0
  88. data/lib/neo4j/driver/transaction.rb +344 -0
  89. data/lib/neo4j/driver/types/duration.rb +76 -0
  90. data/lib/neo4j/driver/types/entity.rb +40 -0
  91. data/lib/neo4j/driver/types/local_date_time.rb +77 -8
  92. data/lib/neo4j/driver/types/local_time.rb +50 -7
  93. data/lib/neo4j/driver/types/node.rb +18 -0
  94. data/lib/neo4j/driver/types/offset_time.rb +78 -7
  95. data/lib/neo4j/driver/types/path.rb +68 -0
  96. data/lib/neo4j/driver/types/point.rb +38 -18
  97. data/lib/neo4j/driver/types/relationship.rb +32 -0
  98. data/lib/neo4j/driver/types/temporal_value.rb +44 -0
  99. data/lib/neo4j/driver/types/unbound_relationship.rb +24 -0
  100. data/lib/neo4j/driver/types/unresolvable_zoned_date_time.rb +38 -0
  101. data/lib/neo4j/driver/types/unsupported_type.rb +25 -0
  102. data/lib/neo4j/driver/types/uuid.rb +29 -0
  103. data/{ruby → lib}/neo4j/driver/version.rb +1 -1
  104. data/lib/neo4j/driver.rb +15 -0
  105. data/lib/neo4j-ruby-driver_loader.rb +34 -20
  106. metadata +115 -441
  107. data/lib/neo4j/driver/internal/bolt_server_address.rb +0 -97
  108. data/lib/neo4j/driver/synchronizable.rb +0 -23
  109. data/lib/neo4j/driver/types/time.rb +0 -45
  110. data/ruby/neo4j/driver/auth_tokens.rb +0 -34
  111. data/ruby/neo4j/driver/bookmark.rb +0 -21
  112. data/ruby/neo4j/driver/config.rb +0 -89
  113. data/ruby/neo4j/driver/graph_database.rb +0 -78
  114. data/ruby/neo4j/driver/internal/async/connection/bolt_protocol_util.rb +0 -51
  115. data/ruby/neo4j/driver/internal/async/connection/bootstrap_factory.rb +0 -22
  116. data/ruby/neo4j/driver/internal/async/connection/channel_attributes.rb +0 -31
  117. data/ruby/neo4j/driver/internal/async/connection/channel_connected_listener.rb +0 -32
  118. data/ruby/neo4j/driver/internal/async/connection/channel_connector_impl.rb +0 -83
  119. data/ruby/neo4j/driver/internal/async/connection/channel_pipeline_builder_impl.rb +0 -22
  120. data/ruby/neo4j/driver/internal/async/connection/direct_connection.rb +0 -30
  121. data/ruby/neo4j/driver/internal/async/connection/event_loop_group_factory.rb +0 -83
  122. data/ruby/neo4j/driver/internal/async/connection/handshake_completed_listener.rb +0 -27
  123. data/ruby/neo4j/driver/internal/async/connection/handshake_handler.rb +0 -113
  124. data/ruby/neo4j/driver/internal/async/connection/netty_channel_initializer.rb +0 -57
  125. data/ruby/neo4j/driver/internal/async/connection/netty_domain_name_resolver.rb +0 -26
  126. data/ruby/neo4j/driver/internal/async/connection/netty_domain_name_resolver_group.rb +0 -19
  127. data/ruby/neo4j/driver/internal/async/connection/routing_connection.rb +0 -36
  128. data/ruby/neo4j/driver/internal/async/connection/stream.rb +0 -12
  129. data/ruby/neo4j/driver/internal/async/connection/stream_reader.rb +0 -16
  130. data/ruby/neo4j/driver/internal/async/connection_context.rb +0 -10
  131. data/ruby/neo4j/driver/internal/async/immutable_connection_context.rb +0 -24
  132. data/ruby/neo4j/driver/internal/async/inbound/byte_buf_input.rb +0 -30
  133. data/ruby/neo4j/driver/internal/async/inbound/channel_error_handler.rb +0 -77
  134. data/ruby/neo4j/driver/internal/async/inbound/chunk_decoder.rb +0 -41
  135. data/ruby/neo4j/driver/internal/async/inbound/connect_timeout_handler.rb +0 -32
  136. data/ruby/neo4j/driver/internal/async/inbound/connection_read_timeout_handler.rb +0 -17
  137. data/ruby/neo4j/driver/internal/async/inbound/inbound_message_dispatcher.rb +0 -171
  138. data/ruby/neo4j/driver/internal/async/inbound/inbound_message_handler.rb +0 -42
  139. data/ruby/neo4j/driver/internal/async/inbound/message_decoder.rb +0 -51
  140. data/ruby/neo4j/driver/internal/async/internal_async_session.rb +0 -92
  141. data/ruby/neo4j/driver/internal/async/internal_async_transaction.rb +0 -13
  142. data/ruby/neo4j/driver/internal/async/leak_logging_network_session.rb +0 -34
  143. data/ruby/neo4j/driver/internal/async/network_connection.rb +0 -194
  144. data/ruby/neo4j/driver/internal/async/network_session.rb +0 -150
  145. data/ruby/neo4j/driver/internal/async/outbound/chunk_aware_byte_buf_output.rb +0 -110
  146. data/ruby/neo4j/driver/internal/async/outbound/outbound_message_handler.rb +0 -39
  147. data/ruby/neo4j/driver/internal/async/pool/channel.rb +0 -66
  148. data/ruby/neo4j/driver/internal/async/pool/channel_pool.rb +0 -31
  149. data/ruby/neo4j/driver/internal/async/pool/channel_tracker.rb +0 -135
  150. data/ruby/neo4j/driver/internal/async/pool/connection_pool_impl.rb +0 -156
  151. data/ruby/neo4j/driver/internal/async/pool/netty_channel_health_checker.rb +0 -87
  152. data/ruby/neo4j/driver/internal/async/pool/netty_channel_pool.rb +0 -52
  153. data/ruby/neo4j/driver/internal/async/pool/network_connection_factory.rb +0 -21
  154. data/ruby/neo4j/driver/internal/async/pool/pool_settings.rb +0 -34
  155. data/ruby/neo4j/driver/internal/async/pool/timed_stack.rb +0 -15
  156. data/ruby/neo4j/driver/internal/async/result_cursors_holder.rb +0 -17
  157. data/ruby/neo4j/driver/internal/async/unmanaged_transaction.rb +0 -212
  158. data/ruby/neo4j/driver/internal/bookmark_holder.rb +0 -9
  159. data/ruby/neo4j/driver/internal/cluster/cluster_composition.rb +0 -48
  160. data/ruby/neo4j/driver/internal/cluster/cluster_composition_lookup_result.rb +0 -14
  161. data/ruby/neo4j/driver/internal/cluster/cluster_routing_table.rb +0 -122
  162. data/ruby/neo4j/driver/internal/cluster/identity_resolver.rb +0 -10
  163. data/ruby/neo4j/driver/internal/cluster/loadbalancing/least_connected_load_balancing_strategy.rb +0 -68
  164. data/ruby/neo4j/driver/internal/cluster/loadbalancing/load_balancer.rb +0 -125
  165. data/ruby/neo4j/driver/internal/cluster/loadbalancing/round_robin_array_index.rb +0 -13
  166. data/ruby/neo4j/driver/internal/cluster/multi_databases_routing_procedure_runner.rb +0 -31
  167. data/ruby/neo4j/driver/internal/cluster/rediscovery_impl.rb +0 -147
  168. data/ruby/neo4j/driver/internal/cluster/route_message_routing_procedure_runner.rb +0 -43
  169. data/ruby/neo4j/driver/internal/cluster/routing_context.rb +0 -77
  170. data/ruby/neo4j/driver/internal/cluster/routing_procedure_cluster_composition_provider.rb +0 -60
  171. data/ruby/neo4j/driver/internal/cluster/routing_procedure_response.rb +0 -35
  172. data/ruby/neo4j/driver/internal/cluster/routing_settings.rb +0 -24
  173. data/ruby/neo4j/driver/internal/cluster/routing_table_handler_impl.rb +0 -95
  174. data/ruby/neo4j/driver/internal/cluster/routing_table_registry_impl.rb +0 -121
  175. data/ruby/neo4j/driver/internal/cluster/single_database_routing_procedure_runner.rb +0 -73
  176. data/ruby/neo4j/driver/internal/connection_settings.rb +0 -16
  177. data/ruby/neo4j/driver/internal/cursor/async_result_cursor_impl.rb +0 -55
  178. data/ruby/neo4j/driver/internal/cursor/async_result_cursor_only_factory.rb +0 -24
  179. data/ruby/neo4j/driver/internal/cursor/disposable_async_result_cursor.rb +0 -61
  180. data/ruby/neo4j/driver/internal/cursor/result_cursor_factory_impl.rb +0 -24
  181. data/ruby/neo4j/driver/internal/cursor/rx_result_cursor_impl.rb +0 -110
  182. data/ruby/neo4j/driver/internal/database_name_util.rb +0 -37
  183. data/ruby/neo4j/driver/internal/default_bookmark_holder.rb +0 -9
  184. data/ruby/neo4j/driver/internal/default_domain_name_resolver.rb +0 -11
  185. data/ruby/neo4j/driver/internal/direct_connection_provider.rb +0 -40
  186. data/ruby/neo4j/driver/internal/driver_factory.rb +0 -126
  187. data/ruby/neo4j/driver/internal/eager_result_value.rb +0 -5
  188. data/ruby/neo4j/driver/internal/handlers/begin_tx_response_handler.rb +0 -20
  189. data/ruby/neo4j/driver/internal/handlers/channel_releasing_reset_response_handler.rb +0 -30
  190. data/ruby/neo4j/driver/internal/handlers/commit_tx_response_handler.rb +0 -23
  191. data/ruby/neo4j/driver/internal/handlers/hello_response_handler.rb +0 -65
  192. data/ruby/neo4j/driver/internal/handlers/init_response_handler.rb +0 -34
  193. data/ruby/neo4j/driver/internal/handlers/legacy_pull_all_response_handler.rb +0 -199
  194. data/ruby/neo4j/driver/internal/handlers/no_op_response_handler.rb +0 -16
  195. data/ruby/neo4j/driver/internal/handlers/ping_response_handler.rb +0 -29
  196. data/ruby/neo4j/driver/internal/handlers/pull_handlers.rb +0 -32
  197. data/ruby/neo4j/driver/internal/handlers/pulln/auto_pull_response_handler.rb +0 -169
  198. data/ruby/neo4j/driver/internal/handlers/pulln/basic_pull_response_handler.rb +0 -298
  199. data/ruby/neo4j/driver/internal/handlers/pulln/fetch_size_util.rb +0 -20
  200. data/ruby/neo4j/driver/internal/handlers/reset_response_handler.rb +0 -34
  201. data/ruby/neo4j/driver/internal/handlers/rollback_tx_response_handler.rb +0 -25
  202. data/ruby/neo4j/driver/internal/handlers/route_message_response_handler.rb +0 -21
  203. data/ruby/neo4j/driver/internal/handlers/routing_response_handler.rb +0 -79
  204. data/ruby/neo4j/driver/internal/handlers/run_response_handler.rb +0 -38
  205. data/ruby/neo4j/driver/internal/handlers/session_pull_response_completion_listener.rb +0 -34
  206. data/ruby/neo4j/driver/internal/handlers/transaction_pull_response_completion_listener.rb +0 -20
  207. data/ruby/neo4j/driver/internal/impersonation_util.rb +0 -22
  208. data/ruby/neo4j/driver/internal/internal_bookmark.rb +0 -9
  209. data/ruby/neo4j/driver/internal/internal_database_name.rb +0 -9
  210. data/ruby/neo4j/driver/internal/internal_driver.rb +0 -83
  211. data/ruby/neo4j/driver/internal/internal_entity.rb +0 -21
  212. data/ruby/neo4j/driver/internal/internal_node.rb +0 -21
  213. data/ruby/neo4j/driver/internal/internal_pair.rb +0 -9
  214. data/ruby/neo4j/driver/internal/internal_path.rb +0 -35
  215. data/ruby/neo4j/driver/internal/internal_point2_d.rb +0 -9
  216. data/ruby/neo4j/driver/internal/internal_point3_d.rb +0 -6
  217. data/ruby/neo4j/driver/internal/internal_record.rb +0 -27
  218. data/ruby/neo4j/driver/internal/internal_relationship.rb +0 -27
  219. data/ruby/neo4j/driver/internal/internal_result.rb +0 -49
  220. data/ruby/neo4j/driver/internal/internal_session.rb +0 -91
  221. data/ruby/neo4j/driver/internal/internal_transaction.rb +0 -48
  222. data/ruby/neo4j/driver/internal/logging/channel_activity_logger.rb +0 -29
  223. data/ruby/neo4j/driver/internal/logging/channel_error_logger.rb +0 -17
  224. data/ruby/neo4j/driver/internal/logging/prefixed_logger.rb +0 -19
  225. data/ruby/neo4j/driver/internal/logging/reformatted_logger.rb +0 -17
  226. data/ruby/neo4j/driver/internal/messaging/abstract_message_writer.rb +0 -23
  227. data/ruby/neo4j/driver/internal/messaging/bolt_protocol.rb +0 -32
  228. data/ruby/neo4j/driver/internal/messaging/bolt_protocol_version.rb +0 -48
  229. data/ruby/neo4j/driver/internal/messaging/common/common_message_reader.rb +0 -51
  230. data/ruby/neo4j/driver/internal/messaging/common/common_value.rb +0 -33
  231. data/ruby/neo4j/driver/internal/messaging/common/common_value_packer.rb +0 -104
  232. data/ruby/neo4j/driver/internal/messaging/common/common_value_unpacker.rb +0 -256
  233. data/ruby/neo4j/driver/internal/messaging/encode/begin_message_encoder.rb +0 -15
  234. data/ruby/neo4j/driver/internal/messaging/encode/commit_message_encoder.rb +0 -14
  235. data/ruby/neo4j/driver/internal/messaging/encode/discard_all_message_encoder.rb +0 -14
  236. data/ruby/neo4j/driver/internal/messaging/encode/discard_message_encoder.rb +0 -15
  237. data/ruby/neo4j/driver/internal/messaging/encode/goodbye_message_encoder.rb +0 -14
  238. data/ruby/neo4j/driver/internal/messaging/encode/hello_message_encoder.rb +0 -15
  239. data/ruby/neo4j/driver/internal/messaging/encode/init_message_encoder.rb +0 -16
  240. data/ruby/neo4j/driver/internal/messaging/encode/logoff_message_encoder.rb +0 -14
  241. data/ruby/neo4j/driver/internal/messaging/encode/logon_message_encoder.rb +0 -15
  242. data/ruby/neo4j/driver/internal/messaging/encode/pull_all_message_encoder.rb +0 -14
  243. data/ruby/neo4j/driver/internal/messaging/encode/pull_message_encoder.rb +0 -15
  244. data/ruby/neo4j/driver/internal/messaging/encode/reset_message_encoder.rb +0 -14
  245. data/ruby/neo4j/driver/internal/messaging/encode/rollback_message_encoder.rb +0 -14
  246. data/ruby/neo4j/driver/internal/messaging/encode/route_message_encoder.rb +0 -24
  247. data/ruby/neo4j/driver/internal/messaging/encode/route_v44_message_encoder.rb +0 -22
  248. data/ruby/neo4j/driver/internal/messaging/encode/run_message_encoder.rb +0 -16
  249. data/ruby/neo4j/driver/internal/messaging/encode/run_with_metadata_message_encoder.rb +0 -17
  250. data/ruby/neo4j/driver/internal/messaging/request/abstract_streaming_message.rb +0 -25
  251. data/ruby/neo4j/driver/internal/messaging/request/begin_message.rb +0 -25
  252. data/ruby/neo4j/driver/internal/messaging/request/commit_message.rb +0 -20
  253. data/ruby/neo4j/driver/internal/messaging/request/discard_all_message.rb +0 -20
  254. data/ruby/neo4j/driver/internal/messaging/request/discard_message.rb +0 -23
  255. data/ruby/neo4j/driver/internal/messaging/request/goodbye_message.rb +0 -20
  256. data/ruby/neo4j/driver/internal/messaging/request/hello_message.rb +0 -30
  257. data/ruby/neo4j/driver/internal/messaging/request/init_message.rb +0 -19
  258. data/ruby/neo4j/driver/internal/messaging/request/logoff_message.rb +0 -13
  259. data/ruby/neo4j/driver/internal/messaging/request/logon_message.rb +0 -13
  260. data/ruby/neo4j/driver/internal/messaging/request/message_with_metadata.rb +0 -17
  261. data/ruby/neo4j/driver/internal/messaging/request/multi_database_util.rb +0 -26
  262. data/ruby/neo4j/driver/internal/messaging/request/pull_all_message.rb +0 -23
  263. data/ruby/neo4j/driver/internal/messaging/request/pull_message.rb +0 -22
  264. data/ruby/neo4j/driver/internal/messaging/request/reset_message.rb +0 -32
  265. data/ruby/neo4j/driver/internal/messaging/request/rollback_message.rb +0 -20
  266. data/ruby/neo4j/driver/internal/messaging/request/route_message.rb +0 -28
  267. data/ruby/neo4j/driver/internal/messaging/request/run_message.rb +0 -23
  268. data/ruby/neo4j/driver/internal/messaging/request/run_with_metadata_message.rb +0 -49
  269. data/ruby/neo4j/driver/internal/messaging/request/transaction_metadata_builder.rb +0 -24
  270. data/ruby/neo4j/driver/internal/messaging/response/failure_message.rb +0 -40
  271. data/ruby/neo4j/driver/internal/messaging/response/ignored_message.rb +0 -29
  272. data/ruby/neo4j/driver/internal/messaging/response/record_message.rb +0 -33
  273. data/ruby/neo4j/driver/internal/messaging/response/success_message.rb +0 -34
  274. data/ruby/neo4j/driver/internal/messaging/v3/bolt_protocol_v3.rb +0 -82
  275. data/ruby/neo4j/driver/internal/messaging/v3/message_format_v3.rb +0 -17
  276. data/ruby/neo4j/driver/internal/messaging/v3/message_writer_v3.rb +0 -27
  277. data/ruby/neo4j/driver/internal/messaging/v4/bolt_protocol_v4.rb +0 -29
  278. data/ruby/neo4j/driver/internal/messaging/v4/message_format_v4.rb +0 -13
  279. data/ruby/neo4j/driver/internal/messaging/v4/message_writer_v4.rb +0 -17
  280. data/ruby/neo4j/driver/internal/messaging/v41/bolt_protocol_v41.rb +0 -25
  281. data/ruby/neo4j/driver/internal/messaging/v42/bolt_protocol_v42.rb +0 -13
  282. data/ruby/neo4j/driver/internal/messaging/v43/bolt_protocol_v43.rb +0 -19
  283. data/ruby/neo4j/driver/internal/messaging/v43/message_format_v43.rb +0 -14
  284. data/ruby/neo4j/driver/internal/messaging/v43/message_writer_v43.rb +0 -20
  285. data/ruby/neo4j/driver/internal/messaging/v44/bolt_protocol_v44.rb +0 -17
  286. data/ruby/neo4j/driver/internal/messaging/v44/message_format_v44.rb +0 -14
  287. data/ruby/neo4j/driver/internal/messaging/v44/message_writer_v44.rb +0 -15
  288. data/ruby/neo4j/driver/internal/messaging/v5/bolt_protocol_v5.rb +0 -21
  289. data/ruby/neo4j/driver/internal/messaging/v5/message_format_v5.rb +0 -18
  290. data/ruby/neo4j/driver/internal/messaging/v5/value_unpacker_v5.rb +0 -17
  291. data/ruby/neo4j/driver/internal/messaging/v51/bolt_protocol_v51.rb +0 -40
  292. data/ruby/neo4j/driver/internal/messaging/v51/message_format_v51.rb +0 -14
  293. data/ruby/neo4j/driver/internal/messaging/v51/message_writer_v51.rb +0 -17
  294. data/ruby/neo4j/driver/internal/metrics/connection_pool_metrics_listener.rb +0 -34
  295. data/ruby/neo4j/driver/internal/metrics/internal_abstract_metrics.rb +0 -46
  296. data/ruby/neo4j/driver/internal/metrics/internal_connection_pool_metrics.rb +0 -105
  297. data/ruby/neo4j/driver/internal/metrics/internal_metrics.rb +0 -82
  298. data/ruby/neo4j/driver/internal/metrics/internal_metrics_provider.rb +0 -18
  299. data/ruby/neo4j/driver/internal/metrics/listener_event.rb +0 -17
  300. data/ruby/neo4j/driver/internal/metrics/metrics_provider.rb +0 -24
  301. data/ruby/neo4j/driver/internal/metrics/time_recorder_listener_event.rb +0 -15
  302. data/ruby/neo4j/driver/internal/packstream/byte_array_incompatible_packer.rb +0 -12
  303. data/ruby/neo4j/driver/internal/packstream/pack_input.rb +0 -47
  304. data/ruby/neo4j/driver/internal/packstream/pack_output.rb +0 -39
  305. data/ruby/neo4j/driver/internal/packstream/pack_stream.rb +0 -324
  306. data/ruby/neo4j/driver/internal/packstream/pack_type.rb +0 -17
  307. data/ruby/neo4j/driver/internal/read_only_bookmark_holder.rb +0 -13
  308. data/ruby/neo4j/driver/internal/resolved_bolt_server_address.rb +0 -35
  309. data/ruby/neo4j/driver/internal/retry/exponential_backoff_retry_logic.rb +0 -150
  310. data/ruby/neo4j/driver/internal/revocation_strategy.rb +0 -19
  311. data/ruby/neo4j/driver/internal/scheme.rb +0 -32
  312. data/ruby/neo4j/driver/internal/security/internal_auth_token.rb +0 -15
  313. data/ruby/neo4j/driver/internal/security/security_plan_impl.rb +0 -48
  314. data/ruby/neo4j/driver/internal/security_setting.rb +0 -66
  315. data/ruby/neo4j/driver/internal/session_factory_impl.rb +0 -32
  316. data/ruby/neo4j/driver/internal/spi/connection.rb +0 -19
  317. data/ruby/neo4j/driver/internal/spi/connection_pool.rb +0 -9
  318. data/ruby/neo4j/driver/internal/spi/response_handler.rb +0 -23
  319. data/ruby/neo4j/driver/internal/summary/internal_database_info.rb +0 -7
  320. data/ruby/neo4j/driver/internal/summary/internal_input_position.rb +0 -11
  321. data/ruby/neo4j/driver/internal/summary/internal_notification.rb +0 -16
  322. data/ruby/neo4j/driver/internal/summary/internal_plan.rb +0 -41
  323. data/ruby/neo4j/driver/internal/summary/internal_profiled_plan.rb +0 -32
  324. data/ruby/neo4j/driver/internal/summary/internal_result_summary.rb +0 -33
  325. data/ruby/neo4j/driver/internal/summary/internal_server_info.rb +0 -6
  326. data/ruby/neo4j/driver/internal/summary/internal_summary_counters.rb +0 -18
  327. data/ruby/neo4j/driver/internal/svm/netty_substitutions.rb +0 -196
  328. data/ruby/neo4j/driver/internal/svm/z_lib_substitutions.rb +0 -21
  329. data/ruby/neo4j/driver/internal/util/certificate_tool.rb +0 -65
  330. data/ruby/neo4j/driver/internal/util/clock.rb +0 -29
  331. data/ruby/neo4j/driver/internal/util/error_util.rb +0 -104
  332. data/ruby/neo4j/driver/internal/util/extract.rb +0 -123
  333. data/ruby/neo4j/driver/internal/util/format.rb +0 -39
  334. data/ruby/neo4j/driver/internal/util/futures.rb +0 -99
  335. data/ruby/neo4j/driver/internal/util/iterables.rb +0 -35
  336. data/ruby/neo4j/driver/internal/util/lock_util.rb +0 -23
  337. data/ruby/neo4j/driver/internal/util/metadata_extractor.rb +0 -101
  338. data/ruby/neo4j/driver/internal/util/mutex.rb +0 -9
  339. data/ruby/neo4j/driver/internal/util/preconditions.rb +0 -16
  340. data/ruby/neo4j/driver/internal/util/result_holder.rb +0 -72
  341. data/ruby/neo4j/driver/internal/util/server_version.rb +0 -60
  342. data/ruby/neo4j/driver/logging1.rb +0 -51
  343. data/ruby/neo4j/driver/net/server_address.rb +0 -9
  344. data/ruby/neo4j/driver/query.rb +0 -48
  345. data/ruby/neo4j/driver/records.rb +0 -13
  346. data/ruby/neo4j/driver/transaction_config.rb +0 -50
  347. data/ruby/neo4j/driver/values.rb +0 -26
  348. data/ruby/neo4j/driver.rb +0 -30
@@ -0,0 +1,1091 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Neo4j
4
+ module Driver
5
+ module Bolt
6
+ # Handles a single Bolt protocol connection over TCP
7
+ class Connection
8
+ DEFAULT_PORT = 7687
9
+ # Per-read upper bound; the wire reassembles across reads, so this is
10
+ # just how much we ask the socket for at once.
11
+ READ_CHUNK = 65_536
12
+
13
+ # The on-demand pull model's response handler: the wire routes a
14
+ # request's reply here (RECORDs then a terminal), and it just collects
15
+ # everything into the shared inbox queue for fetch_response to drain.
16
+ # (A streaming request could register a record-routing handler instead.)
17
+ ResponseCollector = Struct.new(:inbox) do
18
+ def on_record(message) = inbox.push(message)
19
+ def on_success(message) = inbox.push(message)
20
+ def on_failure(message) = inbox.push(message)
21
+ def on_ignored(message) = inbox.push(message)
22
+ # Failure fan-out is handled connection-wide (inbox closed + @broken_error),
23
+ # so a sync handler needs nothing here.
24
+ def fail(_error); end
25
+ end
26
+
27
+ attr_reader :server_version, :server_agent, :protocol, :address
28
+ # idle_since: stamp the pool sets when it pushes a connection
29
+ # back. Bolt::Pool reads it on pop to decide whether to run a
30
+ # liveness probe (idle longer than the configured threshold).
31
+ # created_at: when the TCP/Bolt handshake finished, so the
32
+ # pool can evict connections older than max_connection_lifetime.
33
+ attr_accessor :idle_since, :created_at
34
+ # Set when this connection must not return to the pool — e.g. an
35
+ # auth failure (the server closes the connection after a security
36
+ # FAILURE, and the identity is compromised either way). The direct
37
+ # provider's release discards instead of pooling. (Routing's
38
+ # RoutedConnection carries its own discard_on_release flag.)
39
+ attr_accessor :discard_on_release
40
+ # Set on a security FAILURE specifically: the server closes the
41
+ # connection, so callers must NOT send a RESET (it would error /
42
+ # surface a spurious wire error). Distinct from discard_on_release,
43
+ # which also covers still-alive cases (e.g. NotALeader) that DO
44
+ # want a RESET before the connection is dropped.
45
+ attr_accessor :auth_failed
46
+
47
+ # `domain_name_resolver` is the non-public hostname->IPs hook the
48
+ # DriverFactory wires in (default nil = system DNS). It's an explicit
49
+ # dependency, not part of the user `options`, so factory-only
50
+ # extension points never leak into the driver's public config.
51
+ def initialize(uri, auth, options = {}, domain_name_resolver: nil, clock: Internal::Clock.new)
52
+ @uri = URI(uri)
53
+ # The driver's stored auth — the identity HELLO/LOGON
54
+ # authenticated as on connect, and what Session restores via
55
+ # authenticate(driver_auth) when no per-session :auth was
56
+ # given but a previous lessee had switched identity.
57
+ @driver_auth = auth
58
+ @auth = auth
59
+ @options = options
60
+ @clock = clock
61
+ @domain_name_resolver = domain_name_resolver
62
+ @socket = nil
63
+ # The sans-I/O core: framing + hydration, no socket. Built once the
64
+ # handshake has negotiated a protocol (perform_handshake). This
65
+ # Connection is the on-demand pump over it — it owns the socket and
66
+ # moves bytes between it and the wire on the caller's thread.
67
+ @wire = nil
68
+ # The on-demand pull model: every request registers @collector as its
69
+ # response handler on the wire's FIFO; @collector appends each routed
70
+ # message here, and fetch_response drains it. (The handler seam lets a
71
+ # streaming request register a record-routing handler instead.)
72
+ # The dedicated reader is the sole socket reader; it routes each reply
73
+ # to the handler the request registered on the wire's FIFO. Sync
74
+ # replies (RUN/BEGIN/COMMIT/RESET/ROUTE terminals) go to @collector,
75
+ # which pushes them onto @inbox — a blocking queue fetch_response pops.
76
+ # Streaming PULLs register a StreamHandler that fills a RecordBuffer
77
+ # instead. @inbox is a Thread::Queue: the reader pushes, the consumer
78
+ # pops, both colorless (yields under a Fiber scheduler).
79
+ @inbox = Thread::Queue.new
80
+ @collector = ResponseCollector.new(@inbox)
81
+ # LOGOFF/LOGON replies pipelined ahead of the next operation and not yet
82
+ # consumed (Optimization:AuthPipelining).
83
+ @pending_auth_acks = 0
84
+ # Guards the one-shot #on_close swap: close (caller thread) and
85
+ # mark_closed_broken (reader thread) can race, and firing the callback
86
+ # twice would double-decrement the routing SSR tally.
87
+ @on_close_mutex = Mutex.new
88
+ @recv_timeout = nil # server's connection.recv_timeout_seconds hint
89
+ @read_deadline = nil # monotonic bound for acquisition-phase reads
90
+ # Writes go behind a mutex: the reader writes watermark follow-up
91
+ # nothing — but the consumer writes (new query, next PULL, DISCARD)
92
+ # while the reader reads, so one guarded writer, never two readers.
93
+ @write_mutex = Mutex.new
94
+ # Dedicated reader: a Thread (per-connection lifetime) spawned lazily on
95
+ # the first request, parked on @reader_cv when nothing is in flight,
96
+ # stopped on close. Drives #advance and routes via the wire.
97
+ @reader = nil
98
+ @reader_mutex = Mutex.new
99
+ @reader_cv = ConditionVariable.new # wakes the reader: a reply is expected
100
+ @quiescent_cv = ConditionVariable.new # wakes drainers: in_flight hit 0
101
+ @reader_stopped = false
102
+ @broken_error = nil # set by failure fan-out; raised to inbox poppers
103
+ @server_version = nil
104
+ @bolt_version = nil
105
+ @protocol = nil
106
+ @server_agent = nil
107
+ @closed = false
108
+ @created_at = nil
109
+ @idle_since = nil
110
+ @discard_on_release = false
111
+ @auth_failed = false
112
+ @security_notified = false
113
+ @security_classification = nil
114
+ @session_scoped_auth = false
115
+ @auth_epoch = 0
116
+ end
117
+
118
+ def connect(deadline: nil)
119
+ last_error = nil
120
+ # One monotonic acquisition deadline for the whole connect, shared by
121
+ # every resolved-address attempt AND the handshake/HELLO reads: a
122
+ # server stalling the handshake — or a series of stalled addresses —
123
+ # can't collectively outlast the acquisition timeout. Each attempt
124
+ # gets only the *remaining* budget (open_socket / bounded reads). A
125
+ # total deadline (not a per-read timeout) so interleaved NOOP
126
+ # keepalives can't reset the clock. Cleared once the connection is
127
+ # ready and steady-state reads use the recv-timeout hint instead. A
128
+ # caller-supplied deadline (home-db optimistic acquire + fallback
129
+ # sharing one budget) wins over this connection's own.
130
+ @read_deadline = deadline || acquisition_deadline
131
+ resolved_addresses.each do |host, port|
132
+ open_socket(host, port)
133
+ perform_handshake
134
+ perform_hello
135
+ @read_deadline = nil
136
+ @created_at = current_monotonic
137
+ # Connection is READY: hand steady-state reads to the dedicated
138
+ # reader. (Handshake/hello above read synchronously via
139
+ # fetch_response, so a failed connect never spawns a reader.)
140
+ start_reader
141
+ return self
142
+ rescue Exceptions::AuthenticationException
143
+ # Auth is the same regardless of which address we hit — fail fast.
144
+ discard_socket
145
+ raise
146
+ rescue Exceptions::ServiceUnavailableException, IOError, SystemCallError => e
147
+ last_error = e
148
+ discard_socket
149
+ end
150
+
151
+ raise Exceptions::ServiceUnavailableException, 'No addresses to connect to' if last_error.nil?
152
+
153
+ # A Neo4jException (e.g. the handshake's ServiceUnavailable) is
154
+ # already classified — propagate as-is. A raw transport error
155
+ # (Errno::ECONNRESET when a plaintext client hits a TLS-only
156
+ # server, EOFError on a mid-handshake close, …) must be wrapped
157
+ # so callers see a DriverError rather than a bare SystemCallError.
158
+ # Without this, native MRI's C-OpenSSL leaks Errno::ECONNRESET and
159
+ # the testkit-backend reports a generic BackendError instead of a
160
+ # DriverError — the one TLS test where mri-on-jruby (whose Java
161
+ # socket layer surfaces a classified error) diverged from native
162
+ # mri (test_secure_server_explicitly_disabled_encryption).
163
+ raise last_error if last_error.is_a?(Exceptions::Neo4jException)
164
+
165
+ # Chain the original transport error as `cause` (this raise is
166
+ # outside the per-address rescue, so it's set explicitly rather
167
+ # than auto-populated from $!). Preserves the underlying failure
168
+ # and its backtrace behind the wrapper.
169
+ raise Exceptions::ServiceUnavailableException,
170
+ "Unable to connect to #{@address || @uri}: #{last_error.class}: #{last_error.message}",
171
+ cause: last_error
172
+ end
173
+
174
+ # Lightweight RESET-based liveness probe. Used by Bolt::Pool
175
+ # when an idle connection has been parked longer than the
176
+ # configured liveness threshold and we want to confirm it's
177
+ # still usable before handing it to a session. Any wire error
178
+ # OR a non-SUCCESS RESET response → return false; the pool
179
+ # discards and creates a fresh one.
180
+ # NOT reset! — that swallows errors so the original failure
181
+ # surfaces on the next user-driven call; here we want the
182
+ # probe itself to report the outcome. assert_success! is
183
+ # needed because fetch_response returns Message::Failure /
184
+ # Message::Ignored objects without raising — a "soft" RESET
185
+ # failure would otherwise leave the connection in the pool.
186
+ def alive?
187
+ return false if closed?
188
+
189
+ send_message(Message.reset)
190
+ flush
191
+ drain_quiesced.each(&:assert_success!)
192
+ true
193
+ rescue StandardError
194
+ discard_socket
195
+ @closed = true
196
+ fire_on_close
197
+ false
198
+ end
199
+
200
+ # Cheap, non-blocking "did the peer go away?" check the pool runs before
201
+ # reusing an idle pooled connection. Unlike alive?, no RESET round-trip:
202
+ # a clean idle connection has nothing to read, so one non-blocking read
203
+ # returns :wait_readable and we're done. A server that closed the idle
204
+ # connection (e.g. a router that served a table then EXITed —
205
+ # test_should_successfully_acquire_rt_when_router_ip_changes) shows up as
206
+ # EOF here, so the pool discards it and the next acquire re-resolves and
207
+ # reconnects. NOOP keepalives are drained harmlessly. This is the
208
+ # threaded equivalent of the reactor's background reader noticing an
209
+ # idle close — without a reader per parked connection.
210
+ def broken?
211
+ return true if closed?
212
+
213
+ loop do
214
+ case (chunk = @socket.read_nonblock(READ_CHUNK, exception: false))
215
+ when :wait_readable, :wait_writable
216
+ return false # nothing pending → healthy
217
+ when nil
218
+ mark_closed_broken
219
+ return true # peer closed
220
+ else
221
+ @wire.receive(chunk) # NOOP / stray bytes — drain and re-check
222
+ end
223
+ end
224
+ rescue IOError, SystemCallError
225
+ mark_closed_broken
226
+ true
227
+ end
228
+
229
+ # Monotonic seconds — immune to wall-clock jumps, which is what every
230
+ # age / idle calculation here needs. Through the Clock seam so
231
+ # Backend:MockTime can freeze/advance it.
232
+ def current_monotonic
233
+ @clock.monotonic
234
+ end
235
+
236
+ def close
237
+ return if @closed
238
+
239
+ @closed = true
240
+ fire_on_close
241
+ # Best-effort GOODBYE before we tear down. Frame+write directly rather
242
+ # than via send_message (which would re-arm the reader) / fetch (GOODBYE
243
+ # has no reply). Then stop the reader and close the socket.
244
+ begin
245
+ @wire&.enqueue(Message.goodbye, @collector)
246
+ bytes = @wire&.take_outbound
247
+ if bytes && !bytes.empty?
248
+ @write_mutex.synchronize do
249
+ @socket.write(bytes)
250
+ @socket.flush
251
+ end
252
+ end
253
+ rescue StandardError
254
+ # closing anyway
255
+ end
256
+ stop_reader
257
+ begin
258
+ @socket&.close
259
+ rescue StandardError
260
+ nil
261
+ end
262
+ end
263
+
264
+ def closed?
265
+ @closed || @socket&.closed?
266
+ end
267
+
268
+ # Current auth identity (set by HELLO/LOGON, updated by
269
+ # `authenticate`) and the driver's stored identity (set once at
270
+ # construction). Sessions read driver_auth as the "no per-
271
+ # session :auth was given" default — calling
272
+ # authenticate(driver_auth) on every acquire makes the pool's
273
+ # auth-bleed problem disappear without needing connection-pin
274
+ # bookkeeping.
275
+ attr_reader :auth, :driver_auth
276
+
277
+ # Whether the server advertised `ssr.enabled` in its HELLO hints
278
+ # (Bolt 5.8+ server-side routing). Gates the optimistic home-db cache.
279
+ def ssr_enabled? = @ssr_enabled == true
280
+
281
+ # Fired once, the first time this connection tears down (clean GOODBYE,
282
+ # broken read, or a failed RESET). The routing provider uses it to keep
283
+ # its pool-wide SSR tally current. nil for the direct provider.
284
+ attr_reader :on_close
285
+
286
+ # Register the teardown callback. If the connection already tore down
287
+ # (the reader could break it between build and this assignment), fire it
288
+ # right away so the SSR tally can't miss this connection's decrement.
289
+ def on_close=(callback)
290
+ @on_close_mutex.synchronize { @on_close = callback }
291
+ fire_on_close if closed?
292
+ end
293
+
294
+ # The auth "generation" this connection last authenticated at.
295
+ # The provider bumps its own counter on an AuthorizationExpired
296
+ # failure (the server invalidated its authorization cache for every
297
+ # connection of this identity); a pooled connection authed at an
298
+ # older generation must re-authenticate on next acquire even though
299
+ # its token is unchanged. Set by the provider's connect_factory /
300
+ # ensure_identity.
301
+ attr_accessor :auth_epoch
302
+
303
+ # Bolt 5.1+ re-auth: LOGOFF then LOGON with `new_auth`. Used by
304
+ # Session when it has its own `:auth` and the pooled connection
305
+ # is currently authenticated as somebody else. No-op when the
306
+ # connection already holds the target identity — unless `force`
307
+ # (an AuthorizationExpired-driven refresh re-auths to the *same*
308
+ # token to refresh the server's authorization cache).
309
+ def authenticate(new_auth, force: false, pipelined: true)
310
+ return if !force && @auth == new_auth
311
+ unless @protocol&.supports_re_auth?
312
+ raise Exceptions::UnsupportedFeatureException,
313
+ "Per-session auth requires Bolt 5.1+; negotiated #{@bolt_version}"
314
+ end
315
+
316
+ send_message(Message.logoff)
317
+ send_message(Message.logon(new_auth || {}))
318
+ @auth = new_auth
319
+ # AuthPipelining: enqueue LOGOFF + LOGON but don't flush or read their
320
+ # replies — they ride out with the next operation's messages and are
321
+ # consumed (via #drain_pending_auth_acks, from the next #fetch_response)
322
+ # just before that operation reads its own reply, saving a round-trip.
323
+ # A rejected LOGON surfaces there as the auth failure (the operation's
324
+ # own message is IGNORED). @auth is set optimistically; a failed re-auth
325
+ # discards the connection, so a stale value never gets reused.
326
+ #
327
+ # pipelined: false forces the synchronous round-trip — verify_authentication
328
+ # re-auths then discards the connection with no operation to carry (and
329
+ # drain) the replies, and must see the LOGON's success/failure itself.
330
+ if pipelined
331
+ @pending_auth_acks += 2
332
+ else
333
+ flush
334
+ fetch_response.assert_success!
335
+ fetch_response.assert_success!
336
+ end
337
+ end
338
+
339
+ # True when the connection's current identity came from a per-session
340
+ # auth token rather than the auth-token manager's default. The manager
341
+ # didn't issue that token, so a security failure on such a connection
342
+ # must NOT be reported to it (testkit's get_auth contract:
343
+ # handle_security_exception_count stays 0 for session-scoped auth).
344
+ # Set by the provider's ensure_identity on every acquire so it tracks
345
+ # the current lessee of a reused pooled connection.
346
+ attr_accessor :session_scoped_auth
347
+
348
+ # Provider-set callback (token, error) -> Boolean: feeds a
349
+ # security failure back to the auth-token manager so it can
350
+ # invalidate / refresh the token. nil for drivers built without a
351
+ # managed manager (the static case never invalidates).
352
+ attr_accessor :security_exception_handler
353
+
354
+ # Report a security failure to the auth-token manager (if any) and
355
+ # let it decide retryability. Every operation site funnels errors
356
+ # through classify_failure, so this is the single notification
357
+ # point. When the manager deems the failure retryable (it has
358
+ # invalidated the token so the next acquire re-fetches), surface a
359
+ # SecurityRetryableException wrapping the original (code/message
360
+ # preserved, original chained as cause when the caller re-raises)
361
+ # — mirrors Java, and the testkit reports it as retryable.
362
+ def notify_security_exception(error)
363
+ return error unless error.is_a?(Exceptions::SecurityException)
364
+
365
+ # A security failure means this connection can't be reused — drop
366
+ # it from the pool either way.
367
+ @discard_on_release = true
368
+
369
+ # AuthorizationExpired is the server's authorization-cache expiry,
370
+ # not a closed socket — the connection stays usable, so RESET is
371
+ # fine. Unauthorized / TokenExpired close the connection server-
372
+ # side, so skip RESET there.
373
+ @auth_failed = true unless error.is_a?(Exceptions::AuthorizationExpiredException)
374
+
375
+ # Notify the auth-token manager at most once per connection. The
376
+ # same failure is classified again as it propagates (result
377
+ # streaming on_failure, then the tx rollback re-consuming the
378
+ # failed result), so without this guard the manager's
379
+ # handle_security_exception fires twice. We also cache the
380
+ # classified result and return it on the repeat calls, so the
381
+ # retryability/type stays stable — the first call may upgrade to
382
+ # SecurityRetryableException, and a later call must not downgrade
383
+ # back to the raw error. The connection is discarded after a
384
+ # security failure, so neither the flag nor the cache needs
385
+ # clearing.
386
+ return @security_classification if @security_notified
387
+
388
+ @security_notified = true
389
+ # Always run the provider handler — it performs provider-side work
390
+ # that must happen regardless of who owns the token, notably bumping
391
+ # the auth epoch on AuthorizationExpired so SIBLING pooled
392
+ # connections re-authenticate. `session_scoped_auth` is passed so the
393
+ # handler can skip the auth-token-MANAGER notification for a
394
+ # per-session identity (the manager didn't issue that token, so its
395
+ # handle_security_exception_count must stay 0). A retryable verdict
396
+ # surfaces a SecurityRetryableException wrapping the original
397
+ # (code/message preserved, original chained as cause at re-raise);
398
+ # the handler returns false for session-scoped auth, so no upgrade.
399
+ @security_classification =
400
+ if @security_exception_handler&.call(@auth, error, @session_scoped_auth)
401
+ Exceptions::SecurityRetryableException.new(error.message, code: error.code)
402
+ else
403
+ error
404
+ end
405
+ end
406
+
407
+ # No-op routing classifier for the direct (bolt://) path — there's
408
+ # no routing table to feed back. Still funnels through the auth
409
+ # manager. Routing::RoutedConnection overrides with the real
410
+ # routing classification (and also notifies). Defined here so
411
+ # session.rb / transaction.rb / Result#on_failure can call
412
+ # `connection.classify_failure(e)` unconditionally.
413
+ def classify_failure(error) = notify_security_exception(error)
414
+
415
+ # Frame the message into the wire's outbound buffer and count it as
416
+ # in-flight. Nothing hits the socket until #flush — so several
417
+ # send_messages before a flush pipeline naturally (HELLO+LOGON,
418
+ # RUN+PULL), which is the whole point.
419
+ #
420
+ # A dead/closed connection raises a classified Neo4jException, not a raw
421
+ # IOError: the cleanup and retry paths (Transaction#rollback, reset!,
422
+ # the managed-tx retry) rescue Neo4jException, so a bare IOError would
423
+ # escape them and surface as an unhandled error.
424
+ # Register a request on the wire's FIFO with the handler that will route
425
+ # its reply: @collector (→ @inbox) for sync requests, or a StreamHandler
426
+ # (→ a RecordBuffer) for a streaming PULL. The dedicated reader (started
427
+ # once the connection is READY) delivers it; during the acquisition phase
428
+ # (handshake/hello, before the reader exists) fetch_response drives the
429
+ # reads synchronously.
430
+ def send_message(message, handler = @collector)
431
+ raise Exceptions::ServiceUnavailableException, "Connection to #{@address || @uri} is closed" if closed?
432
+
433
+ @wire.enqueue(message, handler)
434
+ end
435
+
436
+ def send_all(*messages)
437
+ messages.each { |msg| send_message(msg) }
438
+ flush
439
+ end
440
+
441
+ # Enqueue a TELEMETRY report for the API about to open a tx/query, unless
442
+ # the caller disabled it, the server didn't advertise telemetry, or the
443
+ # negotiated protocol predates it (5.4). Returns whether one was sent so
444
+ # the caller reads its (extra, pipelined) SUCCESS before the op's reply.
445
+ def telemetry(api, disabled:)
446
+ return false if api.nil? || disabled || !@telemetry_enabled || !@protocol.supports_telemetry?
447
+
448
+ send_message(@protocol.build_telemetry(api))
449
+ true
450
+ end
451
+
452
+ # Fetch the cluster routing table. Bolt 4.3+ uses the dedicated
453
+ # ROUTE message; older versions have no ROUTE and call a
454
+ # server-side procedure instead (route_via_procedure). Either
455
+ # way the return is the `{ttl:, servers:}` map the caller wraps
456
+ # in Routing::RoutingTable.from_response.
457
+ def route(database: nil, bookmarks: [], imp_user: nil, routing_context: {})
458
+ # Enforce impersonation support before touching the wire: a
459
+ # routed session impersonating against a pre-4.4 cluster must
460
+ # fail (ClientException) rather than silently drop imp_user from
461
+ # the discovery call. Raised here — outside the wire-error
462
+ # begin/rescue — so the still-clean connection isn't RESET.
463
+ @protocol.enforce_impersonation_support!(imp_user)
464
+
465
+ return route_via_procedure(database, bookmarks, routing_context) if @bolt_version < BoltVersion::V4_3
466
+
467
+ begin
468
+ # ROUTE's 3rd field changed at 4.4: 4.3 sends the bare database
469
+ # name (string/null), 4.4+ a `{db, imp_user}` map. The protocol
470
+ # handler owns that shape. The acquisition timeout must encompass
471
+ # discovery, so bound the ROUTE read by the deadline (cleared after).
472
+ @read_deadline = acquisition_deadline
473
+ send_message(@protocol.build_route(routing_context, Array(bookmarks), database, imp_user))
474
+ flush
475
+
476
+ fetch_response.assert_success!.metadata[:rt]
477
+ rescue Exceptions::Neo4jException
478
+ # ROUTE failure leaves the server in FAILED state — RESET clears it
479
+ # so the connection can be reused.
480
+ reset!
481
+ raise
482
+ ensure
483
+ @read_deadline = nil
484
+ end
485
+ end
486
+
487
+ # Pre-4.3 routing: there is no ROUTE message, so fetch the table
488
+ # by calling the server-side procedure and shaping its single
489
+ # row ([ttl, servers]) into the same map ROUTE would return.
490
+ # Bolt 3.0: CALL dbms.cluster.routing.getRoutingTable($context)
491
+ # on the home database (single-DB protocol).
492
+ # Bolt 4.0-4.2: CALL dbms.routing.getRoutingTable($context, $database)
493
+ # run against the `system` database.
494
+ def route_via_procedure(database, bookmarks, routing_context)
495
+ if @bolt_version >= BoltVersion::V4_0
496
+ # 4.0-4.2: dbms.routing.getRoutingTable run against `system`.
497
+ # Pass $database only when a target db is named — the home-db
498
+ # case uses the single-arg form (matches the server procedure
499
+ # overloads the stub scripts pin).
500
+ if database
501
+ query = 'CALL dbms.routing.getRoutingTable($context, $database)'
502
+ params = { context: routing_context, database: database }
503
+ else
504
+ query = 'CALL dbms.routing.getRoutingTable($context)'
505
+ params = { context: routing_context }
506
+ end
507
+ extra = { db: 'system', mode: 'r' }
508
+ # 4.0-4.2 runs the procedure against `system`, which accepts
509
+ # bookmarks (causal consistency for a freshly-created database).
510
+ extra[:bookmarks] = Array(bookmarks) unless Array(bookmarks).empty?
511
+ else
512
+ # 3.0: single-database cluster routing procedure, home db. No
513
+ # system db and no bookmark-aware routing (that arrived with the
514
+ # 4.3 ROUTE message), so the discovery RUN carries only `mode`.
515
+ query = 'CALL dbms.cluster.routing.getRoutingTable($context)'
516
+ params = { context: routing_context }
517
+ extra = { mode: 'r' }
518
+ end
519
+
520
+ send_message(@protocol.build_run(query, params, extra))
521
+ send_message(@protocol.build_pull(n: -1))
522
+ flush
523
+
524
+ summary = fetch_response.assert_success!
525
+ fields = summary.metadata[:fields] || summary.metadata['fields'] || []
526
+ row = nil
527
+ loop do
528
+ response = fetch_response
529
+ case response
530
+ when Message::Success then break # PULL summary — end of stream
531
+ when Message::Record then row ||= fields.zip(response.fields).to_h
532
+ else response.assert_success! # FAILURE / IGNORED — raises
533
+ end
534
+ end
535
+
536
+ unless row
537
+ raise Exceptions::ServiceUnavailableException,
538
+ "Routing procedure on #{@address || @uri} returned no rows"
539
+ end
540
+
541
+ { ttl: row['ttl'], servers: row['servers'] }
542
+ rescue Exceptions::Neo4jException
543
+ reset!
544
+ raise
545
+ end
546
+
547
+ # Defer peer-closed errors from flush so a buffered server
548
+ # response (e.g. a final FAILURE) gets read before we raise.
549
+ # Under JRuby the peer-closed state surfaces eagerly on the
550
+ # very next write/flush; raising here would swallow the
551
+ # FAILURE bytes already in the receive buffer — the
552
+ # test_should_error_on_database_shutdown_using_tx_run stub
553
+ # regression. Every normal request/response cycle pairs flush
554
+ # with a fetch_response (Transaction#run/commit/rollback,
555
+ # Result streaming, Connection#route), so a peer-gone state
556
+ # with nothing buffered still surfaces as
557
+ # ServiceUnavailableException — just from the read side.
558
+ # Connection#close also calls flush but discards exceptions
559
+ # itself (`flush rescue nil`), so it does not need the pair.
560
+ # Non-peer-closed wire errors (e.g. a timed-out write on a
561
+ # socket that has SO_SNDTIMEO set, or EBADF on a
562
+ # closed-out-from-under-us fd) are NOT silenced — they fall
563
+ # through and propagate. We do not set SO_SNDTIMEO and the fd
564
+ # is owned by us, so these are improbable in practice.
565
+ # Drain the wire's outbound buffer to the socket. Writes are mutex-
566
+ # guarded so a future prefetch reader and the consumer's writes never
567
+ # interleave on one socket. Peer-closed errors are deferred (not raised)
568
+ # so a server FAILURE buffered before the close is still read by the
569
+ # paired fetch_response — every request/response cycle pairs flush with
570
+ # a fetch (Transaction#run/commit/rollback, Result streaming, #route),
571
+ # so a genuinely-gone peer still surfaces as ServiceUnavailable from the
572
+ # read side. #close flushes with `rescue nil`, so it needs no pair.
573
+ def flush
574
+ bytes = @wire.take_outbound
575
+ return if bytes.empty?
576
+
577
+ begin
578
+ @write_mutex.synchronize do
579
+ @socket.write(bytes)
580
+ @socket.flush
581
+ end
582
+ rescue Errno::EPIPE, Errno::ECONNRESET, IOError
583
+ # Peer-closed (EPIPE/ECONNRESET) or the reader closed the socket
584
+ # out from under this write mid-flight (IOError "stream closed in
585
+ # another thread", EBADF). Deferred (not raised) so a server FAILURE
586
+ # buffered before the close is still read by the paired
587
+ # fetch_response — see method comment.
588
+ ensure
589
+ # Always wake the reader, even on a failed write: a reply may be
590
+ # expected, OR the socket is dead and the reader must run #advance to
591
+ # hit EOF and fan the failure out — otherwise a parked reader never
592
+ # discovers the break and a drainer in #wait_quiescent hangs.
593
+ wake_reader
594
+ end
595
+ end
596
+
597
+ # Return the next sync reply in request order. The dedicated reader fills
598
+ # @inbox (a blocking queue); this pops it, blocking colorlessly until the
599
+ # reader delivers. On a connection failure the reader closes @inbox and
600
+ # records @broken_error, so a blocked pop wakes with nil and re-raises the
601
+ # classified error rather than hanging.
602
+ def fetch_response
603
+ # A pipelined re-auth's LOGOFF/LOGON replies sit ahead of this
604
+ # operation's own reply — consume them first (AuthPipelining).
605
+ drain_pending_auth_acks
606
+ pop_inbox
607
+ end
608
+
609
+ # Pop the next sync reply. Acquisition phase (no reader yet): drive the
610
+ # reads ourselves. Steady state: the reader fills @inbox; block on pop
611
+ # until it delivers. On a connection failure the reader closes @inbox and
612
+ # records @broken_error, so a blocked pop wakes with nil and re-raises.
613
+ def pop_inbox
614
+ advance while @reader.nil? && @inbox.empty?
615
+ message = @inbox.pop
616
+ raise @broken_error if message.nil? && @broken_error
617
+
618
+ message
619
+ end
620
+
621
+ # Consume the replies to a pipelined re-auth's LOGOFF + LOGON before the
622
+ # next operation reads its own reply. A rejected LOGON raises the auth
623
+ # failure here (its follow-on messages come back IGNORED).
624
+ def drain_pending_auth_acks
625
+ return if @pending_auth_acks.zero?
626
+
627
+ pending = @pending_auth_acks
628
+ @pending_auth_acks = 0
629
+ pending.times { pop_inbox.assert_success! }
630
+ end
631
+
632
+ def fetch_all
633
+ drain_quiesced
634
+ end
635
+
636
+ # Recover from a FAILED server state. Sends RESET and drains all pending
637
+ # responses (including any IGNOREDs from messages queued before the
638
+ # failure — those routed to their handlers; this drains the sync @inbox).
639
+ # Returns once the server has acknowledged the RESET and the connection is
640
+ # quiescent.
641
+ def reset!(propagate: false)
642
+ send_message(Message.reset)
643
+ flush
644
+ messages = drain_quiesced
645
+ # When propagating (verify_connectivity, pool-return) the RESET is a
646
+ # real check: a server FAILURE reply (not just a dead socket) must
647
+ # surface too, so assert success on the drained responses.
648
+ messages.each(&:assert_success!) if propagate
649
+ rescue StandardError
650
+ # If RESET itself fails the connection is likely dead. Recovery paths
651
+ # (`propagate: false`, the default) swallow so they don't mask the
652
+ # original error and the caller discovers the break on next use.
653
+ # verify_connectivity passes `propagate: true`: the RESET *is* the
654
+ # probe, so a failure must surface (and the dead connection be
655
+ # discarded) rather than report false success.
656
+ raise if propagate
657
+ ensure
658
+ # RESET flushed and drained any pipelined re-auth replies with it.
659
+ @pending_auth_acks = 0
660
+ end
661
+
662
+ # True while the caller still owes a fetch: a request whose terminal the
663
+ # wire hasn't seen yet (in_flight), or a message already routed to the
664
+ # inbox but not yet popped. The drain loops spin on this.
665
+ def pending_responses?
666
+ @wire.in_flight.positive? || !@inbox.empty?
667
+ end
668
+
669
+ private
670
+
671
+ def discard_socket
672
+ stop_reader
673
+ begin
674
+ @socket&.close
675
+ rescue StandardError
676
+ nil
677
+ end
678
+ @socket = nil
679
+ # Reset all per-attempt I/O state so a retry on the next address (or a
680
+ # later reset!/drain loop) doesn't carry a phantom in-flight request or
681
+ # half-parsed message forward. @wire (with its handler FIFO) is rebuilt
682
+ # by perform_handshake. A fresh @inbox (the old one may be closed by a
683
+ # fan-out) and cleared reader/broken state so a retry can re-arm the
684
+ # reader. Crucially clear @closed too: a fail_broken on one address set
685
+ # it, and connect() retries the next address with a fresh socket —
686
+ # without this, send_message there would wrongly raise "Connection is
687
+ # closed" and break address failover.
688
+ @wire = nil
689
+ @inbox = Thread::Queue.new
690
+ # Rebind the collector to the fresh inbox — it captured the old queue
691
+ # at init, so without this a retry's sync replies would land in the
692
+ # discarded queue while fetch_response waits on the new one.
693
+ @collector = ResponseCollector.new(@inbox)
694
+ @reader_stopped = false
695
+ @broken_error = nil
696
+ @closed = false
697
+ end
698
+
699
+ # One colorless pump step: pull whatever bytes are available off the
700
+ # socket and feed them to the wire, which routes any decoded messages to
701
+ # the front handler (today: @collector → @inbox). A step may land several
702
+ # messages, one, or none (a partial chunk or a NOOP keepalive) — callers
703
+ # loop until what they need has arrived (fetch_response: @inbox non-empty).
704
+ #
705
+ # read_nonblock(exception: false) returns the bytes, :wait_readable/
706
+ # :wait_writable when it would block, or nil on EOF. The explicit wait
707
+ # honors the recv timeout (readpartial/read ignore IO#timeout for partial
708
+ # reads) and yields under a Fiber scheduler (wait_readable hooks io_wait),
709
+ # so the pump is colorless. The dedicated reader loops this.
710
+ def advance
711
+ case (chunk = @socket.read_nonblock(READ_CHUNK, exception: false))
712
+ when :wait_readable
713
+ @socket.wait_readable(current_read_timeout) or fail_broken(read_timeout_error)
714
+ when :wait_writable # SSL renegotiation mid-read
715
+ @socket.wait_writable(current_read_timeout) or fail_broken(read_timeout_error)
716
+ when nil
717
+ raise EOFError, 'end of file reached'
718
+ else
719
+ @wire.receive(chunk)
720
+ end
721
+ rescue IOError, SystemCallError => e
722
+ fail_broken(Exceptions::ServiceUnavailableException.new(
723
+ "Connection to #{@address || @uri} broken: #{e.class}: #{e.message}"
724
+ ))
725
+ end
726
+
727
+ # The dedicated reader: the sole socket reader for this connection's
728
+ # lifetime. It advances (reads + routes via the wire) while replies are
729
+ # in flight, and parks on @reader_cv when none are — woken by #flush when
730
+ # a request is sent, or by #stop_reader on close. Any read failure is
731
+ # fanned out to every waiter (see #fan_out). A plain Thread: it does
732
+ # blocking I/O and feeds colorless queues/buffers, so a consumer fiber
733
+ # under a host scheduler still yields on pop/shift. (The reactor-native
734
+ # fiber reader + per-active-window lifetime is a later step.)
735
+ def reader_loop
736
+ until @reader_stopped
737
+ @reader_mutex.synchronize do
738
+ # in_flight == 0 ⇒ all expected replies are read: the connection is
739
+ # quiescent. Wake any drainer (reset!/fetch_all/alive?) before we park.
740
+ @quiescent_cv.broadcast if @wire.in_flight.zero?
741
+ @reader_cv.wait(@reader_mutex) while !@reader_stopped && @wire.in_flight.zero?
742
+ end
743
+ # Woken to read — unless we were woken to stop (don't advance on the
744
+ # socket #stop_reader just closed; that would fan out a phantom error).
745
+ advance unless @reader_stopped
746
+ end
747
+ rescue StandardError => e
748
+ fan_out(e)
749
+ end
750
+
751
+ # Block until the connection is quiescent (the reader has read every
752
+ # in-flight reply, wherever it routed — @inbox or a stream buffer) or it
753
+ # broke. The drain loops use this instead of racing on in_flight.
754
+ def wait_quiescent
755
+ @reader_mutex.synchronize do
756
+ @quiescent_cv.wait(@reader_mutex) until @wire.in_flight.zero? || @reader_stopped || @broken_error
757
+ end
758
+ end
759
+
760
+ # Wait for quiescence, then pop every sync reply sitting in @inbox
761
+ # (non-blocking). Stream replies went to their buffers, not here.
762
+ def drain_quiesced
763
+ wait_quiescent
764
+ raise @broken_error if @broken_error
765
+
766
+ messages = []
767
+ loop { messages << @inbox.pop(true) }
768
+ rescue ThreadError, ClosedQueueError
769
+ messages || []
770
+ end
771
+
772
+ # Start the dedicated reader once the connection is READY (called at the
773
+ # end of connect). Idempotent; never re-armed once stopped.
774
+ def start_reader
775
+ return if @reader || @reader_stopped
776
+
777
+ @reader = Thread.new { reader_loop }
778
+ end
779
+
780
+ # Wake a parked reader: a reply is now expected (a request was flushed).
781
+ def wake_reader
782
+ @reader_mutex.synchronize { @reader_cv.broadcast }
783
+ end
784
+
785
+ # Stop the reader and wait for it to exit. Closing the socket unblocks a
786
+ # reader parked in advance's wait_readable; the stopped flag + broadcast
787
+ # unblocks one parked on @reader_cv.
788
+ def stop_reader
789
+ reader = @reader
790
+ @reader = nil
791
+ # Wake the parked reader (@reader_cv) and any drainer blocked in
792
+ # #wait_quiescent (@quiescent_cv) — stopping is a terminal transition
793
+ # they must observe, else a concurrent reset!/fetch_all/alive? hangs.
794
+ @reader_mutex.synchronize do
795
+ @reader_stopped = true
796
+ @reader_cv.broadcast
797
+ @quiescent_cv.broadcast
798
+ end
799
+ return unless reader
800
+
801
+ begin
802
+ @socket&.close
803
+ rescue StandardError
804
+ nil
805
+ end
806
+ reader.join unless reader == Thread.current
807
+ end
808
+
809
+ # Failure fan-out: a dead/timed-out connection must wake every waiter,
810
+ # not just the front one. Record the classified error, close @inbox so
811
+ # sync poppers (fetch_response) return nil → re-raise it, fail each
812
+ # outstanding stream buffer (via its handler) so a cursor parked in
813
+ # buffer.await wakes and re-raises too, and broadcast @quiescent_cv so a drainer
814
+ # parked in #wait_quiescent (reset!/fetch_all/alive?) wakes on the
815
+ # @broken_error condition instead of waiting forever for an in-flight
816
+ # reply that will never arrive. Idempotent; may run on the reader thread
817
+ # (reader_loop rescue) or the consumer thread (flush write failure).
818
+ def fan_out(error)
819
+ @broken_error ||= error
820
+ @inbox.close
821
+ @wire&.fail_pending(error)
822
+ mark_closed_broken
823
+ @reader_mutex.synchronize { @quiescent_cv.broadcast }
824
+ end
825
+
826
+ # A read timeout or wire error means this connection is unusable: hang
827
+ # up (so the peer sees the disconnect — the recv-timeout contract
828
+ # asserts the driver hangs up a timed-out connection) and mark it closed
829
+ # so the pool discards it on the next acquire rather than reusing a
830
+ # broken connection. Then raise the classified error.
831
+ def fail_broken(error)
832
+ mark_closed_broken
833
+ raise error
834
+ end
835
+
836
+ def mark_closed_broken
837
+ begin
838
+ @socket&.close
839
+ rescue StandardError
840
+ nil
841
+ end
842
+ @closed = true
843
+ fire_on_close
844
+ end
845
+
846
+ # Invoke the teardown callback exactly once (nilled after firing, so the
847
+ # several @closed transitions can each call it safely).
848
+ def fire_on_close
849
+ # Atomically claim the callback so concurrent teardown paths (close vs
850
+ # the reader's mark_closed_broken) fire it exactly once; call outside
851
+ # the lock — the callback re-enters the load balancer's SSR mutex.
852
+ callback = @on_close_mutex.synchronize do
853
+ cb = @on_close
854
+ @on_close = nil
855
+ cb
856
+ end
857
+ callback&.call(self)
858
+ end
859
+
860
+ # The timeout the next read may take. During acquisition (handshake,
861
+ # ROUTE) it's the remaining budget of the total deadline; in steady
862
+ # state it's the server's recv-timeout hint (nil = block indefinitely).
863
+ def current_read_timeout
864
+ return [@read_deadline - current_monotonic, 0.001].max if @read_deadline
865
+
866
+ @recv_timeout
867
+ end
868
+
869
+ # A read timeout means different things in different phases: during
870
+ # acquisition the connection-acquisition budget was exceeded (a generic
871
+ # ServiceUnavailable); in steady state the server breached its own
872
+ # recv-timeout hint (the specific ConnectionReadTimeoutException, which
873
+ # routing turns into server eviction). Fresh instance per failure.
874
+ def read_timeout_error
875
+ if @read_deadline
876
+ Exceptions::ServiceUnavailableException.new(
877
+ "Timed out acquiring a connection to #{@address || @uri} within the acquisition timeout"
878
+ )
879
+ else
880
+ Exceptions::ConnectionReadTimeoutException.new(
881
+ 'Connection read timed out due to it taking longer than the server-supplied timeout value via configuration hint.'
882
+ )
883
+ end
884
+ end
885
+
886
+ # Monotonic deadline from the connection-acquisition timeout (nil when
887
+ # unconfigured). Bounds the handshake and ROUTE reads so a stalled
888
+ # server can't outlast the acquisition budget.
889
+ def acquisition_deadline
890
+ acq = @options[:connection_acquisition_timeout]&.to_f
891
+ acq && current_monotonic + acq
892
+ end
893
+
894
+ # Seconds left until the shared acquisition deadline (@read_deadline),
895
+ # clamped at 0; nil when the acquisition timeout is unconfigured. Used
896
+ # to give each connect attempt only the remaining budget.
897
+ def remaining_read_budget
898
+ @read_deadline && [@read_deadline - current_monotonic, 0.0].max
899
+ end
900
+
901
+ # Resolve the URI's host:port into a list of [host, port] pairs to try
902
+ # in order. Hosts are kept in their native form — IPv6 stays bracketed
903
+ # ("[::1]") so address strings re-parse unambiguously; brackets are
904
+ # only stripped at the TCPSocket boundary.
905
+ # With a `domain_name_resolver` (Java's DomainNameResolver), the
906
+ # callable receives the hostname and returns one or more IPs, each
907
+ # paired with the original port. The custom *address* resolver
908
+ # (ServerAddressResolver) is a separate, routing-only concern handled
909
+ # by the LoadBalancer, not here.
910
+ def resolved_addresses
911
+ host = @uri.host
912
+ port = @uri.port || DEFAULT_PORT
913
+
914
+ # Domain-name resolution (hostname -> one or more IPs) happens at
915
+ # connect time, on every connection. The custom *address* resolver
916
+ # (ServerAddressResolver) is a separate, routing-only concern that
917
+ # expands the seed into initial routers — applied by the
918
+ # LoadBalancer, not here (Java draws the same line).
919
+ if @domain_name_resolver
920
+ Array(@domain_name_resolver.call(host)).map { |ip| [ip.to_s, port] }
921
+ else
922
+ [[host, port]]
923
+ end
924
+ end
925
+
926
+ def split_addr(addr, default_port)
927
+ # rpartition handles IPv6: "[::1]:7687" -> ["[::1]", ":", "7687"]
928
+ host, sep, port = addr.to_s.rpartition(':')
929
+ sep.empty? ? [addr.to_s, default_port] : [host, Integer(port)]
930
+ end
931
+
932
+ def format_address(host, port)
933
+ # Wrap raw IPv6 (`::1`) in brackets so the result re-parses correctly.
934
+ host = "[#{host}]" if host.include?(':') && !host.start_with?('[')
935
+ "#{host}:#{port}"
936
+ end
937
+
938
+ def strip_brackets(host)
939
+ host&.start_with?('[') && host.end_with?(']') ? host[1..-2] : host
940
+ end
941
+
942
+ def open_socket(host, port)
943
+ timeout = @options[:connection_timeout]
944
+ bare_host = strip_brackets(host)
945
+ # Bound the TCP connect by the smaller of the connection timeout and
946
+ # the REMAINING acquisition budget (the shared @read_deadline), so
947
+ # acquisition_timeout caps a connect to a non-responsive/non-routable
948
+ # address — not just the handshake reads — and retries across
949
+ # addresses can't collectively exceed it (testkit
950
+ # test_should_fail_when_acquisition_timeout_is_reached_first, where
951
+ # acquisition 2s < connection 720s).
952
+ connect_timeout = [timeout&.to_f, remaining_read_budget].compact.min
953
+ tcp_socket = if connect_timeout
954
+ Socket.tcp(bare_host, port,
955
+ connect_timeout: connect_timeout)
956
+ else
957
+ TCPSocket.new(bare_host, port)
958
+ end
959
+ rescue SystemCallError, SocketError => e
960
+ raise Exceptions::ServiceUnavailableException,
961
+ "Unable to connect to #{format_address(host,
962
+ port)}, ensure the database is running and that there is a working network connection to it. (#{e.message})"
963
+ else
964
+ @address = format_address(host, port)
965
+ tcp_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
966
+
967
+ if timeout
968
+ timeval = [timeout, 0].pack('l_2')
969
+ tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval)
970
+ tcp_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeval)
971
+ end
972
+
973
+ @socket = wrap_with_tls(tcp_socket, bare_host, port)
974
+ end
975
+
976
+ # When the URI uses a +s/+ssc scheme (or :encryption is set
977
+ # explicitly), wrap the TCP socket in an OpenSSL::SSL::SSLSocket
978
+ # before returning. Errors during the TLS handshake — peer
979
+ # certificate refused, hostname mismatch, server doesn't speak
980
+ # TLS — turn into Neo4j-shaped exceptions so the caller doesn't
981
+ # have to know whether the wire is encrypted.
982
+ def wrap_with_tls(tcp_socket, hostname, port)
983
+ tls = TlsConfig.new(uri: @uri, options: @options)
984
+ ctx = tls.ssl_context
985
+ return tcp_socket unless ctx
986
+
987
+ ssl = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
988
+ ssl.sync_close = true
989
+ ssl.hostname = hostname # SNI
990
+ ssl.connect
991
+ ssl.post_connection_check(hostname) if tls.verify_hostname?
992
+ ssl
993
+ rescue OpenSSL::SSL::SSLError => e
994
+ begin
995
+ tcp_socket.close
996
+ rescue StandardError
997
+ nil
998
+ end
999
+ raise Exceptions::SecurityException,
1000
+ "TLS handshake to #{format_address(hostname, port)} failed: #{e.message}"
1001
+ rescue SystemCallError, IOError => e
1002
+ begin
1003
+ tcp_socket.close
1004
+ rescue StandardError
1005
+ nil
1006
+ end
1007
+ raise Exceptions::ServiceUnavailableException,
1008
+ "Connection lost during TLS handshake to #{format_address(hostname, port)}: #{e.message}"
1009
+ end
1010
+
1011
+ def perform_handshake
1012
+ # Bound the (raw, pre-wire) version negotiation by the acquisition
1013
+ # deadline too, so a server that stalls the magic-byte exchange can't
1014
+ # outlast it. Handshake reads via wait_readable, so the bound fires on
1015
+ # JRuby as well as CRuby (read()+IO#timeout would not).
1016
+ agreed_version = Handshake.new(@socket, deadline: @read_deadline, clock: @clock).negotiate
1017
+ @server_version = agreed_version
1018
+ @bolt_version = BoltVersion.from_int(agreed_version)
1019
+ @protocol = ProtocolVersionHandler.for_version(self, agreed_version)
1020
+ # Stand up the sans-I/O core now that a protocol is negotiated: it
1021
+ # configures the packer (UTC datetime flag) and owns hydration.
1022
+ @wire = Wire.new(@protocol)
1023
+
1024
+ puts "Negotiated Bolt version: #{@bolt_version} (0x#{agreed_version.to_s(16)})" if ENV['DEBUG']
1025
+ end
1026
+
1027
+ def perform_hello
1028
+ auth_hash = case @auth
1029
+ when Hash
1030
+ @auth
1031
+ else
1032
+ {}
1033
+ end
1034
+
1035
+ # Use protocol handler to build version-specific HELLO message.
1036
+ # `routing_context` is set by Routing::LoadBalancer (nil for
1037
+ # direct bolt:// drivers); the protocol handler drops it from
1038
+ # the HELLO payload when nil. `user_agent` may be overridden by
1039
+ # the caller (testkit threads its configured agent through the
1040
+ # driver options). On Bolt 5.1+ the HELLO carries no auth —
1041
+ # @protocol.build_hello_message strips it and we send a
1042
+ # separate LOGON below.
1043
+ hello_msg = @protocol.build_hello_message(
1044
+ user_agent: @options[:user_agent] || "neo4j-ruby-driver/#{Neo4j::Driver::VERSION}",
1045
+ auth: auth_hash,
1046
+ routing: @options[:routing_context],
1047
+ # Driver-level NotificationsConfig; only reaches the wire on Bolt
1048
+ # 5.2+ (older protocols return {} from notification_config_extra).
1049
+ notification_config: @options[:notification_config]
1050
+ )
1051
+
1052
+ # Pipeline HELLO and (on 5.1+) LOGON: enqueue both, flush once, then
1053
+ # read both replies. A pipelined server answers only once it has the
1054
+ # whole handshake (the recv-timeout liveness stub is C: HELLO / C:
1055
+ # LOGON / S: SUCCESS / S: SUCCESS), so reading HELLO's reply before
1056
+ # sending LOGON would deadlock. On 5.0/4.x build_logon_message is nil
1057
+ # — auth went in the HELLO map — and this is a single round-trip.
1058
+ logon_msg = @protocol.build_logon_message(auth_hash)
1059
+ send_message(hello_msg)
1060
+ send_message(logon_msg) if logon_msg
1061
+ flush
1062
+
1063
+ hello = fetch_response.assert_success!
1064
+ @server_agent = hello.metadata[:server]
1065
+ # Bolt 4.3/4.4 UTC patch: if the server confirmed `patch_bolt: ["utc"]`
1066
+ # (we advertised it in HELLO), switch datetime packing to UTC-seconds
1067
+ # (0x49/0x69). Native on 5.0+, so this only ever fires on 4.3/4.4.
1068
+ @wire.enable_utc_datetime if Array(hello.metadata[:patch_bolt]).include?('utc')
1069
+ # The server may advertise connection.recv_timeout_seconds in HELLO's
1070
+ # SUCCESS hints; from now a steady-state read that exceeds it is a
1071
+ # broken connection (ConnectionReadTimeoutException).
1072
+ apply_recv_timeout_hint(hello.metadata[:hints])
1073
+ # `telemetry.enabled` hint (Bolt 5.4+) opts the server into receiving
1074
+ # TELEMETRY reports; without it the driver stays silent.
1075
+ @telemetry_enabled = hello.metadata.dig(:hints, :'telemetry.enabled') == true
1076
+ # `ssr.enabled` hint (Bolt 5.8+) means this server does server-side
1077
+ # routing, so the driver may guess a home database optimistically (the
1078
+ # server re-routes if the guess is wrong) — see the home-db cache.
1079
+ @ssr_enabled = hello.metadata.dig(:hints, :'ssr.enabled') == true
1080
+
1081
+ fetch_response.assert_success! if logon_msg
1082
+ end
1083
+
1084
+ def apply_recv_timeout_hint(hints)
1085
+ seconds = hints && hints[:'connection.recv_timeout_seconds']
1086
+ @recv_timeout = seconds if seconds&.positive?
1087
+ end
1088
+ end
1089
+ end
1090
+ end
1091
+ end