neo4j-ruby-driver 6.0.0.alpha.2 → 6.2.1.beta.1

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