Skip to content

feat(session): add desired configuration rolling refresh - #668

Merged
SunSi12138 merged 67 commits into
devfrom
feat/651-session-refresh-rollout
Sep 12, 2026
Merged

SunSi12138 merged 67 commits into
devfrom
feat/651-session-refresh-rollout

Conversation

@SunSi12138

@SunSi12138 SunSi12138 commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add immutable server desired-session generations with explicit FutureOnly vs RollingRefresh intent
  • add canonical typed TryPublishDesiredSessionAsync(...) results for expected lifecycle/implementation rejection while retaining the throwing convenience API
  • add capability-gated SessionRefreshRequested with server-instance-scoped generation identity and bounded 24-byte payload validation in the common Protocol v2 frame parser
  • pin one complete desired snapshot at the physical accept boundary and pass it explicitly through connection lifecycle and handshake; store pins by RpcSession reference identity so a delayed disconnect cannot remove a newer session pin when a transport ID is reused; no ambient AsyncLocal state reaches the RPC request hot path
  • make rolling rollout Server-owned: caller cancellation cancels only the caller wait, same-config Rolling requests are retryable, and every explicit Rolling request advances a request epoch so a same-generation request arriving during an active scan forces a subsequent stale-session scan
  • implement bounded blue-green replacement for fixed, static-cluster, and dynamic-cluster clients
  • linearize planned replacement with one shared lineage-target publication: ordinary source/replacement admission and stale snapshots all consult that target, pre-cut reservations remain accepted by their source, and pinned old generations retain only one shared redirect to the newest committed replacement
  • serialize replacement admission-fatal publication and the actual lineage-target cut with a short connection-level gate, released before topology cleanup; fatal publication that wins rejects the cut and preserves source selection and refresh debt, while a cut that wins has already redirected ordinary RPC before a later fatal publication; ordinary RPC admission remains lock-free
  • preserve the bounded single planned-source allowance for MaxRetiringConnections = 0: admitted source work drains while the source remains physically Ready; further refreshes wait until that planned source drains
  • count fixed/static/dynamic planned sources against the retirement budget while their calls keep them physically Ready; a full budget defers every further refresh, including an idle source that could admit work during dialing, and the retained refresh debt resumes after a source drains
  • preserve source-endpoint / endpoint-generation affinity, retiring limits, per-source refresh debt, and worker ownership hand-off for multi-connection pools
  • capture the refresh request owned by each replacement attempt and transfer newer source debt to its replacement under the topology gate at the cut; completion only clears its captured request and never relies on a separate server catch-up notification to preserve accepted intent
  • reject fatal observations when a reserved one-way call acquires untracked ownership, while still allowing healthy pre-cut source reservations to finish
  • reconcile pool resizing against admission-eligible connections, so physically Ready planned sources do not cause their replacement to be retired as surplus; cluster resize candidates recheck eligibility under their owner gate
  • route planned replacement dials through the shared ConnectTransportAsync(...) physical-dial limiter from fix(client): enforce MaxConcurrentClusterConnects on physical dials #666; the temporary replace-before-retire exception applies to connection capacity, not concurrent dial count
  • capture the published replacement connection in a stable disconnect callback identity and route disconnect cleanup through the owning fixed/static/dynamic topology, terminating pending work and starting the correct reconnect path
  • keep planned refresh out of GoAway, circuit-breaker failure sampling, and failure reconnect backoff
  • safely degrade unsupported and anonymous-pipe sessions to future-only convergence
  • omit SessionRefresh from one-shot client handshake offers and return the current desired snapshot after a publication finishes waiting, including when a newer FutureOnly generation superseded it
  • allow refresh control frames while a session is draining so a later desired generation cannot turn graceful drain into a protocol failure
  • bound server refresh enqueue waits to one second per session, cancel timed-out enqueues, and allow same-generation retries without blocking later sessions
  • account internally consumed refresh control frames exactly once and update session activity before dispatch

Scope

