Skip to content

Prevent stopped SSE watchers from updating reconnected clients - #206

Merged
juliusvonkohout merged 5 commits into
kserve:masterfrom
charan-rathore:fix/sse-watcher-generation
Oct 15, 2026
Merged

juliusvonkohout merged 5 commits into
kserve:masterfrom
charan-rathore:fix/sse-watcher-generation

Conversation

@charan-rathore

Copy link
Copy Markdown
Contributor

A watcher can finish an in-flight callback after its final client disconnects. If another client has already registered under the same namespace or resource key, the obsolete callback currently reaches that new client and overwrites the replay snapshot.

Track the active watcher generation separately from its factory placeholder and ignore callbacks from earlier generations. Keep generation validation, snapshot recording, and nonblocking queue delivery under one reentrant lock. Remove the generation when the final client leaves; watcher shutdown stays outside the lock.

Tests cover namespace and single-resource disconnect/reconnect, obsolete initial and deletion events, and the snapshot returned to a later client. Both regression tests fail before the change and pass afterwards.

Validation:

  • Connection manager:10 tests pass.
  • Watchers and routes:4 tests pass.
  • Version detection and runtime dependencies:7 tests pass in additional direct-file runs.
  • Black26.5.1:all54 backend and common backend files pass.
  • Patch application and reverse checks pass in a clean worktree.

Docker image build/smoke and Kubernetes integration were not run. Frontend checks were not run for this backend-only change.

@juliusvonkohout

Copy link
Copy Markdown
Contributor

Which queue do you mean ?

@charan-rathore

Copy link
Copy Markdown
Contributor Author

Sorry, that comment landed in the wrong place - please disregard it.

@charan-rathore charan-rathore reopened this Oct 2, 2026
@juliusvonkohout
juliusvonkohout requested a balanced review from Copilot October 2, 2026 10:27
@juliusvonkohout

Copy link
Copy Markdown
Contributor

@LogicalGuy77

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Snapshot replay remains unordered with concurrent watcher delivery, which can leave newly registered clients with stale state.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

This pull request prevents obsolete SSE watchers from updating reconnected clients.

Changes:

  • Adds watcher-generation validation under a reentrant lock.
  • Removes generations after final disconnection.
  • Adds namespace and resource regression tests.
File Description
backend/​apps/​common/​sse/​manager.py Adds generation tracking and callback synchronization.
backend/​apps/​common/​sse/​manager_test.py Tests obsolete callback rejection and snapshot preservation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread backend/apps/common/sse/manager.py Resolved
@charan-rathore

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and thanks @juliusvonkohout for pulling in a reviewer. The Copilot comment on manager.py is right.

What happens: in register_namespace_watch, a late client is added to the client set and the cached snapshot is read under the lock, but the snapshot was only put on the client's queue after the lock was released. A watcher callback takes the same lock, so it could run in that gap, see the new client in the set, and enqueue a newer event first. The older INITIAL snapshot then landed after it and the page could show stale data. register_single_watch had the same gap. This was already the case before this PR, it just sits right next to the code I changed.

Fix (5102d89): the snapshot replay now happens inside the lock, in both register_namespace_watch and register_single_watch. The queue write is put_nowait, so it does not block while the lock is held, and the lock is reentrant. A callback now has to wait until the replay is queued, so the client always gets the snapshot first and any newer event after it.

Tests: two new ones in manager_test.py (namespace and single). Each makes the late client's queue start a watcher callback on another thread at the moment the snapshot is enqueued, then checks the order is INITIAL then the newer event. Both fail on the previous manager.py and pass now. I ran manager_test.py, routes_test.py and watchers_test.py directly with python3 (all OK). I did not run the full backend test suite or test against a real cluster.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new test helper violates the repository's mandatory expressive-naming standard.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Comment thread backend/apps/common/sse/manager_test.py Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The generation checks correctly prevent obsolete callbacks, with focused coverage for both watcher types.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@juliusvonkohout

Copy link
Copy Markdown
Contributor

Pleae sign your commits properly according to the DCO.

Commit sha: e24233d, Author: Charan Rathore, Committer: Charan Rathore; The sign-off is missing.
Commit sha: 5102d89, Author: Charan Rathore, Committer: Charan Rathore; The sign-off is missing.
Commit sha: bd443a0, Author: Charan Rathore, Committer: Charan Rathore; The sign-off is missing.
Commit sha: 56e7b55, Author: Charan Rathore, Committer: Charan Rathore; The sign-off is missing.

A late client was added to the client set and its snapshot captured under the lock, but the snapshot was enqueued after the lock was released. A watcher callback could enqueue a newer event in between, so the older snapshot arrived after it. Replay inside the lock instead, as the callbacks already take it, and add tests for the namespace and single-resource paths.

Signed-off-by: Charan Rathore <[email protected]>
@charan-rathore
charan-rathore force-pushed the fix/sse-watcher-generation branch from 56e7b55 to 8965968 Compare October 5, 2026 15:37
@charan-rathore

Copy link
Copy Markdown
Contributor Author

All four commits now carry a Signed-off-by line. The code is unchanged, only the commit messages were rewritten, and the DCO check passes on the new head (8965968).

Prepared with AI assistance; Charan Rathore is directing and responsible for this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Three new test variable names violate the mandatory explicit-name standard.

2 open findings

🧠 Review effort: Balanced

Comment thread backend/apps/common/sse/manager_test.py Resolved
Comment thread backend/apps/common/sse/manager_test.py Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The generation checks and lock-scoped delivery correctly address the race with focused regression coverage.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

@juliusvonkohout

Copy link
Copy Markdown
Contributor

Please fix the tests

@juliusvonkohout

Copy link
Copy Markdown
Contributor

Let me try to rerun it first

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused lifecycle changes address both races with regression coverage and no unresolved findings.

0 open findings

🧠 Review effort: Balanced

@juliusvonkohout
juliusvonkohout merged commit e3bb62c into kserve:master Oct 15, 2026
9 of 11 checks passed
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.

3 participants