Skip to content

fix: reject fetch when the client SSE stream is on another replica - #187

Open
davidgrldo wants to merge 1 commit into
benc-uk:mainfrom
davidgrldo:fix/185-sse-multi-replica-oom
Open

davidgrldo wants to merge 1 commit into
benc-uk:mainfrom
davidgrldo:fix/185-sse-multi-replica-oom

Conversation

@davidgrldo

Copy link
Copy Markdown

Fixes #185

Root cause

With multiple replicas and no sticky sessions, /api/fetch and the /updates SSE stream can land on different pods. handleFetchData then added the clientID to the namespace group even though the client has no SSE channel on this pod, leaving a phantom group member. Every informer event for that namespace sent on a nil channel and blocked the informer handler goroutine forever, so client-go's unbounded pending-notification buffer grew on every event and every 1-minute resync until the pod was OOMKilled (~450 MiB/hour in the reporter's cluster).

Changes

  • handleFetchData only joins the namespace group when the clientID has an SSE connection on this pod, and returns 409 otherwise — no more phantom group members
  • Frontend retries the fetch (up to 5×, 250 ms apart) on 409, so it eventually lands on the pod holding the SSE stream, and the client actually gets live updates instead of silently degrading
  • go.mod temporarily replaces go-rest-api with a fork (tag v1.1.1-fix.185) carrying the broker-side fix: non-blocking sends (skip missing clients, drop when the receiver is not ready), Stream exiting on r.Context().Done() (goroutine leak), and mutex-guarded broker state. See fix(sse): never block senders, and stop leaking stream goroutines go-rest-api#4 — the replace should be dropped once that is released upstream
  • Helm values now document that multiple replicas need sticky sessions (session affinity) for live updates to work

Why both sides are needed

The guard alone still leaves a small window: a client disconnect (listen()) can race a send between the group cleanup and the channel delete, which is enough to stall an informer once. The broker fix (benc-uk/go-rest-api#4) makes sends non-blocking, so a stall can never happen again; the guard + frontend retry restore correct behaviour (live updates) for the multi-replica case.

Verification

  • go build ./..., go vet ./..., go test ./server/services/ -short ✅
  • make lint equivalents: eslint, prettier, golangci-lint — 0 issues ✅
  • Broker unit tests (new in the fork) pass with -race ✅

Workaround for existing users until this ships: run a single replica or enable sticky sessions on the ingress.

Fixes benc-uk#185

With multiple replicas and no sticky sessions, /api/fetch and the /updates
SSE stream can land on different pods. handleFetchData then added the
clientID to the namespace group even though the client has no SSE channel
on this pod, leaving a phantom group member. Every informer event for that
namespace sent to a nil channel and blocked the informer handler goroutine
forever, so client-go's unbounded pending-notification buffer grew on every
event and every 1 minute resync until the pod was OOMKilled (~450 MiB/hour,
from ~200 MiB steady state to a 2 GiB limit in ~4 hours).

- handleFetchData now only joins the namespace group when the clientID has
  an SSE connection on this pod, and returns 409 otherwise
- The frontend retries the fetch (up to 5 times) on 409, so it lands on the
  pod holding the SSE stream
- go.mod temporarily replaces go-rest-api with a fork carrying the broker
  fix (non-blocking sends, no goroutine leak, mutex-guarded state); drop the
  replace once the fix is released upstream
- Helm values now document that multiple replicas need sticky sessions for
  live updates to work

Note: the broker-side fix is required in addition to the guard, because a
client disconnect (listen) can still race a send between the group cleanup
and the channel delete.

This branch has not been deployed

No deployments
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.

Memory leak to OOMKill when /api/fetch and /updates SSE land on different replicas (nil-channel send in SSE broker blocks informer handlers)

1 participant