Support BOLT v5 protocol and add client-side router pool#48
Conversation
ebc7f34 to
93fdbb6
Compare
93fdbb6 to
f0da740
Compare
f0da740 to
4046fd1
Compare
4046fd1 to
5a577a4
Compare
Что понравилось
Блокеры1. Handshake / version negotiation сейчас не соответствует заявленной матрице совместимостиГде:
Сейчас дефолтный Проблема в том, что по BOLT range-encoding boltVersionProposal bcfg = B.concat $ encodeStrict <$> [version bcfg, 0, 0 :: Word32, 0]То есть клиент вообще не предлагает ни
Отдельно: Что бы я предложил:
Пока этого нет, основное обещание PR про backward compatibility выглядит сломанным на wire-level. 2.
|
|
Thanks for the review!
You are right. I've limited supported versions to Bolt v3 and Bolt v5.6 - v5.8. Version 4 and hasbolt will not accept connections to unsupported servers even if user tries to override Handshake process was simplified to get rid of checks for various 5.x variants.
All RouterPool-related reexports were removed from
Fixed. Now we send only GOODBYE.
Not applicable anymore, support for 4.x was explicitly disabled.
I've decided to leave it as is for now, let's look into it again someone actually tries to use IPv6
Removed tests for now removed code. The rest is not important IMO.
Docs and comments were fixed. |
| -- On connection error or exception: pipe is destroyed. | ||
| runPoolAction :: (MonadIO m, MonadMask m) => RouterPool -> AccessMode -> BoltActionT m a -> m (Either BoltError a) | ||
| runPoolAction rp mode action = do | ||
| (result, _) <- MC.generalBracket |
There was a problem hiding this comment.
"
E-варианты обещают вернуть Either, но acquirePipe выполняется до runE внутри generalBracket. Ошибки выбора сервера, отсутствия reader/writer или подключения вылетят как исключения, а не как Left BoltError. Для пользователя это ломает ожидаемый контракт runRouterPoolE/runRouterPoolReadE.
"
| _ -> do | ||
| -- No idle pipe - reserve a slot on the best server, return full | ||
| -- ranked list so caller can fall back to others on connect failure. | ||
| let sp = M.findWithDefault emptyServerPool best (psServers ps) |
There was a problem hiding this comment.
"
При отсутствии idle-pipe код просто увеличивает spInUse и открывает новое соединение, не проверяя rpcMaxPerServer. При всплеске параллельных запросов можно открыть сильно больше лимита и перегрузить Neo4j; лишние соединения закрываются только после возврата.
"
| pure pipe | ||
| connectWithRouting' :: MonadPipe m => BoltCfg -> m Pipe | ||
| connectWithRouting' bcfg = do | ||
| conn <- C.connect (secure bcfg) (host bcfg) (fromIntegral $ port bcfg) (socketTimeout bcfg) |
There was a problem hiding this comment.
"
После C.connect вызывается handshake без bracket/onException. Если handshake упадёт на версии, auth или чтении ответа, TCP-соединение уже создано, но не закрывается. В RouterPool это особенно заметно: повторные refresh/fallback-попытки могут копить незакрытые соединения.
"
Summary
This PR upgrades hasbolt from BOLT v3 to BOLT v5 (versions 5.0–5.8) as the default protocol, adding Neo4j 5.x support while maintaining backward compatibility with v3/v4 servers. It also introduces a client-side connection pool with cluster routing (
RouterPool).Version bump: 0.1.7.2 → 0.1.8.0
BOLT v5 protocol changes
HELLO(without credentials) followed by a separateLOGONmessage, instead of inlining credentials inHELLO. Onclose,LOGOFFis sent beforeGOODBYE.PULL_ALL/DISCARD_ALLwithPULL {n: -1}/DISCARD {n: -1}(parameterized variants).Pipe(previously assumed client == server version).versionAcceptedallows any non-zero server response, enabling fallback from v5 to v3.RequestLogon,RequestLogoff,RequestTelemetry,RequestPull,RequestDiscard,RequestRoute— with corresponding serialization instances and signature constants.notifications_minimum_severity,notifications_disabled_categories)bolt_agentin HELLOnotifications_disabled_categories→notifications_disabled_classificationsBoltCfgnow defaults to version0x00070805(v5.0–5.8 range proposal) and user agenthasbolt/1.8.Graph type changes (Neo4j 5 compatibility)
Node,Relationship, andURelationshipgainelementIdfields (Text, empty string for v3 servers).New
BoltCfgfieldsnotifMinSeverity :: Maybe Text— notification severity filternotifDisabledClass :: [Text]— disabled notification categories/classificationsdatabase :: Maybe Text— target database name (sent inBEGIN,RUN,ROUTEextras)Transactions
transactReadadded — sendsmode: "r"inBEGINfor read routing in clusters.transact/transactReadnow handle bothBoltError(viacatchError) and IO exceptions (viaMonadCatch.onException) for rollback, requiringMonadCatch mconstraint.BEGIN/RUNnow include notification filtering and database extras.Client-side router pool (
RouterPool)New module
Database.Bolt.Connection.RouterPoolprovides a connection pool for Neo4j clusters:ROUTEmessage (BOLT v4.3+) to discover readers, writers, and routers.rpcMaxPerServer, default 10) and idle timeout (rpcIdleTimeout, default 30s).generalBracket(fromexceptionspackage) ensures pipes are returned on success/app error and destroyed on connection error/async exception.connectRouterPool/closeRouterPool,runRouterPool/runRouterPoolRead(and*Evariants returningEither),getRoutingTable.Routing table parsing (
RoutingTable)New module
Database.Bolt.Connection.RoutingTable:parseRoutingTable— parsesROUTEresponse (supports both Bolt 4.3 top-level and 4.4+ nestedrtkey formats).parseAddress— parseshost:portand[ipv6]:portaddress strings.isExpired— TTL-based expiry check.PackStream fixes and additions
getInt8/getInt16be/getInt32betogetWord8/getWord16be/getWord32be. This fixes incorrect decoding of sizes >= 128 (e.g., a 200-character text was decoded as negative size with signed reads).Bytes ByteStringconstructor inValue, withBoltValue ByteStringandIsValue ByteStringinstances. Supports PackStream bytes markers0xCC/0xCD/0xCE.Other changes
AuthTokenShowinstance now redacts credentials.mkFailureuses safeM.lookupinstead of partial(!)for extracting error code/message from failure responses.BoltActionTderivesMonadThrow/MonadCatch(viaexceptionspackage).closecatches and ignores errors when sendingLOGOFF/GOODBYE(prevents exceptions on already-broken connections).catchErrorinpullKeysnow re-throws non-RecordHasNoKeyerrors instead of swallowing them.isNewVersion→isV3and added granular version checks (isV5,isV4_3,isV5_2,isV5_3,isV5_6).New dependencies
exceptions >= 0.10— forMonadMask/generalBracketin pool pipe lifecycletime >= 1.9— for routing table TTL/expiryasync >= 2.2— for background reaper threadcontainerslower bound raised to>= 0.6.0.1(fornubOrdfromData.Containers.ListUtils)Tests
All tests are pure (no Neo4j instance required), 113 total. New test groups:
isV3,isV5,isV4_3,isV5_2,isV5_3,isV5_6), unsigned size field roundtrips (text >= 128 bytes, list >= 128 elements), TELEMETRY message, Bytes value pack/unpack (including bytes16 marker for 256+ bytes), BoltCfg default checks.helloMap: v3 (credentials inline, routing ignored), v5.1 (no credentials, optional routing), v5.3 (bolt_agentincluded).notifExtra: pre-v5.2 returns empty, v5.2 severity/disabled categories, v5.6 key rename to classifications, combined and empty cases.ToStructure Request: Roundtrip throughtoStructure→pack→unpackforRequestLogon,RequestLogoff,RequestTelemetry,RequestPull,RequestDiscard,RequestRoute,RequestInit(v3/v5/v5+routing),RequestBegin.isConnectionError: ClassifiesCannotReadChunkas connection error;ResponseError,RoutingTableUnavailable, etc. as non-connection errors.reconcileState: Keeps existing servers (preserves in-use count), adds new servers with empty pools, removes stale servers returning their idle pipes, full server set replacement.db/modeextras.parseAddress(IPv4, IPv6, hostname with dots, error cases: port 0/65536/non-numeric/trailing text, colon-only, IPv6 without bracket-colon),parseRoutingTable(valid, missing servers, empty writers, nestedrtkey),isExpired(future, past, exact boundary).