The initial runtime-published desired Session configuration intentionally includes only MaxFramePayloadBytes, constrained by the immutable build-time hard ceiling. This PR does not add active-session flow-control renegotiation, compression wire-profile renegotiation, TLS/listener topology replacement, or generic Builder hot reload.

Validation

Direct regression coverage includes:

  • Protocol v2 payload, full-frame parsing, capability negotiation, and phase rules
  • Server desired publication, accept-boundary snapshot pinning, and handshake/update races
  • FutureOnly -> same-config RollingRefresh, caller cancellation/retry, and same-generation Rolling arriving during an active scan
  • stopped/custom implementation structured-result behavior
  • fixed stale-snapshot eligibility-cut routing to the Ready replacement
  • deterministic fatal-publication versus cut contention: a fatal publication held inside the connection gate rejects a contending cut without changing source admission or redirect
  • deterministic pause after lineage-target publication but before retirement bookkeeping: ordinary endpoint selection already reaches the replacement, pre-cut source work can register, and a contending fatal publication is ordered after that cut; both cases cover first and repeated refreshes
  • bounded server notification backpressure, same-generation retry, cancellation cleanup, and exact receive-byte/activity accounting for internally consumed refresh frames
  • both throwing and structured publication APIs return a newer desired generation published while an earlier rollout was paused
  • fixed/static/dynamic full retirement budgets block another physical replacement for both busy and idle sources, then resume the same debt after the pinned call drains; static/dynamic cover both zero and one retiring slots, and all 40-generation redirect tests reserve one slot for the pinned source and one for transient swaps
  • a deterministic cancellation callback delays an old session disconnect after it is terminal; a replacement with the same stable ID binds its own pin, retains that pin after the old callback finishes, receives the next rolling notification, and releases its pin on its own disconnect
  • actual anonymous-pipe client handshake offers omit SessionRefresh while replaceable client transport offers continue to advertise it
  • fixed/static/dynamic handshake-success replacement failure before the eligibility cut: source remains Ready/selectable, no zero-ready is published, and the same refresh debt retries
  • fixed/static/dynamic handshake-success replacement failure after the admission reservation but before the source eligibility cut: the source stays the sole Ready connection, no zero-ready is published, and the same refresh debt retries
  • a long-lived streaming call pinning the oldest generation across more than 32 successful rolling refreshes: the shared redirect stays constant-depth instead of chaining disposed predecessors, and stale admission from the pinned source still reaches the newest Ready connection
  • static/dynamic MaxRetiringConnections = 0 selection-to-registration races
  • fixed/static/dynamic replacement disconnect cleanup, pending termination, and reconnect ownership
  • refresh-worker ownership hand-off without stranded debt
  • multi-cluster planned refresh under MaxConcurrentClusterConnects = 1, proving aggregate physical-dial concurrency remains bounded
  • fixed/static/dynamic source affinity and multi-connection refresh debt/coalescing
  • fixed/static/dynamic replacement handshake paused before publication: higher-generation refresh received on the source transfers to the replacement, waits for retiring capacity, and triggers a subsequent replacement without any new rollout or catch-up; duplicate and older requests cause no additional replacement
  • one-way reservation followed by admission-fatal publication while the physical session remains Ready: invocation returns Unavailable, emits no Request, and releases both admission and untracked ownership
  • fixed/static/dynamic pool shrink during planned refresh retirement retains the eligible replacement, completes new unary work, and lets the original source call drain without reconnect
  • deterministic eligibility-cut synchronization in the fixed/static/dynamic refresh regressions instead of using heartbeat emission as a publication proxy

Local validation for the final cut protocol: 1802 Release Unit tests pass in UTC, including first-generation and repeated-refresh interleavings, the fixed/static/dynamic rollback and reconnect regressions, and 40-generation bounded redirect tests. Formatting and maintainability gates pass. Negative controls reproduce idle-budget and reused-ID failures, and the latest interaction regressions also fail against their preceding implementation in all three debt-transfer paths, fixed resizing, and fatal one-way admission. Negative controls that remove fatal synchronization or let source admission ignore the published cut both fail their behavioral assertions. See the PR Checks tab for CI on the current commit; preceding PR Quick/Extended runs are historical evidence for their earlier commits.

