Skip to content

fix: data races on dynamic endpoint state - #133

Open
fzipi wants to merge 2 commits into
mainfrom
fix/dynamic-endpoint-races
Open

fzipi wants to merge 2 commits into
mainfrom
fix/dynamic-endpoint-races

Conversation

@fzipi

@fzipi fzipi commented Aug 18, 2026

Copy link
Copy Markdown
Member

Two data races on the package-global endpoint configuration state, both reachable from concurrent requests.

handleReset deleted from dynamicEndpoints without holding dynamicEndpointMutex, racing against the reads in handleDefault. It now takes the write lock and uses clear().

computeEndpointKey mutated a package-global maphash.HashReset, WriteString, Sum64 — on every request, entirely outside the lock. Concurrent calls interleave those steps, so a key could be computed from another request's method and URL, matching the wrong configured endpoint or missing one. The shared hash is gone; the key is now method + " " + url and dynamicEndpoints is keyed by string.

go test runs with -race now. Without it neither race was detectable in CI.

TestConcurrentConfigurationAccess drives /configure_reflection, /reset, and a configured endpoint concurrently. Verified it reports DATA RACE with either fix reverted individually, and passes with both applied.

🤖 Generated with Claude Code

handleReset deleted from the dynamicEndpoints map without holding the
mutex, racing against handleDefault's reads.

computeEndpointKey mutated a package-global maphash.Hash on every
request outside any lock, so concurrent calls interleaved Reset/Write
and could return a key derived from another request's method and URL.
Replaced the shared hash with a plain string key.

Tests now run with -race; without it neither race was detectable in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fzipi
fzipi requested a review from theseion August 18, 2026 01:34
Comment thread server/server_test.go
}()
go func() {
defer wg.Done()
response, err := client.Get(server.URL + "/concurrent")

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.

Wouldn't the error be returned here? 404, since reset was called before.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No — that path never 404s. Everything unmatched falls through to handleDefault on /, which is the black hole: it writes an empty 200. I verified it against a live Handler() with /concurrent unconfigured and got 200.

So the only two outcomes here are 201 while the configuration is live and 200 once /reset has discarded it, depending on how the goroutines interleave. The test was ignoring the status entirely, which is what made the question reasonable — it now asserts the response is one of those two, so a genuine 404 or 500 regression would fail it. Pushed as a follow-up commit.

The unconfigured case falls through to the catch-all handler and returns
200, never 404, so the request can only be 200 or 201 depending on
whether /reset has run yet. Assert that rather than ignoring the status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants