Skip to content

fix(gateway): prevent API routes from randomly disappearing after concurrent deploys causing 404s#2747

Open
tricktron wants to merge 4 commits into
wso2:mainfrom
tricktron:api-publish-race-bug
Open

fix(gateway): prevent API routes from randomly disappearing after concurrent deploys causing 404s#2747
tricktron wants to merge 4 commits into
wso2:mainfrom
tricktron:api-publish-race-bug

Conversation

@tricktron

Copy link
Copy Markdown
Contributor

Purpose

updateSnapshotAsync spawns a goroutine per event. Multiple goroutines call UpdateSnapshot concurrently. Each reads the config store, translates, increments the version, and writes the snapshot without any synchronization. A slow goroutine can overwrite a newer snapshot with stale data, dropping API routes. Gateway pods then 404 requests depending on which snapshot they received.

Resolves #2739

Goals

  1. Serialize UpdateSnapshot with a mutex so concurrent callers can't interleave the read-write sequence
  2. The event loop is already serial. Therefore, remove the unnecessary goroutine from updateSnapshotAsync to make it synchronous. Also rename it to updateSnapshot.

Approach

Two changes, matching patterns already in the codebase:
Add sync.Mutex to xds.SnapshotManager: Lock/Unlock wraps the entire UpdateSnapshot body. The other four snapshot managers (subscription, apikey, lazyresource, policy) already do this. xds.SnapshotManager was the only one without it.
Rename updateSnapshotAsync -> updateSnapshot, remove the go func() wrapper. The event loop processes events serially, and the subscription manager's UpdateSnapshot is already called synchronously in the same path.

The test uses Go's testHook pattern (same as net/http) to force the exact goroutine interleaving that causes the stale overwrite. It fails without the mutex and passes with it. Check out the b494ad5 to see it fail or remove the mutex manually.

User stories

As a gateway operator, I want all deployed API routes to be consistently available across all gateway pods so that requests aren't randomly 404'd depending on which pod handles them.

Documentation

N/A. Bug fix with no API changes, no new user-facing behavior.

Automation tests

  • TestConcurrentUpdateSnapshot in pkg/xds/snapshot_test.go: deterministic TOCTOU race reproduction using test hook. Forces stale interleaving, asserts the snapshot contains routes for all APIs.
  • Existing eventlistener tests pass with no regressions.

Security checks

Samples

N/A. Internal concurrency fix, no sample changes.

Related PRs

None.

Test environment

  • Go 1.26.5 with -race flag
  • linux/amd64

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The event listeners now perform bounded synchronous xDS snapshot updates. SnapshotManager.UpdateSnapshot serializes concurrent execution, and a regression test verifies routes from concurrent API updates remain in the final snapshot.

xDS snapshot race fix

Layer / File(s) Summary
Synchronous event snapshot refreshes
gateway/gateway-controller/pkg/eventlistener/api_processor.go, gateway/gateway-controller/pkg/eventlistener/llm_provider_processor.go, gateway/gateway-controller/pkg/eventlistener/mcp_processor.go
API, LLM, and MCP handlers replace asynchronous snapshot refreshes with a synchronous helper using a 10-second timeout and direct error logging.
Serialized SnapshotManager updates
gateway/gateway-controller/pkg/xds/snapshot.go
UpdateSnapshot is protected by a mutex, and an afterGetAll hook is invoked after configuration retrieval.
Concurrent update regression coverage
gateway/gateway-controller/pkg/xds/snapshot_test.go
A coordinated concurrent-update test verifies the final snapshot contains routes for both REST APIs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: pubudu538, malinthaprasan, chamilaadhi, ashera96, tgtshanika

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main fix: preventing route loss from concurrent deploys.
Description check ✅ Passed All required template sections are present and filled with relevant details.
Linked Issues check ✅ Passed The mutex fix, synchronous update, and concurrency test address #2739's race and stale snapshot overwrite.
Out of Scope Changes check ✅ Passed The extra test hook and locking changes support the race fix and stay within scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Add a deterministic test that reproduces the TOCTOU race in
UpdateSnapshot. A test hook forces goroutine A to read a stale
store while goroutine B writes the correct snapshot — then A
overwrites it with a higher version number.

The test is RED: api-two is missing from the final snapshot.
Protect the GetAll → Translate → IncrementVersion → SetSnapshot
sequence with sync.Mutex, matching the pattern used by the other
four snapshot managers (subscription, apikey, lazyresource, policy).
Rename updateSnapshotAsync to updateSnapshot and remove the
go func() wrapper. The event loop is already serial, and the
mutex on SnapshotManager now protects concurrent callers from
other paths (certificates, control plane sync).
@tricktron
tricktron force-pushed the api-publish-race-bug branch from 1326ac4 to 50f94bf Compare July 18, 2026 14:54

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-controller/pkg/xds/snapshot.go`:
- Around line 66-74: Replace the blocking sync.Mutex update gate in the gateway
controller with a context-aware semaphore, such as a buffered channel, and
update the serialization logic around UpdateSnapshot to acquire it using the
caller’s context. If acquisition is canceled, return ctx.Err() immediately
without translation or cache work; release the gate after the update completes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 04d2da14-19bf-434c-9361-c771a1d4da3d

📥 Commits

Reviewing files that changed from the base of the PR and between 1326ac4 and 50f94bf.

📒 Files selected for processing (5)
  • gateway/gateway-controller/pkg/eventlistener/api_processor.go
  • gateway/gateway-controller/pkg/eventlistener/llm_provider_processor.go
  • gateway/gateway-controller/pkg/eventlistener/mcp_processor.go
  • gateway/gateway-controller/pkg/xds/snapshot.go
  • gateway/gateway-controller/pkg/xds/snapshot_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • gateway/gateway-controller/pkg/eventlistener/mcp_processor.go
  • gateway/gateway-controller/pkg/eventlistener/llm_provider_processor.go
  • gateway/gateway-controller/pkg/xds/snapshot_test.go
  • gateway/gateway-controller/pkg/eventlistener/api_processor.go

Comment on lines +66 to +74
mu sync.Mutex
cache cache.SnapshotCache
translator *Translator
store *storage.ConfigStore
logger *slog.Logger
nodeID string // Node ID for Envoy (default: "router-node")
statusCallback StatusUpdateCallback
sdsSecretManager *SDSSecretManager
afterGetAll func() // nil in production; test hook for deterministic race testing

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make update serialization honor the caller’s context deadline.

The new sync.Mutex blocks before UpdateSnapshot can observe ctx.Done(). Under concurrent updates, a synchronous event refresh can wait past its bounded timeout and then still perform translation and cache work. Use a context-aware gate (for example, a buffered channel semaphore) and return the context error when acquisition is canceled.

Also applies to: 106-107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-controller/pkg/xds/snapshot.go` around lines 66 - 74, Replace
the blocking sync.Mutex update gate in the gateway controller with a
context-aware semaphore, such as a buffered channel, and update the
serialization logic around UpdateSnapshot to acquire it using the caller’s
context. If acquisition is canceled, return ctx.Err() immediately without
translation or cache work; release the gate after the update completes.

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.

[Bug]: xDS snapshot race in updateSnapshotAsync causes routes to randomly go missing after startup

1 participant