Closes #651

@SunSi12138 SunSi12138 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本轮复审确认上次 5 条 blocker 均已实质修复:shared dial permit、linearizable admission cut、Server-owned rollout、structured publication result、zero-retiring selection→registration race 都已闭环。当前还剩 1 个新的 replacement-publication race blocker;另外 #669 已进入 dev,当前 head 相对最新 dev 仍 behind_by=1,修复后请一并同步再跑 exact-head gate。

Comment thread src/SharpLink.Client/SharpLinkClient.SessionRefresh.cs Outdated

@SunSi12138 SunSi12138 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

继续复审发现另一个与 #651 bounded/reclaim contract 直接相关的 blocker:planned-refresh redirect 当前是逐代强引用链,连续 refresh + 长生命周期旧调用可以让链和已 dispose 的中间 connection/session 对象持续增长;32-hop cap 最终还会让极端 stale snapshot admission 失败。请把 redirect convergence 做成有界结构,并补 long-lived source + repeated successful refresh 的 deterministic regression。

Comment thread src/SharpLink.Client/ClientConnection.cs Outdated
The blue-green eligibility cut reserved replacement admission but did not
freeze the replacement's fatal transition, so a replacement that failed
after the reservation and before the source admission closure still let
the cut proceed against a dead target.

Linearize the cut on a connection-level commit CAS: the admission
reservation stays in TryReserveSessionRefreshCommit, the separate
TryCommitSessionRefreshRetirement claims the cut, and
ObserveFatalFailureForAdmission (including Fail) rejects an unclaimed
commit. A failure published first rolls the replacement back, keeps the
source selectable, and retries the refresh debt; a failure after the
claim follows ordinary post-cut semantics. Fixed, static, and dynamic cut
sites all use the same claim.

Replace the fixed 32-hop predecessor chain with one shared
SessionRefreshRedirect per refresh lineage. Every retired generation
points at the same indirection and it holds only the newest Ready
replacement, so a pinned long-lived source no longer retains disposed
predecessors and stale admission resolves in constant depth regardless of
how many rolling refreshes completed.

Regressions cover the post-reservation pre-cut replacement failure for
fixed/static/dynamic, and more than 32 successful refreshes pinned by a
long-lived server-streaming call.
@SunSi12138
SunSi12138 marked this pull request as draft September 12, 2026 14:51
@SunSi12138
SunSi12138 marked this pull request as ready for review September 12, 2026 14:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc5e663eaa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/SharpLink.Server/SharpLinkServer.DesiredSession.cs
Comment thread src/SharpLink.Runtime/RpcSession.InboundFrameParsing.cs
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

Repository owner deleted a comment from chatgpt-codex-connector Bot Sep 12, 2026
@SunSi12138
SunSi12138 marked this pull request as draft September 12, 2026 15:15
@SunSi12138
SunSi12138 marked this pull request as ready for review September 12, 2026 15:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b077841238

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/SharpLink.Server/SharpLinkServer.DesiredSession.cs Outdated
Comment thread src/SharpLink.Client/SharpLinkClient.SessionRefresh.cs Outdated
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@SunSi12138
SunSi12138 marked this pull request as draft September 12, 2026 16:02
@SunSi12138
SunSi12138 marked this pull request as ready for review September 12, 2026 16:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efbdcff165

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/SharpLink.Client/SharpLinkClient.StaticCluster.SessionRefresh.cs Outdated
Comment thread src/SharpLink.Server/SharpLinkServer.DesiredSession.cs Outdated
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4dadc3da8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/SharpLink.Client/SharpLinkClient.SessionRefresh.cs Outdated
Comment thread src/SharpLink.Client/ClientConnection.cs Outdated
Comment thread src/SharpLink.Client/ClientConnection.cs
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: cd932c627f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@SunSi12138
SunSi12138 merged commit debebcb into dev Sep 12, 2026
12 checks passed
@SunSi12138
SunSi12138 deleted the feat/651-session-refresh-rollout branch September 12, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant