Skip to content

Bolt Handshake Manifest v1 #1102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ebb9160
Refactor bolt socket
robsdedude Oct 9, 2024
a1584b0
WIP: Bolt handshake v2
robsdedude Oct 9, 2024
cdf1a8e
Fix handshake v2 logging
robsdedude Oct 10, 2024
74863c4
Fix unit tests
robsdedude Oct 10, 2024
b23dca5
Fix socket resource leak
robsdedude Oct 10, 2024
0b5c785
WIP
robsdedude Oct 11, 2024
710f984
Merge branch '5.0' into bolt-handshake-v2
robsdedude Oct 16, 2024
d411518
Merge branch '5.0' into bolt-handshake-v2
robsdedude Oct 16, 2024
cb5dc26
WIP
robsdedude Oct 16, 2024
380c70c
WIP
robsdedude Oct 16, 2024
fe85543
Merge branch '5.0' into bolt-handshake-v2
robsdedude Oct 23, 2024
98a3c2e
TestKit + tox config improvements
robsdedude Oct 23, 2024
d316b24
Merge branch '5.0' into bolt-handshake-v2
robsdedude Oct 31, 2024
7c231e2
Improve handshake logging
robsdedude Nov 1, 2024
958272d
Merge branch '5.0' into bolt-handshake-v2
robsdedude Nov 21, 2024
5a03869
Merge branch '5.0' into bolt-handshake-v2
robsdedude Dec 3, 2024
60d77c8
Add TestKit feature flag for handshake manifest v1
robsdedude Dec 4, 2024
8670ffa
Merge branch '5.0' into bolt-handshake-v2
robsdedude Jan 28, 2025
80ffed0
Drop support for Bolt 4.0-4.2
robsdedude Jan 28, 2025
68e8ce6
Tests: fix event loop shutdown for Python <= 3.8
robsdedude Jan 28, 2025
89c7004
fixup! Drop support for Bolt 4.0-4.2
robsdedude Jan 28, 2025
c802344
amend! Drop support for Bolt 4.0-4.2
robsdedude Jan 28, 2025
923e8f5
Revert: Run against TestKit's 5.0 branch again
robsdedude Jan 29, 2025
0db7ca3
TestKit glue: remove tracemalloc option for faster testing
robsdedude Feb 4, 2025
20f488f
Update comments
robsdedude Feb 4, 2025
143ae8d
Merge branch '5.0' into bolt-handshake-v2
robsdedude Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/neo4j/_async/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
]


