Add stateless subscriptions example with listen-based e2e - #9
Conversation
1da5197 to
2b95c26
Compare
| still believes it is subscribed — recovery is `Unsubscribe` (which | ||
| clears that client-side entry) followed by a fresh `Subscribe`, and any | ||
| replica can answer the new stream. A client that must survive | ||
| connection drops therefore needs its own liveness signal — e.g. a |
There was a problem hiding this comment.
I think periodic re-read cannot be used as liveness signal here, since each ReadResource is a separate stateless POST and it can succeed even when the subscriptions/listen connection is dead. If resource stays unchanged, client has no way to notice the dead stream and will never run the Unsubscribe + Subscribe recovery, could we call this a polling/reconciliation fallback instead, and use a missed heartbeat timeout or periodic re-subscribe for liveness?
There was a problem hiding this comment.
Right again. A re-read is its own stateless POST, so it succeeds whether or not the listen stream is alive; with an unchanged resource it proves nothing, and even when the resource did change it detects the missed update after the fact, not the stream's health. Reworded in 760c89e: liveness now means a signal that rides the stream itself (a missed-heartbeat timeout on a subscribed heartbeat resource, or an unconditional periodic re-subscribe), and periodic re-reads are called what they are, a reconciliation fallback that bounds staleness but cannot detect a dead stream.
…or the listen stream
| clears that client-side entry) followed by a fresh `Subscribe`, and any | ||
| replica can answer the new stream. A client that must survive | ||
| connection drops therefore needs a liveness signal that rides the | ||
| stream itself: a subscribed heartbeat resource the server touches on an |
There was a problem hiding this comment.
I think the heartbeat has to be sent on each watched URI's own stream, since ClientSession.Subscribe opens a separate subscriptions/listen POST for each URI, and a separate heartbeat subscription can keep arriving while watched URI's stream is already dead. Could we recommend either emitting a periodic update for every watched URI, or unconditionally Unsubscribe + Subscribe each URI, and update the same guidance in main.go?
Stacked on #8 (go-sdk v1.7.0 bump) — the follow-up it deferred.
Adds
examples/subscriptions/cmd/subscriptions-stateless: the tasks subscription wiring served withStreamableHTTPOptions{Stateless: true, PropagateRequestCancellation: true}— the deployment shape for horizontally scaled servers behind a plain round-robin load balancer, and the only mode in which the Go SDK speaks protocol revision 2026-07-28.What the e2e suite pins
server/discover(the connect helper fails if a session ever falls back below it), with normal tool round-trips.resources/subscribedoesn't exist on this path —ClientSession.Subscribeopens a per-URIsubscriptions/listenstream, the server'sSubscribeHandlerstill fires per URI (same ACL gate as the legacy path), andResourceUpdatedpushes flow back over the stream. Two concurrent clients on the same URI prove per-connection fan-out.Unsubscribeends the listen stream,UnsubscribeHandlerfires, and further mutations deliver nothing.PropagateRequestCancellation: aborting the HTTP request mid-call cancels the in-flight handler context (proven with an instrumented blocking tool).initializefrom pre-2026 clients (echoing their requested version), and GET is rejected with 405/Allow: POSTper the stateless contract.Subscription registration is asynchronous under
subscriptions/listen(the client dispatches without awaiting), so delivery tests mutate in a poll loop until the notification arrives rather than mutating once — the suite passed three consecutive-race -count=3runs with no flakes.Docs: new "Serving stateless" section in
examples/subscriptions/README.md(why no session affinity is needed: subscription state lives on the connection; a dropped stream is re-issued by the client to whichever replica answers next; feed every replica from a shared event source), plus main README examples-table and cmd-list entries.Verification
go test -race -count=1 ./...green (new suite: 6 tests, also run at-count=3for flake detection)golangci-lint run0 issues;go mod tidyno-op (no new dependencies)