Release: merge the reviewed 2.0.0 dev content into main - #690
Merged
Merged
Conversation
…articipant shapes (#681) * perf(client): allocate shared logical-call state only for multi-participant calls * perf(client): keep shared logical-call state to multi-participant shapes Review follow-ups on the first revision: - Replace the inverted `Kind != Unary` test with an explicit list of multi-participant shapes, so a plain OneWay no longer allocates the shared logical-call state. - Capture the client interceptor generation exactly once per invocation and use that same snapshot for both the allocation decision and the executed pipeline (including the deferred dynamic-module server/duplex paths). Re-reading the live generation inside the helper opened a TOCTOU window against runtime interceptor replacement. - Copy the resolved control at the interceptor terminal boundary (`_control with { Metadata = ... }`) instead of rebuilding it field by field, so no captured field can be silently dropped. - Slim ClientLogicalCallState down to the mutable deadline-claim flag. The frozen deadline, time provider, telemetry detail and captured retry generation live on the control, so nothing can drift out of sync with a copied control. Delete the now unused ClientRetryGeneration.SharedLogicalCall. - Add regression tests for the plain unary/plain oneway allocation boundary and for the captured interceptor generation. Validation: unit 1827/1827, integration 451/451, generator 264/264, load 60/60; solution builds with 0 warnings.
…682) A timed OneWay call with client streams hung forever when its logical deadline elapsed while the producer was still running: the pending deadline claimed the call and completed the pooled lease operation, the producer then finished and TryComplete missed, and the send-failure handler awaited the same pooled operation a second time. A pooled operation is single-observation, so that await never completes: the invocation never returned, the producer stayed alive, and the client's logical invocation accounting never drained. Observe the terminal result exactly once, on every path. The client-stream catch now only publishes the local send/producer failure to the pending request table and lets the table arbitrate inside its completion gate, so a deadline, caller cancellation, or connection close that already claimed the call stays authoritative instead of being replaced by the local exception. That also means the lease operation is always observed, and its GetResult finally block now returns it to the pool on the failure path.
…ion (#684) * refactor(runtime): sample the request time budget once at frame creation The send pump owned a per-frame process-local RpcDeadline so it could stamp the wire TimeBudget at emission, defer timed writes past the batching window, and compact requests whose deadline elapsed while they waited in the local queue. Sample the remaining budget once, when the frame is built, and let the pump copy frames verbatim. The end-to-end deadline stays authoritative on the caller: the client pending-deadline timer still fails a call whose Request never reaches the transport, and cancellation still terminates a remote call that already started. * perf(runtime): coalesce the send batch into one output span Writing each frame into the transport pipe as it arrives costs one GetSpan per frame, which fragments the outgoing segments at larger payload sizes. Copy the batch into a single span at flush time instead; the frames were already serialized with their wire TimeBudget, so this copy still inspects no deadline. * refactor(runtime): name the batch window for what it is The flush policy gate no longer relates to per-frame deadlines after the time budget is sampled once at frame creation, so rename the flag and the affected test fixture to describe the explicit batch window. * fix(client): arbitrate request publication against the pending deadline Two deadline gaps were left open once the send pump stopped compacting expired frames: - A plain OneWay registers no pending entry, so nothing enforced the caller's deadline while the pump held its Request. Race the emission wait against the sampled deadline; the frame is already published, so losing the race only stops the caller from blocking past its own lifetime. - A cancellable call armed its pending deadline before the Request was serialized and queued. A deadline that elapsed during serialization emitted a Cancel first and then published the Request after it, which the peer dispatches - the cancel is discarded for an unknown request. Publish the frame under the owning call's completion gate instead, and emit the transport cancel only when the Request actually reached the queue. Local dispatcher cleanup still runs for every terminal reason. * docs(changelog): state the new wire budget contract precisely * refactor(client): keep the publication gate inside the LOC budget Move TryPublishRequest into its own partial file so PendingRequestTable.cs stays within the maintainability baseline. * fix(client): publish a request only after the send queue accepted it Frame preparation now runs before the publication decision instead of inside the completion gate, so a user-provided compression provider never runs while a call's terminal arbitration is blocked and its faults surface synchronously, before the caller decides whether the frame was published. The frame is marked published only for a frame the send queue accepted, so a refused Request can no longer produce a cancel for a Request the peer never received. A plain OneWay that loses its emission race against its own deadline now also publishes the deadline cancel behind its Request, so the peer stops work whose caller has already given up, and one whose budget elapsed while its payload was still being serialized is not published at all. * fix(client): end pending-owned calls whose head Request is stuck in the transport A call that owns a pending entry waited for the emission of its head Request on the bare caller token. The deadline, a caller cancellation and a closed connection all end such a call by completing its pending entry and cancelling its producer token, but none of them cancelled the token the emission wait was observing, so the invocation stayed parked in the send path until the transport recovered even though the call already had a terminal reason. The OneWay-with-client-stream, client-streaming and duplex starts now wait on the pending call's own producer token, which is the token that ownership model cancels. A shape without a producer entry keeps the caller token, and a shape that waits on nothing (server streaming, whose caller is not blocked on this path) is unchanged. A plain OneWay also only checked its budget before frame preparation, so a Request whose budget elapsed inside the compression provider was published and then cancelled. It now re-checks after preparation, immediately before the linearization point, and returns the prepared frame instead of admitting it. * style(test): apply whitespace formatting to the moved benchmark shape tests * fix(client): make the publication gate the deadline authority TryPublishRequest only checked that the slot was still alive before admitting the frame, so whether an elapsed deadline had already won was read off the PendingDeadlineScheduler callback rather than decided under the same completion gate. A scheduler callback that runs late let a Request whose budget had already elapsed reach the peer, carrying the creation-time budget it captured before a slow codec or compression provider. The gate now decides both boundaries together, exactly as an inbound response already does in TryTakeMatchingCall: while it holds the completion gate it checks IsExpired, atomically removes the slot when the deadline won, and completes the call with DeadlineExceeded outside the gate. Only a live deadline can admit a frame and mark it published. The refused-publication path now reaches the client-stream producer with an already cancelled producer token, so the test writer honours that token instead of parking the invocation.
…688) * fix(client): add pending terminal lifetime signal * fix(client): release server streaming start on terminal * test(client): cover stalled server stream emission terminal * chore(client): preserve existing invoker rationale
…undant checkpoint (#689) * perf(client): sample the resolve-stage deadline once and drop the redundant checkpoint A plain timed Unary read the monotonic clock ten times per invocation. Three of those samples carried no information the call had not already established: - ResolveCallControl created the deadline from one sample and then validated it with a second read of the same clock a few lines later, - the invoker checked deadline progress before connection selection, which cannot have moved past the stage-local validation it just repeated, - the pre-registration checkpoint after selection remains the first sample that can observe blocking work. The resolve stage now keeps one timestamp and validates the selected deadline against it, including the inherited-deadline branch, which already had the comparison sample in hand. The pre-selection checkpoint is gone; the post-selection checkpoint and the publication gate still take their own authoritative samples. Reads per timed Unary invocation drop from 10 to 8, and the samples that can fail a call are unchanged. * fix(client): validate resolved deadline at latest comparison sample * test(client): pin exact unary clock-read budget * test(client): cover later inherited deadline validation sample * chore(client): keep deadline helper formatting stable
Brings the five reviewed dev changes that landed after the previous release merge (#681, #682, #684, #688, #689) onto main history: the logical-call allocation shape split, the single-observation OneWay client-stream terminal, the single-sample request time budget, the server-streaming terminal signal, and the resolve-stage clock-read reduction. The merged tree is identical to the verified dev commit 708b9d5; this merge adds main history only, with no additional source changes.
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Promote the five reviewed
devchanges that landed after the previous release merge ontomain, somaincarries the final 2.0.0 client and runtime content:Changes
This merge adds
mainhistory only. The release branch tree is identical to the verifieddevcommit708b9d5(git diff origin/devis empty on the merge commit), so no source change reachesmainbeyond the five reviewed PRs above.Compatibility
TimeBudget(once, at frame creation, and again at publication admission), which is a documented behavioral refinement of the advisory server budget, not a wire change.Validation
Local gates on the merge commit
7bb5b55:dotnet build Sharplink.slnx -c Release- 0 warnings, 0 errors.SharpLink.UnitTests- 1848/1848 passing.dotnet format whitespace Sharplink.slnx --verify-no-changes- clean.bash eng/check-maintainability.sh- passed (1159 files checked, 18 historical oversized files within baseline).python3 eng/verify-release-versions.py- verified 16 version declarations against released 1.1.1, no development-only increments.dotnet pack Sharplink.slnx -c Release -o artifacts/nuget+bash eng/verify-packages.sh artifacts/nuget- verified 8 package and symbol pairs for 2.0.0 at7bb5b55.Evidence
Performance evidence for the two performance-relevant PRs in this set is recorded in their own PRs (#688, #689) against exact baselines on the bare-metal probe host; no further performance lane is gated on this merge.