Skip to content

Release: merge the reviewed 2.0.0 dev content into main - #690

Merged
SunSi12138 merged 6 commits into
mainfrom
release/2.0.0-final
Sep 13, 2026
Merged

SunSi12138 merged 6 commits into
mainfrom
release/2.0.0-final

Conversation

@SunSi12138

Copy link
Copy Markdown
Owner

Motivation

Promote the five reviewed dev changes that landed after the previous release merge onto main, so main carries the final 2.0.0 client and runtime content:

PR change
#681 allocate the shared logical-call state only for multi-participant shapes
#682 observe the OneWay client-stream terminal exactly once
#684 sample the request time budget once at frame creation
#688 release the server-streaming start task on the pending terminal
#689 sample the resolve-stage deadline once and drop the redundant checkpoint

Changes

This merge adds main history only. The release branch tree is identical to the verified dev commit 708b9d5 (git diff origin/dev is empty on the merge commit), so no source change reaches main beyond the five reviewed PRs above.

Compatibility

  • Public API: unchanged; the public API baseline for 2.0.0 is untouched.
  • Protocol/wire format: unchanged in shape. refactor(runtime): sample the request time budget once at frame creation #684 changes when the client samples the request 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.
  • Generated code or contract manifest: unchanged.
  • NativeAOT and platform behavior: unchanged.

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 at 7bb5b55.
  • Release Gate on this PR (the comprehensive release lane: codec compatibility, allocation gate, three-platform matrix, package smoke).
  • Tag and publish after merge - see the release checklist below.

Evidence

$ git diff origin/dev --name-only | wc -l
       0
$ git log --oneline -1
7bb5b55 Merge dev into the 2.0.0 main release candidate
$ bash eng/verify-packages.sh artifacts/nuget
Verified 8 package and symbol pairs for 2.0.0 at 7bb5b55637306a87a504931b2baa5cb520a2dc0e.

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.

…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.
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