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.
- checksums.yaml +4 -4
- data/README.md +113 -411
- data/{ruby → lib}/neo4j/driver/access_mode.rb +2 -1
- data/lib/neo4j/driver/auth_token_managers.rb +31 -0
- data/lib/neo4j/driver/auth_tokens.rb +41 -0
- data/lib/neo4j/driver/auto_commit_retries_mode.rb +18 -0
- data/lib/neo4j/driver/bolt/bolt_version.rb +67 -0
- data/lib/neo4j/driver/bolt/connection.rb +1091 -0
- data/lib/neo4j/driver/bolt/handshake.rb +236 -0
- data/lib/neo4j/driver/bolt/message/failure.rb +139 -0
- data/lib/neo4j/driver/bolt/message/ignored.rb +27 -0
- data/lib/neo4j/driver/bolt/message/record.rb +30 -0
- data/lib/neo4j/driver/bolt/message/success.rb +34 -0
- data/lib/neo4j/driver/bolt/message.rb +112 -0
- data/lib/neo4j/driver/bolt/pool.rb +255 -0
- data/lib/neo4j/driver/bolt/protocol/base.rb +146 -0
- data/lib/neo4j/driver/bolt/protocol/v3.rb +43 -0
- data/lib/neo4j/driver/bolt/protocol/v4.rb +24 -0
- data/lib/neo4j/driver/bolt/protocol/v43.rb +28 -0
- data/lib/neo4j/driver/bolt/protocol/v44.rb +23 -0
- data/lib/neo4j/driver/bolt/protocol/v5.rb +27 -0
- data/lib/neo4j/driver/bolt/protocol/v51.rb +24 -0
- data/lib/neo4j/driver/bolt/protocol/v52.rb +28 -0
- data/lib/neo4j/driver/bolt/protocol/v53.rb +27 -0
- data/lib/neo4j/driver/bolt/protocol/v54.rb +14 -0
- data/lib/neo4j/driver/bolt/protocol/v55.rb +17 -0
- data/lib/neo4j/driver/bolt/protocol/v56.rb +14 -0
- data/lib/neo4j/driver/bolt/protocol/v57.rb +31 -0
- data/lib/neo4j/driver/bolt/protocol/v58.rb +15 -0
- data/lib/neo4j/driver/bolt/protocol/v6.rb +43 -0
- data/lib/neo4j/driver/bolt/protocol/v61.rb +21 -0
- data/lib/neo4j/driver/bolt/protocol_version_handler.rb +39 -0
- data/lib/neo4j/driver/bolt/record_buffer.rb +143 -0
- data/lib/neo4j/driver/bolt/stream_handler.rb +53 -0
- data/lib/neo4j/driver/bolt/tls_config.rb +129 -0
- data/lib/neo4j/driver/bolt/wire.rb +321 -0
- data/lib/neo4j/driver/bookmark.rb +15 -0
- data/lib/neo4j/driver/bookmark_managers.rb +39 -0
- data/lib/neo4j/driver/client_certificate.rb +19 -0
- data/lib/neo4j/driver/client_certificate_manager.rb +37 -0
- data/lib/neo4j/driver/client_certificate_managers.rb +15 -0
- data/lib/neo4j/driver/client_certificates.rb +14 -0
- data/lib/neo4j/driver/direct/connection_provider.rb +262 -0
- data/lib/neo4j/driver/driver.rb +268 -0
- data/lib/neo4j/driver/eager_result.rb +10 -0
- data/lib/neo4j/driver/exceptions/neo4j_exception.rb +16 -7
- data/lib/neo4j/driver/exceptions/no_such_record_exception.rb +1 -1
- data/lib/neo4j/driver/exceptions/protocol_exception.rb +0 -5
- data/lib/neo4j/driver/exceptions/security_retryable_exception.rb +14 -0
- data/lib/neo4j/driver/exceptions/transaction_terminated_exception.rb +13 -0
- data/lib/neo4j/driver/graph_database.rb +29 -0
- data/lib/neo4j/driver/internal/clock.rb +20 -0
- data/lib/neo4j/driver/internal/clock_adapter.rb +21 -0
- data/lib/neo4j/driver/internal/default_bookmark_manager.rb +54 -0
- data/lib/neo4j/driver/internal/deprecator.rb +9 -5
- data/lib/neo4j/driver/internal/driver_factory.rb +128 -0
- data/lib/neo4j/driver/internal/duration_normalizer.rb +14 -39
- data/lib/neo4j/driver/internal/extras.rb +23 -0
- data/lib/neo4j/driver/internal/home_db_cache.rb +57 -0
- data/lib/neo4j/driver/internal/internal_auth_token_manager.rb +37 -0
- data/lib/neo4j/driver/internal/metrics.rb +31 -0
- data/lib/neo4j/driver/internal/security/static_auth_token_manager.rb +25 -0
- data/lib/neo4j/driver/internal/validator.rb +16 -2
- data/lib/neo4j/driver/net.rb +29 -0
- data/lib/neo4j/driver/packstream/markers.rb +49 -0
- data/lib/neo4j/driver/packstream/packer.rb +335 -0
- data/lib/neo4j/driver/packstream/structure.rb +27 -0
- data/lib/neo4j/driver/packstream/unpacker.rb +170 -0
- data/lib/neo4j/driver/record.rb +43 -0
- data/lib/neo4j/driver/result.rb +282 -0
- data/lib/neo4j/driver/routing/load_balancer.rb +706 -0
- data/lib/neo4j/driver/routing/routed_connection.rb +132 -0
- data/lib/neo4j/driver/routing/routing_table.rb +127 -0
- data/lib/neo4j/driver/routing/server_address.rb +25 -0
- data/lib/neo4j/driver/routing_control.rb +13 -0
- data/lib/neo4j/driver/session.rb +577 -0
- data/lib/neo4j/driver/summary/database_info.rb +25 -0
- data/lib/neo4j/driver/summary/gql_notification.rb +38 -0
- data/lib/neo4j/driver/summary/gql_status_object.rb +35 -0
- data/lib/neo4j/driver/summary/notification.rb +46 -0
- data/lib/neo4j/driver/summary/plan.rb +23 -0
- data/lib/neo4j/driver/summary/profile.rb +28 -0
- data/lib/neo4j/driver/summary/query.rb +19 -0
- data/lib/neo4j/driver/summary/query_type.rb +5 -1
- data/lib/neo4j/driver/summary/result_summary.rb +215 -0
- data/lib/neo4j/driver/summary/server_info.rb +33 -0
- data/lib/neo4j/driver/summary/summary_counters.rb +72 -0
- data/lib/neo4j/driver/transaction.rb +344 -0
- data/lib/neo4j/driver/types/duration.rb +76 -0
- data/lib/neo4j/driver/types/entity.rb +40 -0
- data/lib/neo4j/driver/types/local_date_time.rb +77 -8
- data/lib/neo4j/driver/types/local_time.rb +50 -7
- data/lib/neo4j/driver/types/node.rb +18 -0
- data/lib/neo4j/driver/types/offset_time.rb +78 -7
- data/lib/neo4j/driver/types/path.rb +68 -0
- data/lib/neo4j/driver/types/point.rb +38 -18
- data/lib/neo4j/driver/types/relationship.rb +32 -0
- data/lib/neo4j/driver/types/temporal_value.rb +44 -0
- data/lib/neo4j/driver/types/unbound_relationship.rb +24 -0
- data/lib/neo4j/driver/types/unresolvable_zoned_date_time.rb +38 -0
- data/lib/neo4j/driver/types/unsupported_type.rb +25 -0
- data/lib/neo4j/driver/types/uuid.rb +29 -0
- data/{ruby → lib}/neo4j/driver/version.rb +1 -1
- data/lib/neo4j/driver.rb +15 -0
- data/lib/neo4j-ruby-driver_loader.rb +34 -20
- metadata +115 -441
- data/lib/neo4j/driver/internal/bolt_server_address.rb +0 -97
- data/lib/neo4j/driver/synchronizable.rb +0 -23
- data/lib/neo4j/driver/types/time.rb +0 -45
- data/ruby/neo4j/driver/auth_tokens.rb +0 -34
- data/ruby/neo4j/driver/bookmark.rb +0 -21
- data/ruby/neo4j/driver/config.rb +0 -89
- data/ruby/neo4j/driver/graph_database.rb +0 -78
- data/ruby/neo4j/driver/internal/async/connection/bolt_protocol_util.rb +0 -51
- data/ruby/neo4j/driver/internal/async/connection/bootstrap_factory.rb +0 -22
- data/ruby/neo4j/driver/internal/async/connection/channel_attributes.rb +0 -31
- data/ruby/neo4j/driver/internal/async/connection/channel_connected_listener.rb +0 -32
- data/ruby/neo4j/driver/internal/async/connection/channel_connector_impl.rb +0 -83
- data/ruby/neo4j/driver/internal/async/connection/channel_pipeline_builder_impl.rb +0 -22
- data/ruby/neo4j/driver/internal/async/connection/direct_connection.rb +0 -30
- data/ruby/neo4j/driver/internal/async/connection/event_loop_group_factory.rb +0 -83
- data/ruby/neo4j/driver/internal/async/connection/handshake_completed_listener.rb +0 -27
- data/ruby/neo4j/driver/internal/async/connection/handshake_handler.rb +0 -113
- data/ruby/neo4j/driver/internal/async/connection/netty_channel_initializer.rb +0 -57
- data/ruby/neo4j/driver/internal/async/connection/netty_domain_name_resolver.rb +0 -26
- data/ruby/neo4j/driver/internal/async/connection/netty_domain_name_resolver_group.rb +0 -19
- data/ruby/neo4j/driver/internal/async/connection/routing_connection.rb +0 -36
- data/ruby/neo4j/driver/internal/async/connection/stream.rb +0 -12
- data/ruby/neo4j/driver/internal/async/connection/stream_reader.rb +0 -16
- data/ruby/neo4j/driver/internal/async/connection_context.rb +0 -10
- data/ruby/neo4j/driver/internal/async/immutable_connection_context.rb +0 -24
- data/ruby/neo4j/driver/internal/async/inbound/byte_buf_input.rb +0 -30
- data/ruby/neo4j/driver/internal/async/inbound/channel_error_handler.rb +0 -77
- data/ruby/neo4j/driver/internal/async/inbound/chunk_decoder.rb +0 -41
- data/ruby/neo4j/driver/internal/async/inbound/connect_timeout_handler.rb +0 -32
- data/ruby/neo4j/driver/internal/async/inbound/connection_read_timeout_handler.rb +0 -17
- data/ruby/neo4j/driver/internal/async/inbound/inbound_message_dispatcher.rb +0 -171
- data/ruby/neo4j/driver/internal/async/inbound/inbound_message_handler.rb +0 -42
- data/ruby/neo4j/driver/internal/async/inbound/message_decoder.rb +0 -51
- data/ruby/neo4j/driver/internal/async/internal_async_session.rb +0 -92
- data/ruby/neo4j/driver/internal/async/internal_async_transaction.rb +0 -13
- data/ruby/neo4j/driver/internal/async/leak_logging_network_session.rb +0 -34
- data/ruby/neo4j/driver/internal/async/network_connection.rb +0 -194
- data/ruby/neo4j/driver/internal/async/network_session.rb +0 -150
- data/ruby/neo4j/driver/internal/async/outbound/chunk_aware_byte_buf_output.rb +0 -110
- data/ruby/neo4j/driver/internal/async/outbound/outbound_message_handler.rb +0 -39
- data/ruby/neo4j/driver/internal/async/pool/channel.rb +0 -66
- data/ruby/neo4j/driver/internal/async/pool/channel_pool.rb +0 -31
- data/ruby/neo4j/driver/internal/async/pool/channel_tracker.rb +0 -135
- data/ruby/neo4j/driver/internal/async/pool/connection_pool_impl.rb +0 -156
- data/ruby/neo4j/driver/internal/async/pool/netty_channel_health_checker.rb +0 -87
- data/ruby/neo4j/driver/internal/async/pool/netty_channel_pool.rb +0 -52
- data/ruby/neo4j/driver/internal/async/pool/network_connection_factory.rb +0 -21
- data/ruby/neo4j/driver/internal/async/pool/pool_settings.rb +0 -34
- data/ruby/neo4j/driver/internal/async/pool/timed_stack.rb +0 -15
- data/ruby/neo4j/driver/internal/async/result_cursors_holder.rb +0 -17
- data/ruby/neo4j/driver/internal/async/unmanaged_transaction.rb +0 -212
- data/ruby/neo4j/driver/internal/bookmark_holder.rb +0 -9
- data/ruby/neo4j/driver/internal/cluster/cluster_composition.rb +0 -48
- data/ruby/neo4j/driver/internal/cluster/cluster_composition_lookup_result.rb +0 -14
- data/ruby/neo4j/driver/internal/cluster/cluster_routing_table.rb +0 -122
- data/ruby/neo4j/driver/internal/cluster/identity_resolver.rb +0 -10
- data/ruby/neo4j/driver/internal/cluster/loadbalancing/least_connected_load_balancing_strategy.rb +0 -68
- data/ruby/neo4j/driver/internal/cluster/loadbalancing/load_balancer.rb +0 -125
- data/ruby/neo4j/driver/internal/cluster/loadbalancing/round_robin_array_index.rb +0 -13
- data/ruby/neo4j/driver/internal/cluster/multi_databases_routing_procedure_runner.rb +0 -31
- data/ruby/neo4j/driver/internal/cluster/rediscovery_impl.rb +0 -147
- data/ruby/neo4j/driver/internal/cluster/route_message_routing_procedure_runner.rb +0 -43
- data/ruby/neo4j/driver/internal/cluster/routing_context.rb +0 -77
- data/ruby/neo4j/driver/internal/cluster/routing_procedure_cluster_composition_provider.rb +0 -60
- data/ruby/neo4j/driver/internal/cluster/routing_procedure_response.rb +0 -35
- data/ruby/neo4j/driver/internal/cluster/routing_settings.rb +0 -24
- data/ruby/neo4j/driver/internal/cluster/routing_table_handler_impl.rb +0 -95
- data/ruby/neo4j/driver/internal/cluster/routing_table_registry_impl.rb +0 -121
- data/ruby/neo4j/driver/internal/cluster/single_database_routing_procedure_runner.rb +0 -73
- data/ruby/neo4j/driver/internal/connection_settings.rb +0 -16
- data/ruby/neo4j/driver/internal/cursor/async_result_cursor_impl.rb +0 -55
- data/ruby/neo4j/driver/internal/cursor/async_result_cursor_only_factory.rb +0 -24
- data/ruby/neo4j/driver/internal/cursor/disposable_async_result_cursor.rb +0 -61
- data/ruby/neo4j/driver/internal/cursor/result_cursor_factory_impl.rb +0 -24
- data/ruby/neo4j/driver/internal/cursor/rx_result_cursor_impl.rb +0 -110
- data/ruby/neo4j/driver/internal/database_name_util.rb +0 -37
- data/ruby/neo4j/driver/internal/default_bookmark_holder.rb +0 -9
- data/ruby/neo4j/driver/internal/default_domain_name_resolver.rb +0 -11
- data/ruby/neo4j/driver/internal/direct_connection_provider.rb +0 -40
- data/ruby/neo4j/driver/internal/driver_factory.rb +0 -126
- data/ruby/neo4j/driver/internal/eager_result_value.rb +0 -5
- data/ruby/neo4j/driver/internal/handlers/begin_tx_response_handler.rb +0 -20
- data/ruby/neo4j/driver/internal/handlers/channel_releasing_reset_response_handler.rb +0 -30
- data/ruby/neo4j/driver/internal/handlers/commit_tx_response_handler.rb +0 -23
- data/ruby/neo4j/driver/internal/handlers/hello_response_handler.rb +0 -65
- data/ruby/neo4j/driver/internal/handlers/init_response_handler.rb +0 -34
- data/ruby/neo4j/driver/internal/handlers/legacy_pull_all_response_handler.rb +0 -199
- data/ruby/neo4j/driver/internal/handlers/no_op_response_handler.rb +0 -16
- data/ruby/neo4j/driver/internal/handlers/ping_response_handler.rb +0 -29
- data/ruby/neo4j/driver/internal/handlers/pull_handlers.rb +0 -32
- data/ruby/neo4j/driver/internal/handlers/pulln/auto_pull_response_handler.rb +0 -169
- data/ruby/neo4j/driver/internal/handlers/pulln/basic_pull_response_handler.rb +0 -298
- data/ruby/neo4j/driver/internal/handlers/pulln/fetch_size_util.rb +0 -20
- data/ruby/neo4j/driver/internal/handlers/reset_response_handler.rb +0 -34
- data/ruby/neo4j/driver/internal/handlers/rollback_tx_response_handler.rb +0 -25
- data/ruby/neo4j/driver/internal/handlers/route_message_response_handler.rb +0 -21
- data/ruby/neo4j/driver/internal/handlers/routing_response_handler.rb +0 -79
- data/ruby/neo4j/driver/internal/handlers/run_response_handler.rb +0 -38
- data/ruby/neo4j/driver/internal/handlers/session_pull_response_completion_listener.rb +0 -34
- data/ruby/neo4j/driver/internal/handlers/transaction_pull_response_completion_listener.rb +0 -20
- data/ruby/neo4j/driver/internal/impersonation_util.rb +0 -22
- data/ruby/neo4j/driver/internal/internal_bookmark.rb +0 -9
- data/ruby/neo4j/driver/internal/internal_database_name.rb +0 -9
- data/ruby/neo4j/driver/internal/internal_driver.rb +0 -83
- data/ruby/neo4j/driver/internal/internal_entity.rb +0 -21
- data/ruby/neo4j/driver/internal/internal_node.rb +0 -21
- data/ruby/neo4j/driver/internal/internal_pair.rb +0 -9
- data/ruby/neo4j/driver/internal/internal_path.rb +0 -35
- data/ruby/neo4j/driver/internal/internal_point2_d.rb +0 -9
- data/ruby/neo4j/driver/internal/internal_point3_d.rb +0 -6
- data/ruby/neo4j/driver/internal/internal_record.rb +0 -27
- data/ruby/neo4j/driver/internal/internal_relationship.rb +0 -27
- data/ruby/neo4j/driver/internal/internal_result.rb +0 -49
- data/ruby/neo4j/driver/internal/internal_session.rb +0 -91
- data/ruby/neo4j/driver/internal/internal_transaction.rb +0 -48
- data/ruby/neo4j/driver/internal/logging/channel_activity_logger.rb +0 -29
- data/ruby/neo4j/driver/internal/logging/channel_error_logger.rb +0 -17
- data/ruby/neo4j/driver/internal/logging/prefixed_logger.rb +0 -19
- data/ruby/neo4j/driver/internal/logging/reformatted_logger.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/abstract_message_writer.rb +0 -23
- data/ruby/neo4j/driver/internal/messaging/bolt_protocol.rb +0 -32
- data/ruby/neo4j/driver/internal/messaging/bolt_protocol_version.rb +0 -48
- data/ruby/neo4j/driver/internal/messaging/common/common_message_reader.rb +0 -51
- data/ruby/neo4j/driver/internal/messaging/common/common_value.rb +0 -33
- data/ruby/neo4j/driver/internal/messaging/common/common_value_packer.rb +0 -104
- data/ruby/neo4j/driver/internal/messaging/common/common_value_unpacker.rb +0 -256
- data/ruby/neo4j/driver/internal/messaging/encode/begin_message_encoder.rb +0 -15
- data/ruby/neo4j/driver/internal/messaging/encode/commit_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/discard_all_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/discard_message_encoder.rb +0 -15
- data/ruby/neo4j/driver/internal/messaging/encode/goodbye_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/hello_message_encoder.rb +0 -15
- data/ruby/neo4j/driver/internal/messaging/encode/init_message_encoder.rb +0 -16
- data/ruby/neo4j/driver/internal/messaging/encode/logoff_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/logon_message_encoder.rb +0 -15
- data/ruby/neo4j/driver/internal/messaging/encode/pull_all_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/pull_message_encoder.rb +0 -15
- data/ruby/neo4j/driver/internal/messaging/encode/reset_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/rollback_message_encoder.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/encode/route_message_encoder.rb +0 -24
- data/ruby/neo4j/driver/internal/messaging/encode/route_v44_message_encoder.rb +0 -22
- data/ruby/neo4j/driver/internal/messaging/encode/run_message_encoder.rb +0 -16
- data/ruby/neo4j/driver/internal/messaging/encode/run_with_metadata_message_encoder.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/request/abstract_streaming_message.rb +0 -25
- data/ruby/neo4j/driver/internal/messaging/request/begin_message.rb +0 -25
- data/ruby/neo4j/driver/internal/messaging/request/commit_message.rb +0 -20
- data/ruby/neo4j/driver/internal/messaging/request/discard_all_message.rb +0 -20
- data/ruby/neo4j/driver/internal/messaging/request/discard_message.rb +0 -23
- data/ruby/neo4j/driver/internal/messaging/request/goodbye_message.rb +0 -20
- data/ruby/neo4j/driver/internal/messaging/request/hello_message.rb +0 -30
- data/ruby/neo4j/driver/internal/messaging/request/init_message.rb +0 -19
- data/ruby/neo4j/driver/internal/messaging/request/logoff_message.rb +0 -13
- data/ruby/neo4j/driver/internal/messaging/request/logon_message.rb +0 -13
- data/ruby/neo4j/driver/internal/messaging/request/message_with_metadata.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/request/multi_database_util.rb +0 -26
- data/ruby/neo4j/driver/internal/messaging/request/pull_all_message.rb +0 -23
- data/ruby/neo4j/driver/internal/messaging/request/pull_message.rb +0 -22
- data/ruby/neo4j/driver/internal/messaging/request/reset_message.rb +0 -32
- data/ruby/neo4j/driver/internal/messaging/request/rollback_message.rb +0 -20
- data/ruby/neo4j/driver/internal/messaging/request/route_message.rb +0 -28
- data/ruby/neo4j/driver/internal/messaging/request/run_message.rb +0 -23
- data/ruby/neo4j/driver/internal/messaging/request/run_with_metadata_message.rb +0 -49
- data/ruby/neo4j/driver/internal/messaging/request/transaction_metadata_builder.rb +0 -24
- data/ruby/neo4j/driver/internal/messaging/response/failure_message.rb +0 -40
- data/ruby/neo4j/driver/internal/messaging/response/ignored_message.rb +0 -29
- data/ruby/neo4j/driver/internal/messaging/response/record_message.rb +0 -33
- data/ruby/neo4j/driver/internal/messaging/response/success_message.rb +0 -34
- data/ruby/neo4j/driver/internal/messaging/v3/bolt_protocol_v3.rb +0 -82
- data/ruby/neo4j/driver/internal/messaging/v3/message_format_v3.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/v3/message_writer_v3.rb +0 -27
- data/ruby/neo4j/driver/internal/messaging/v4/bolt_protocol_v4.rb +0 -29
- data/ruby/neo4j/driver/internal/messaging/v4/message_format_v4.rb +0 -13
- data/ruby/neo4j/driver/internal/messaging/v4/message_writer_v4.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/v41/bolt_protocol_v41.rb +0 -25
- data/ruby/neo4j/driver/internal/messaging/v42/bolt_protocol_v42.rb +0 -13
- data/ruby/neo4j/driver/internal/messaging/v43/bolt_protocol_v43.rb +0 -19
- data/ruby/neo4j/driver/internal/messaging/v43/message_format_v43.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/v43/message_writer_v43.rb +0 -20
- data/ruby/neo4j/driver/internal/messaging/v44/bolt_protocol_v44.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/v44/message_format_v44.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/v44/message_writer_v44.rb +0 -15
- data/ruby/neo4j/driver/internal/messaging/v5/bolt_protocol_v5.rb +0 -21
- data/ruby/neo4j/driver/internal/messaging/v5/message_format_v5.rb +0 -18
- data/ruby/neo4j/driver/internal/messaging/v5/value_unpacker_v5.rb +0 -17
- data/ruby/neo4j/driver/internal/messaging/v51/bolt_protocol_v51.rb +0 -40
- data/ruby/neo4j/driver/internal/messaging/v51/message_format_v51.rb +0 -14
- data/ruby/neo4j/driver/internal/messaging/v51/message_writer_v51.rb +0 -17
- data/ruby/neo4j/driver/internal/metrics/connection_pool_metrics_listener.rb +0 -34
- data/ruby/neo4j/driver/internal/metrics/internal_abstract_metrics.rb +0 -46
- data/ruby/neo4j/driver/internal/metrics/internal_connection_pool_metrics.rb +0 -105
- data/ruby/neo4j/driver/internal/metrics/internal_metrics.rb +0 -82
- data/ruby/neo4j/driver/internal/metrics/internal_metrics_provider.rb +0 -18
- data/ruby/neo4j/driver/internal/metrics/listener_event.rb +0 -17
- data/ruby/neo4j/driver/internal/metrics/metrics_provider.rb +0 -24
- data/ruby/neo4j/driver/internal/metrics/time_recorder_listener_event.rb +0 -15
- data/ruby/neo4j/driver/internal/packstream/byte_array_incompatible_packer.rb +0 -12
- data/ruby/neo4j/driver/internal/packstream/pack_input.rb +0 -47
- data/ruby/neo4j/driver/internal/packstream/pack_output.rb +0 -39
- data/ruby/neo4j/driver/internal/packstream/pack_stream.rb +0 -324
- data/ruby/neo4j/driver/internal/packstream/pack_type.rb +0 -17
- data/ruby/neo4j/driver/internal/read_only_bookmark_holder.rb +0 -13
- data/ruby/neo4j/driver/internal/resolved_bolt_server_address.rb +0 -35
- data/ruby/neo4j/driver/internal/retry/exponential_backoff_retry_logic.rb +0 -150
- data/ruby/neo4j/driver/internal/revocation_strategy.rb +0 -19
- data/ruby/neo4j/driver/internal/scheme.rb +0 -32
- data/ruby/neo4j/driver/internal/security/internal_auth_token.rb +0 -15
- data/ruby/neo4j/driver/internal/security/security_plan_impl.rb +0 -48
- data/ruby/neo4j/driver/internal/security_setting.rb +0 -66
- data/ruby/neo4j/driver/internal/session_factory_impl.rb +0 -32
- data/ruby/neo4j/driver/internal/spi/connection.rb +0 -19
- data/ruby/neo4j/driver/internal/spi/connection_pool.rb +0 -9
- data/ruby/neo4j/driver/internal/spi/response_handler.rb +0 -23
- data/ruby/neo4j/driver/internal/summary/internal_database_info.rb +0 -7
- data/ruby/neo4j/driver/internal/summary/internal_input_position.rb +0 -11
- data/ruby/neo4j/driver/internal/summary/internal_notification.rb +0 -16
- data/ruby/neo4j/driver/internal/summary/internal_plan.rb +0 -41
- data/ruby/neo4j/driver/internal/summary/internal_profiled_plan.rb +0 -32
- data/ruby/neo4j/driver/internal/summary/internal_result_summary.rb +0 -33
- data/ruby/neo4j/driver/internal/summary/internal_server_info.rb +0 -6
- data/ruby/neo4j/driver/internal/summary/internal_summary_counters.rb +0 -18
- data/ruby/neo4j/driver/internal/svm/netty_substitutions.rb +0 -196
- data/ruby/neo4j/driver/internal/svm/z_lib_substitutions.rb +0 -21
- data/ruby/neo4j/driver/internal/util/certificate_tool.rb +0 -65
- data/ruby/neo4j/driver/internal/util/clock.rb +0 -29
- data/ruby/neo4j/driver/internal/util/error_util.rb +0 -104
- data/ruby/neo4j/driver/internal/util/extract.rb +0 -123
- data/ruby/neo4j/driver/internal/util/format.rb +0 -39
- data/ruby/neo4j/driver/internal/util/futures.rb +0 -99
- data/ruby/neo4j/driver/internal/util/iterables.rb +0 -35
- data/ruby/neo4j/driver/internal/util/lock_util.rb +0 -23
- data/ruby/neo4j/driver/internal/util/metadata_extractor.rb +0 -101
- data/ruby/neo4j/driver/internal/util/mutex.rb +0 -9
- data/ruby/neo4j/driver/internal/util/preconditions.rb +0 -16
- data/ruby/neo4j/driver/internal/util/result_holder.rb +0 -72
- data/ruby/neo4j/driver/internal/util/server_version.rb +0 -60
- data/ruby/neo4j/driver/logging1.rb +0 -51
- data/ruby/neo4j/driver/net/server_address.rb +0 -9
- data/ruby/neo4j/driver/query.rb +0 -48
- data/ruby/neo4j/driver/records.rb +0 -13
- data/ruby/neo4j/driver/transaction_config.rb +0 -50
- data/ruby/neo4j/driver/values.rb +0 -26
- 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
|