feat(extension): migrate task transport from gRPC to JSON-RPC over TCP loopback (PR 2/3)#1864
Merged
Conversation
…r TCP loopback Replace the existing gRPC + Netty HTTP/2 transport for the task server with LSP4J JSON-RPC over a plain TCP loopback socket. This eliminates the mid-frame HTTP/2 race (vscode-gradle issue #1815) at its source by removing the Netty stack from the task-server hot path entirely. What changes on the gradle-server side - New package ransport.jsonrpc exposing six methods on gradle/ segment: getBuild, runBuild, getProjectDependencies, cancelBuild, cancelBuilds, executeCommand. Streaming RPCs (getBuild/runBuild) deliver intermediate progress through gradle/getBuild/reply and gradle/runBuild/reply notifications and complete the JSON-RPC request with the terminal reply. - Protobuf wire schema (proto/gradle.proto) is reused for payload bytes; request/response/notification params carry the base64-encoded proto bytes plus a stream id. This keeps the message layer stable across the migration and avoids a JSON re-encoding of large progress payloads. - GradleServer becomes a thin bootstrap: it connects out to the Node listener on 127.0.0.1:<port>, builds an LSP4J launcher backed by a cached worker pool, and blocks until the socket closes. - Handlers no longer hold gRPC StreamObservers. Streaming handlers take (Request, CompletableFuture<GradleResponse>, GradleClient, long streamId); unary handlers take (Request, CompletableFuture<GradleResponse>). Removed - io.grpc:grpc-protobuf, grpc-stub, grpc-netty (plus grpc-testing) dependencies, the protoc-gen-grpc-java plugin, and the �uild/generated/source/proto/main/grpc source set. - TaskService (gRPC service shim) and ErrorMessageBuilder (only used by the gRPC error path). - The Netty HTTP/2 mid-frame log filter and its tests (GradleServerFilterTest). - Forced etty-bom pin (no longer relevant since Netty is gone from the task-server runtime). Tests - GradleServerTest now exercises handlers directly via CompletableFuture<GradleResponse> and a Mockito-mocked GradleClient, so the test surface verifies Gradle Tooling API interactions without spinning up a gRPC InProcess channel. - One extra --add-opens (java.util.concurrent) is added to the test JVM args so PowerMock can introspect CompletableFuture on JDK 17+. This commit is the gradle-server half of the migration. The TypeScript extension still speaks gRPC and is updated in PR 2; the proto files and the root-build grpcVersion constant are cleaned up in PR 3. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ip tests Add JsonRpcTransportTest exercising the new transport package end-to-end over a pair of piped streams. Two Launcher instances are wired together (server side: GradleService stub + GradleClient remote proxy; client side: recording GradleClient + GradleService remote proxy) so the full request/response/notification path is driven without a real socket and without a dependency on the Gradle Tooling API. Coverage: - Base64 protobuf encode/decode roundtrip via JsonRpcCodec. - All six RPC methods (getBuild, unBuild, getProjectDependencies, cancelBuild, cancelBuilds, �xecuteCommand) — payload identity, stream-id propagation, and null eply handling. - Streaming notifications: getBuild/reply carries the right stream id and payload to the client side; unBuild/reply is delivered on the separate unBuild notification channel without leaking into the getBuild channel. - Error mapping across the wire for ResponseErrorException produced by JsonRpcCodec.error(...) — covers NOT_FOUND, CANCELLED and INTERNAL (the last one via a Throwable carrier). - Pinned constants for the four error codes that form part of the wire contract with the TS client, to catch accidental renumbering. The handler-level behaviour (Gradle Tooling API interaction, debug init script, JVM args, etc.) stays covered by GradleServerTest, which talks to handlers directly via CompletableFuture<GradleResponse> rather than through the JSON-RPC dispatch layer — so the two test classes between them cover the wire layer and the business logic without overlapping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR migrates the extension-side Gradle task transport from gRPC to JSON-RPC over a TCP loopback listener, matching the Java-side transport cut-over and removing the gRPC client/stub path.
Changes:
- Adds a new
transport/jsonrpcpackage with loopback listener setup, proto/base64 codec, stream ID allocation, JSON-RPC error mapping, and a typedGradleJsonRpcClient. - Rewires
GradleServer,TaskServerClient, and terminal error handling to use the JSON-RPC connection instead of gRPC. - Removes gRPC code generation/dependencies and deletes the spurious-cancel retry helper/tests.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
extension/src/transport/jsonrpc/types.ts |
Defines JSON-RPC wire DTO shapes. |
extension/src/transport/jsonrpc/streamId.ts |
Adds stream ID allocation for multiplexed streaming RPCs. |
extension/src/transport/jsonrpc/protoCodec.ts |
Adds base64 encoding/decoding for protobuf payloads. |
extension/src/transport/jsonrpc/loopbackServer.ts |
Adds loopback TCP listener for JVM callback connection. |
extension/src/transport/jsonrpc/JsonRpcErrors.ts |
Adds JSON-RPC error constants and conversion helpers. |
extension/src/transport/jsonrpc/index.ts |
Exports the new transport package API. |
extension/src/transport/jsonrpc/GradleJsonRpcClient.ts |
Implements typed JSON-RPC facade for Gradle task operations. |
extension/src/test/unit/transport/loopbackServer.test.ts |
Tests listener binding, connection resolution, and logger forwarding. |
extension/src/test/unit/transport/GradleJsonRpcClient.test.ts |
Tests codec, unary RPCs, streaming dispatch, multiplexing, and error translation. |
extension/src/test/unit/retryOnSpuriousCancel.test.ts |
Removes tests for deleted gRPC retry helper. |
extension/src/terminal/GradleRunnerTerminal.ts |
Updates task terminal error handling to JSON-RPC errors. |
extension/src/server/GradleServer.ts |
Creates the loopback listener before spawning the JVM and exposes the accepted connection. |
extension/src/Extension.ts |
Replaces gRPC client logger wiring with JSON-RPC transport logger wiring. |
extension/src/client/TaskServerClient.ts |
Rewrites task client calls from gRPC stubs/streams to GradleJsonRpcClient. |
extension/src/client/retryOnSpuriousCancel.ts |
Deletes the gRPC-specific retry helper. |
extension/package.json |
Removes gRPC dev dependencies and adds direct vscode-jsonrpc dependency. |
extension/package-lock.json |
Updates dependency lockfile for gRPC removal and JSON-RPC direct dependency. |
extension/build.gradle |
Removes gRPC service stub generation while preserving protobuf message generation. |
Files not reviewed (1)
- extension/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
84b97f5 to
47e6836
Compare
- GradleServiceImpl: validate JSON-RPC params/request nullability and map Base64 decode errors to ERROR_UNKNOWN instead of ERROR_INTERNAL so client-side input bugs surface with the correct wire code. - ExecuteCommandHandler: split error replies so unknown commands return ERROR_NOT_FOUND and argument-count violations return ERROR_UNKNOWN; reserve ERROR_INTERNAL for genuine server failures. - GradleServer: extract a workerThreadFactory() that hands out unique `gradle-jsonrpc-worker-N` names via AtomicInteger so concurrent handlers are distinguishable in thread dumps and logs. Adds regression coverage: - JsonRpcTransportTest: 4 new tests for null params, null request, invalid Base64, and null cancelBuilds params. - ExecuteCommandHandlerTest (new): 4 tests covering the new error code mapping plus the happy path. - GradleServerThreadFactoryTest (new): 3 tests covering unique naming, daemon flag, and per-instance counter isolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4fb9d1f to
f11d1d0
Compare
…P loopback (PR 2/3)
Pairs with PR 1 (gradle-server Java side) to complete the cut-over from
@grpc/grpc-js to vscode-jsonrpc as the wire transport for task RPCs:
- Add `extension/src/transport/jsonrpc/` package: `GradleJsonRpcClient` (typed
facade over `MessageConnection` exposing the six `gradle/*` methods plus the
two `gradle/*/reply` streaming notifications), `loopbackServer` (binds
`127.0.0.1:0` and resolves to a `MessageConnection` on inbound connect),
`protoCodec`/`streamId`/`JsonRpcErrors` helpers, `types` wire shapes.
- Flip `GradleServer` to listen-then-spawn: the extension now binds the
ephemeral port before launching the JVM, then passes it via `--port=N`;
the JVM (PR 1) connects back as a TCP client. Adds
`GradleServer.awaitTaskConnection()` so the client can pick up the
inbound `MessageConnection`.
- Rewrite `TaskServerClient` end-to-end against `GradleJsonRpcClient`:
the six RPCs now go through JSON-RPC, error-branching uses the new
`JsonRpcErrors` codes (`NOT_FOUND`/`CANCELLED`/`UNKNOWN`/`INTERNAL`)
via `isNotFound`/`isCancelled`/`isUnknown` helpers, and the gRPC-only
`retryOnSpuriousCancel` wrappers are dropped (no HTTP/2 stream race
exists under plain TCP).
- Update `GradleRunnerTerminal.handleError` to use `isUnknown` instead of
`grpc.status.UNKNOWN`.
- Remove gRPC plumbing: delete `proto/gradle_grpc_pb.{js,d.ts}`,
`client/retryOnSpuriousCancel.ts`; strip `protobuf.plugins.grpc` /
`task.plugins.grpc` / `task.plugins.ts { option 'service=...' }` from
`extension/build.gradle`; drop `@grpc/grpc-js` and `grpc-tools` from
`devDependencies` and add `vscode-jsonrpc` (^6.0.0) to `dependencies`.
- `Extension.ts`: drop the now-unused gRPC `clientLogger`.
Closes #1815 once PR 1 + PR 2 land together. PR 3 will remove the now-unused
`service Gradle` block from `proto/gradle.proto` and the `grpcVersion`
entry from the root `build.gradle`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oundtrip tests Mirrors PR 1's `JsonRpcTransportTest` on the TS side. Wires two `MessageConnection`s back-to-back over `PassThrough` streams and drives `GradleJsonRpcClient` against a vanilla server-side `MessageConnection`, covering: - Base64 proto codec roundtrip. - `getBuild` streaming dispatch (multiple notifications, terminal null response). - Concurrent streaming calls multiplexed by `streamId` without cross-pollination. - `runBuild` streaming dispatch. - Unary `getProjectDependencies` / `cancelBuild` / `cancelBuilds` / `executeCommand`. - `ResponseError` translation back into `GradleRpcError` for the `NOT_FOUND` / `CANCELLED` / `INTERNAL` codes that callers branch on. Also removes the obsolete `retryOnSpuriousCancel` test now that the helper it covered is gone from the gRPC cut-over. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review caught that `GradleServer.start()` was binding the loopback listener before the `getGradleServerEnv()` null-check. On the no-Java path the early `return` left the listener bound, the 30 s connect timer armed, and the `awaitTaskConnection()` promise un-consumed (`_onDidStart` never fires when env is missing, so `TaskServerClient.connectToServer` is never reached). The eventual rejection surfaced as an `UnhandledPromiseRejection`. Move the listener creation below the env check so the no-Java path leaves no bound socket or pending promise behind. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sport diagnostics
Forward vscode-jsonrpc protocol logs into a project Logger channel so transport-level errors/warnings are visible in the existing Gradle output, matching the prior Logger("grpc") behaviour without coupling the transport package to the project Logger class.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ding Two unit tests for createLoopbackListener: - Asserts an ephemeral 127.0.0.1 port is bound and the connection promise resolves on the first inbound socket. - Asserts a user-supplied vscode-jsonrpc Logger receives protocol-level diagnostics by sending a framed payload that is neither request, response, nor notification and confirming the spy's �rror callback was invoked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…accept If createLoopbackListener().dispose() runs before any inbound JVM socket is accepted (e.g. GradleServer's exit handler tearing the listener down after a failed JVM spawn), the connection promise was left unsettled, making TaskServerClient.connectToServer() and other awaiters hang forever. Capture the reject callback and invoke it from dispose() when no socket has been accepted, with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Java handlers send the terminal GetBuildReply (containing GET_BUILD_RESULT) and terminal RunBuildReply (containing CANCELLED or RUN_BUILD_RESULT) as the JSON-RPC response body, not as a stream notification. The TS side was discarding the response and only processing stream notifications, so getBuild always returned undefined and the task list never populated. Extract the reply handler into a local function and invoke it on the terminal reply returned by getBuild()/runBuild() before returning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regression coverage for the fix in 6f8f396: the terminal GetBuildReply / RunBuildReply (carrying GET_BUILD_RESULT or RUN_BUILD_RESULT) arrives on the JSON-RPC response body, not as a stream notification. The new cases assert that getBuild()/runBuild() return the decoded terminal reply so callers can read the build result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
314a220 to
b97ed0e
Compare
chagong
approved these changes
Jun 1, 2026
wenytang-ms
added a commit
that referenced
this pull request
Jun 1, 2026
…1867) Both transport halves now speak JSON-RPC over a TCP loopback socket (#1863 PR 1/3, #1864 PR 2/3). This final cleanup removes the now-dead gRPC references so the repo has zero gRPC plumbing and is ready to cut a release. - proto/gradle.proto: delete the `service Gradle { ... }` block (all message definitions stay - they remain the JSON-RPC payload schema). - build.gradle: delete the unused `grpcVersion` ext property. - cgmanifest.json: drop the 4 stale io.grpc maven entries (deps removed in PR 1/3). - ThirdPartyNotices.txt: drop the grpc notice + renumber the index. - npm-package: drop the unused `@grpc/grpc-js` dependency + refresh lock. - extension/webpack.config.js: drop the dead `@grpc/proto-loader` external. - docs (README/ARCHITECTURE/CONTRIBUTING): describe the transport as JSON-RPC over TCP loopback instead of gRPC. - code comments: reword stale/legacy "gRPC" mentions to "legacy transport" while keeping the error-code-parity rationale. No runtime behavior change; no proto schema change; log/telemetry paths untouched.
wenytang-ms
added a commit
that referenced
this pull request
Jun 1, 2026
Follow-up cleanup after the JSON-RPC transport migration (#1863, #1864, #1867). These three message types were the last grpc-named identifiers left in the codebase. - proto/gradle.proto: GrpcGradleClosure/Method/Field -> GradleClosureProto/MethodProto/FieldProto (and the pluginClosures field reference). Renaming a message does not change protobuf wire format (field numbers/tags are unchanged), and both ends regenerate from the same proto. - GetBuildHandler.java: updated imports and usages. The "Proto" suffix avoids colliding with the existing com.microsoft.gradle.api domain models GradleClosure/Method/Field used in the same file. - .vscode/settings.json: drop the now-unused "Grpc" cSpell word ("Proto" is already present). Verified: extension proto regenerated, tsc passes; gradle-server proto codegen + GetBuildHandler resolve the new symbols; spotless clean.
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.
Migrate task transport from gRPC to JSON-RPC over TCP loopback (PR 2/3)
Companion to #1863 (PR 1/3, gradle-server Java side). Together they replace the gRPC + Netty + HTTP/2 task transport with JSON-RPC (LSP4J wire format) over a plain TCP loopback connection, permanently eliminating the mid-frame race described in #1815.
This PR is the TypeScript extension half of the hard cut-over: it deletes the gRPC client and rebuilds
TaskServerClienton top ofvscode-jsonrpc. No version ships between PR 1, PR 2 and PR 3.What changes
New transport package —
extension/src/transport/jsonrpc/GradleJsonRpcClient.tsMessageConnection. Exposes the sixgradle/*methods (getBuild,runBuild,getProjectDependencies,cancelBuild,cancelBuilds,executeCommand) and demultiplexes the two streaminggradle/*/replynotifications back to per-call callbacks bystreamId.loopbackServer.ts127.0.0.1:0, returns{ port, connection: Promise<MessageConnection> }. Accepts a single inbound socket, wraps it withSocketMessageReader/Writer, enforces a 30 s connect timeout (matchesTaskSocketServer.CONNECT_TIMEOUT_MSon the Java side).protoCodec.tsencodeProto(Uint8Array): string/decodeProto(string): Uint8Array— base64 ↔ proto bytes.streamId.tsJsonRpcErrors.tsUNKNOWN=-32000,NOT_FOUND=-32001,CANCELLED=-32002,INTERNAL=-32603) +GradleRpcErrorshape +isNotFound/isCancelled/isUnknownhelpers +toGradleRpcError(ResponseError)converter.types.tsGradleRequestParams,GradleResponse,GradleStreamPayload— the wire shapes pinned by PR 1.Architecture flip — extension listens, JVM dials in
PR 1 turned the JVM into a TCP client (
TaskSocketServer). PR 2 makes the extension the TCP server:GradleServer.start()now callscreateLoopbackListener()before spawning the JVM, then passes the bound ephemeral port via the same--port=<n>arg. This eliminates any window where the JVM could try to connect before the extension is ready.GradleServer.awaitTaskConnection(): Promise<MessageConnection>exposes the resulting connection toTaskServerClient.TaskServerClient.ts— full rewriteGrpcClient,grpc.credentials.createInsecure,waitForReady,getChannel().getConnectivityState, and the@grpc/grpc-js/gradle_grpc_pbimports.GradleJsonRpcClientwrapping theMessageConnectionreceived fromGradleServer.awaitTaskConnection().isNotFound(err)/isCancelled(err)/isUnknown(err)fromJsonRpcErrors.retryOnSpuriousCancelwrappers — the Node.js HTTP/2 stream race the helper compensated for cannot occur over plain TCP + LSP4J framing.GradleRunnerTerminal.tsimport { ServiceError, status } from "@grpc/grpc-js"→ import fromtransport/jsonrpc.err.code === status.UNKNOWN→isUnknown(err).Cleanup (no compat shims, per the design)
extension/src/proto/gradle_grpc_pb.{js,d.ts}(no longer generated).extension/src/client/retryOnSpuriousCancel.tsand its unit test.extension/build.gradle: dropprotobuf.plugins.grpc { … }, drop the per-taskgrpc { … }wiring, dropts { option 'service=grpc-node,mode=grpc-js' }soprotoc-gen-tsstops emitting service stubs. Thejsbuiltin +tsplugin remain —gradle_pb.{js,d.ts}are still generated and used for message classes.extension/package.json: remove@grpc/grpc-jsandgrpc-toolsfromdevDependencies; addvscode-jsonrpc: ^6.0.0todependencies(was already transitively present viavscode-languageclient, now pinned as a direct dep).Extension.ts: drop the now-unusedclientLogger(was only used to set the grpc-js global logger).Wire contract (pinned by PR 1)
gradle/getBuild,runBuild,getProjectDependencies,cancelBuild,cancelBuilds,executeCommandgradle/getBuild/reply,gradle/runBuild/reply{ request: string | null, streamId: number | null }—requestis base64 of proto bytes;streamIdset only on streaming RPCs{ reply: string | null }— base64 of terminal*Replyproto bytes (null for streaming RPCs whose full payload was delivered via notifications){ streamId: number, payload: string }— base64 of streamed*Replyproto bytes-32000 UNKNOWN,-32001 NOT_FOUND,-32002 CANCELLED,-32603 INTERNALJSON-RPC communication design
Streaming model — request + correlated notifications + terminal reply
gRPC server-streaming (
GetBuild,RunBuild) is reshaped onto JSON-RPC's request/notification split:gradle/*/replynotifications, each carrying itsstreamId.reply: nullwhen the full payload was already delivered via notifications).This split is required because a JSON-RPC notification has no id to
await, while only the response can settle the call's promise. Per-call lifecycle inGradleJsonRpcClient: allocate astreamId→ register its sink → send the request → each inbound notification is dispatched to the sink keyed bystreamId→ the response resolves the promise → the sink is removed in afinally. A late notification arriving after the promise settles finds no sink (Map.get→undefined) and is silently dropped, so it can never bleed into another in-flight call.Cancellation — business-level, not
$/cancelRequestCancellation does not use JSON-RPC's
$/cancelRequest. It keeps the existing business-level contract: a separatecancelBuildRPC carrying thecancellationKey. The streaming request's promise stays open until the server emits a terminalCancelledreply. This mirrors the previous gRPC behaviour and keeps cancellation semantics identical across the cut-over.Single connection — strict ordering, deliberate head-of-line tradeoff
All six methods and both notification streams share one TCP socket with LSP4J
Content-Lengthframing, which guarantees strict in-order delivery — this is exactly what makes "all of a stream's notifications arrive before its terminal response" hold, and is the basis of the streaming correctness above. The tradeoff is no multiplexing: concurrent builds are serialized on a single connection. That is intentional — collapsing to one non-multiplexed socket is precisely what removes the HTTP/2 frame race behind #1815.Connection lifecycle & failure semantics
On socket close/error the
MessageConnectionfiresonClose/onErrorand any in-flightsendRequestpromises reject. There is no auto-reconnect at the transport layer: a dropped connection surfaces as a rejected call, and recovery goes through the existingGradleServerJVM-exit handler +restart()path. This is consistent with droppingretryOnSpuriousCancel— there is no silent retry on the wire.Security model (current) — and deferred handshake auth
The listener binds
127.0.0.1only (no remote reachability), accepts only the first inbound connection, and enforces a 30 s connect timeout. The ephemeral port comes fromlisten(0)(kernel-assigned free port — no port-probe race, no collision-retry needed). A cryptographic handshake nonce to authenticate the connecting JVM (so a same-host process cannot pre-empt the real JVM by connecting to the loopback port first) was considered but is intentionally deferred — not implemented in this PR, tracked as a follow-up. Note: switching ports would not substitute for it, since any loopback port is enumerable and reachable by same-host processes regardless of its number.Tests
New
extension/src/test/unit/transport/GradleJsonRpcClient.test.ts(7 tests, all passing locally):getBuildstreaming dispatch — multiple notifications, terminalreply: nullresponse.streamIdwithout cross-pollination.runBuildstreaming dispatch.getProjectDependencies/cancelBuild/cancelBuilds/executeCommandroundtrip.ResponseErrortranslation intoGradleRpcErrorforNOT_FOUND/CANCELLED/INTERNAL.The test wires two
MessageConnections back-to-back overPassThroughstreams, mirroring the LSP4J piped-streams setup used by PR 1'sJsonRpcTransportTest.java.Existing unit tests: pass (
npm run compile,npm run lint:eslint— 0 errors, only pre-existingno-explicit-anywarnings unrelated to this PR).What is intentionally not in this PR
proto/gradle.protostill definesservice Gradle { … }— unused after this PR, removed in PR 3.build.gradle'sgrpcVersion,grpc-protobuf-lite, etc. — still referenced by the Java module post-PR 1 cleanup (the Java side keeps the proto message classes generated). Final removal in PR 3.Risk log
vscode-jsonrpcmajor-version mismatch with the onevscode-languageclientalready resolves^6.0.0matching the resolved transitive; npm dedups to a single instance.GradleServer.tscontinues to handle the failure path.retryOnSpuriousCancelre-surfaces a transient cancelTaskSocketServeruses a single non-multiplexed TCP socket.Closes #1815 once PR 1 + PR 2 land together (default branch behavior change ships only after both are merged).