# [bolt-version-bump] search tag when changing bolt version support
from . import ( # noqa - imports needed to register protocol handlers
_bolt3,
_bolt4,
_bolt5,
)
from ._bolt import AsyncBolt
from ._common import (
check_supported_server_product,
Expand Down
183 changes: 32 additions & 151 deletions src/neo4j/_async/io/_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from logging import getLogger
from time import monotonic

from ..._async_compat.network import AsyncBoltSocket
from ..._async_compat.util import AsyncUtil
from ..._codec.hydration import (
HydrationHandlerABC,
Expand Down Expand Up @@ -52,6 +51,7 @@
SessionExpired,
)
from ..config import AsyncPoolConfig
from ._bolt_socket import AsyncBoltSocket
from ._common import (
AsyncInbox,
AsyncOutbox,
Expand All @@ -60,6 +60,8 @@


if t.TYPE_CHECKING:
import typing_extensions as te

from ..._api import TelemetryAPI


Expand Down Expand Up @@ -273,98 +275,31 @@ def assert_notification_filtering_support(self):
f"{self.server_info.agent!r}"
)

# [bolt-version-bump] search tag when changing bolt version support
@classmethod
def protocol_handlers(cls, protocol_version=None):
"""
Return a dictionary of available Bolt protocol handlers.

The handlers are keyed by version tuple. If an explicit protocol
version is provided, the dictionary will contain either zero or one
items, depending on whether that version is supported. If no protocol
version is provided, all available versions will be returned.

:param protocol_version: tuple identifying a specific protocol
version (e.g. (3, 5)) or None
:returns: dictionary of version tuple to handler class for all
relevant and supported protocol versions
:raise TypeError: if protocol version is not passed in a tuple
"""
# Carry out Bolt subclass imports locally to avoid circular dependency
# issues.
from ._bolt3 import AsyncBolt3
from ._bolt4 import (
AsyncBolt4x1,
AsyncBolt4x2,
AsyncBolt4x3,
AsyncBolt4x4,
)
from ._bolt5 import (
AsyncBolt5x0,
AsyncBolt5x1,
AsyncBolt5x2,
AsyncBolt5x3,
AsyncBolt5x4,
AsyncBolt5x5,
AsyncBolt5x6,
AsyncBolt5x7,
)

handlers = {
AsyncBolt3.PROTOCOL_VERSION: AsyncBolt3,
# 4.0 unsupported because no space left in the handshake
AsyncBolt4x1.PROTOCOL_VERSION: AsyncBolt4x1,
AsyncBolt4x2.PROTOCOL_VERSION: AsyncBolt4x2,
AsyncBolt4x3.PROTOCOL_VERSION: AsyncBolt4x3,
AsyncBolt4x4.PROTOCOL_VERSION: AsyncBolt4x4,
AsyncBolt5x0.PROTOCOL_VERSION: AsyncBolt5x0,
AsyncBolt5x1.PROTOCOL_VERSION: AsyncBolt5x1,
AsyncBolt5x2.PROTOCOL_VERSION: AsyncBolt5x2,
AsyncBolt5x3.PROTOCOL_VERSION: AsyncBolt5x3,
AsyncBolt5x4.PROTOCOL_VERSION: AsyncBolt5x4,
AsyncBolt5x5.PROTOCOL_VERSION: AsyncBolt5x5,
AsyncBolt5x6.PROTOCOL_VERSION: AsyncBolt5x6,
AsyncBolt5x7.PROTOCOL_VERSION: AsyncBolt5x7,
}
protocol_handlers: t.ClassVar[dict[Version, type[AsyncBolt]]] = {}

def __init_subclass__(cls: type[te.Self], **kwargs: t.Any) -> None:
protocol_version = cls.PROTOCOL_VERSION
if protocol_version is None:
return handlers

if not isinstance(protocol_version, tuple):
raise TypeError("Protocol version must be specified as a tuple")

if protocol_version in handlers:
return {protocol_version: handlers[protocol_version]}

return {}

@classmethod
def version_list(cls, versions):
"""
Return a list of supported protocol versions in order of preference.

The number of protocol versions (or ranges) returned is limited to 4.
"""
# In fact, 4.3 is the fist version to support ranges. However, the
# range support got backported to 4.2. But even if the server is too
# old to have the backport, negotiating BOLT 4.1 is no problem as it's
# equivalent to 4.2
first_with_range_support = Version(4, 2)
result = []
for version in versions:
if (
result
and version >= first_with_range_support
and result[-1][0] == version[0]
and result[-1][1][1] == version[1] + 1
):
# can use range to encompass this version
result[-1][1][1] = version[1]
continue
result.append(Version(version[0], [version[1], version[1]]))
if len(result) == 4:
break
return result
raise ValueError(
"AsyncBolt subclasses must define PROTOCOL_VERSION"
)
if not (
isinstance(protocol_version, Version)
and len(protocol_version) == 2
and all(isinstance(i, int) for i in protocol_version)
):
raise TypeError(
"PROTOCOL_VERSION must be a 2-tuple of integers, not "
f"{protocol_version!r}"
)
if protocol_version in AsyncBolt.protocol_handlers:
cls_conflict = AsyncBolt.protocol_handlers[protocol_version]
raise TypeError(
f"Multiple classes for the same protocol version "
f"{protocol_version}: {cls}, {cls_conflict}"
)
cls.protocol_handlers[protocol_version] = cls
super().__init_subclass__(**kwargs)

@classmethod
def get_handshake(cls):
Expand All @@ -374,12 +309,9 @@ def get_handshake(cls):
The length is 16 bytes as specified in the Bolt version negotiation.
:returns: bytes
"""
supported_versions = sorted(
cls.protocol_handlers().keys(), reverse=True
return (
b"\x00\x00\x01\xff\x00\x07\x07\x05\x00\x04\x04\x04\x00\x00\x00\x03"
)
offered_versions = cls.version_list(supported_versions)
versions_bytes = (v.to_bytes() for v in offered_versions)
return b"".join(versions_bytes).ljust(16, b"\x00")

@classmethod
async def ping(cls, address, *, deadline=None, pool_config=None):
Expand Down Expand Up @@ -455,68 +387,17 @@ async def open(
)

pool_config.protocol_version = protocol_version

# Carry out Bolt subclass imports locally to avoid circular dependency
# issues.

# avoid new lines after imports for better readability and conciseness
# fmt: off
if protocol_version == (5, 7):
from ._bolt5 import AsyncBolt5x7
bolt_cls = AsyncBolt5x7
elif protocol_version == (5, 6):
from ._bolt5 import AsyncBolt5x6
bolt_cls = AsyncBolt5x6
elif protocol_version == (5, 5):
from ._bolt5 import AsyncBolt5x5
bolt_cls = AsyncBolt5x5
elif protocol_version == (5, 4):
from ._bolt5 import AsyncBolt5x4
bolt_cls = AsyncBolt5x4
elif protocol_version == (5, 3):
from ._bolt5 import AsyncBolt5x3
bolt_cls = AsyncBolt5x3
elif protocol_version == (5, 2):
from ._bolt5 import AsyncBolt5x2
bolt_cls = AsyncBolt5x2
elif protocol_version == (5, 1):
from ._bolt5 import AsyncBolt5x1
bolt_cls = AsyncBolt5x1
elif protocol_version == (5, 0):
from ._bolt5 import AsyncBolt5x0
bolt_cls = AsyncBolt5x0
elif protocol_version == (4, 4):
from ._bolt4 import AsyncBolt4x4
bolt_cls = AsyncBolt4x4
elif protocol_version == (4, 3):
from ._bolt4 import AsyncBolt4x3
bolt_cls = AsyncBolt4x3
elif protocol_version == (4, 2):
from ._bolt4 import AsyncBolt4x2
bolt_cls = AsyncBolt4x2
elif protocol_version == (4, 1):
from ._bolt4 import AsyncBolt4x1
bolt_cls = AsyncBolt4x1
# Implementation for 4.0 exists, but there was no space left in the
# handshake to offer this version to the server. Hence, the server
# should never request us to speak bolt 4.0.
# elif protocol_version == (4, 0):
# from ._bolt4 import AsyncBolt4x0
# bolt_cls = AsyncBolt4x0
elif protocol_version == (3, 0):
from ._bolt3 import AsyncBolt3
bolt_cls = AsyncBolt3
# fmt: on
else:
protocol_handlers = AsyncBolt.protocol_handlers
bolt_cls = protocol_handlers.get(protocol_version)
if bolt_cls is None:
log.debug("[#%04X] C: <CLOSE>", s.getsockname()[1])
await AsyncBoltSocket.close_socket(s)

supported_versions = cls.protocol_handlers().keys()
# TODO: 6.0 - raise public DriverError subclass instead
raise BoltHandshakeError(
"The neo4j server does not support communication with this "
"driver. This driver has support for Bolt protocols "
f"{tuple(map(str, supported_versions))}.",
f"{tuple(map(str, AsyncBolt.protocol_handlers.keys()))}.",
address=address,
request_data=handshake,
response_data=data,
Expand Down
Loading