-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Preserve live peer sessions across equivalent route revision bumps #9342
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
Changes from all commits
29d362c
61e458b
baa0c49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,6 +200,16 @@ actor CmxConnectivityPeerSession { | |
| continue redial | ||
| } | ||
|
|
||
| // The dead-on-arrival probe suspends this actor. A concurrent | ||
| // caller that dialed in that window may have installed first; | ||
| // installing over it would leak its session and double-record | ||
| // an established lifecycle for the same peer. | ||
| if let installed = activeConnection { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A concurrent invalidation, remote close, or replacement dial can run while the redundant session is being closed, but this returns the previously captured Prompt for AI agents |
||
| if installed.id != pending.id { | ||
| await connected.close() | ||
| } | ||
| return installed.session | ||
| } | ||
| install( | ||
| connected, | ||
| id: pending.id, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||
| /// Authoritative route material whose change requires live session teardown. | ||||||
| /// | ||||||
| /// Volatile freshness fields are excluded on purpose: `last_seen_at`, path | ||||||
| /// hints, direct ports, and display names move on every registration | ||||||
| /// heartbeat and shape only the next dial, never the trust of an already | ||||||
| /// admitted connection. Comparing this content lets a route revision bump | ||||||
| /// keep healthy sessions whose routes did not materially change. | ||||||
| struct CmxConnectivityRouteContent: Equatable, Sendable { | ||||||
| /// Trust material shared by every route in one account snapshot. | ||||||
| struct AccountMaterial: Equatable, Sendable { | ||||||
| let relayFleet: [String] | ||||||
| let lanRendezvous: CmxIrohLANRendezvous | ||||||
| let grantVerificationKeys: CmxIrohGrantVerificationKeySet | ||||||
| } | ||||||
|
|
||||||
| /// Admission-relevant material of one broker binding. | ||||||
| struct BindingMaterial: Equatable, Sendable { | ||||||
| let bindingID: String | ||||||
| let appInstanceID: String | ||||||
| let tag: String | ||||||
| let platform: CmxIrohPlatform | ||||||
| let identityGeneration: Int | ||||||
| let pairingEnabled: Bool | ||||||
| let capabilities: [String] | ||||||
|
|
||||||
| init(binding: CmxIrohBrokerBinding) { | ||||||
| bindingID = binding.bindingID | ||||||
| appInstanceID = binding.appInstanceID | ||||||
| tag = binding.tag | ||||||
| platform = binding.platform | ||||||
| identityGeneration = binding.identityGeneration | ||||||
| pairingEnabled = binding.pairingEnabled | ||||||
| capabilities = binding.capabilities | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A capability-order-only revision will invalidate the live peer session even though the advertised capability set is unchanged. Store a canonical ordering here so route-content equality matches the set semantics used by the admission policy. Prompt for AI agents |
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| let account: AccountMaterial | ||||||
| private let peerRoutes: [CmxConnectivityPeerID: [BindingMaterial]] | ||||||
|
|
||||||
| init(snapshot: CmxIrohDiscoveryResponse) { | ||||||
| account = AccountMaterial( | ||||||
| relayFleet: snapshot.relayFleet, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A revision that only reorders the same relay fleet will be treated as an account-material change, causing unnecessary teardown of all live sessions. Canonicalize the fleet order (or compare it as a set) when constructing route content so equivalent snapshots remain equivalent. Prompt for AI agents
Suggested change
|
||||||
| lanRendezvous: snapshot.lanRendezvous, | ||||||
| grantVerificationKeys: snapshot.grantVerificationKeys | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A revision that only reorders equivalent grant verification keys will be treated as changed account trust material and tear down all live sessions. Canonicalize the key array by Prompt for AI agents |
||||||
| ) | ||||||
| var routes: [CmxConnectivityPeerID: [BindingMaterial]] = [:] | ||||||
| for binding in snapshot.bindings { | ||||||
| let peerID = CmxConnectivityPeerID( | ||||||
| identity: binding.endpointID, | ||||||
| deviceID: binding.deviceID | ||||||
| ) | ||||||
| routes[peerID, default: []].append(BindingMaterial(binding: binding)) | ||||||
| } | ||||||
| peerRoutes = routes.mapValues { bindings in | ||||||
| bindings.sorted { $0.bindingID < $1.bindingID } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Returns the material route for one peer, or nil when unrouted. | ||||||
| func peerRoute( | ||||||
| for peerID: CmxConnectivityPeerID | ||||||
| ) -> [BindingMaterial]? { | ||||||
| peerRoutes[peerID] | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -149,7 +149,10 @@ extension CmxIrohHostRuntime { | |
| try requireCurrent(revision) | ||
| await handleRoute(metadata, discovered.pathHints) | ||
| try requireCurrent(revision) | ||
| await connectivityEngine.didInstallRouteRevision(discoveredRevision) | ||
| await connectivityEngine.didInstallRouteRevision( | ||
| discoveredRevision, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Overlapping host reconciliations can install an older discovery snapshot after a newer one because this call forwards the fetched revision without a monotonic check. That can roll back the engine's installed route revision and tear down or retain sessions based on stale route content; an engine-level monotonic install/coalescing guard would keep older completions from winning. Prompt for AI agents |
||
| routes: discovery | ||
| ) | ||
| scheduleLANPublication( | ||
| binding: metadata, | ||
| rendezvous: discovery.lanRendezvous, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: A live peer can survive the first snapshot installed after the engine recorded a revision without route content.
performRouteSynccan leaverouteContentnil for an unchanged response, but this same-revision branch skips the documented fail-closed invalidation; compare the prior content and invalidate, or invalidate all when the baseline is missing, before storing it.Prompt for AI agents