Skip to content

Fix concurrent isolated-startup races (network create + ingress DNS latch) - #6071

Merged
ChrisJBurns merged 3 commits into
mainfrom
fix-external-network-create-race
Jul 28, 2026
Merged

Fix concurrent isolated-startup races (network create + ingress DNS latch)#6071
ChrisJBurns merged 3 commits into
mainfrom
fix-external-network-create-race

Conversation

@ChrisJBurns

@ChrisJBurns ChrisJBurns commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

The E2E Tests Core (core) shard has been failing on almost every PR because several MCP workloads of the same image start concurrently (the group_rm / list_group specs) and hit a family of concurrent isolated-startup races. Diagnostics added in #6063 captured the actual Docker errors. There are three distinct bugs; #6069 fixed one, and this PR fixes the other two:

  1. Shared-network create race (fixed here). Concurrently starting workloads share the toolhive-external network. createNetwork checks existence then creates in two non-atomic steps, so more than one caller races on NetworkCreate and all but one fails with network ... already exists → workload status "error". Fix: treat a NetworkCreate conflict as success (the network is already in the desired state).

  2. Ingress port collision — fixed by Stop deriving the ingress proxy port from a fixed upstream port #6069 (FindOrUsePort(upstreamPort+1) → random), merged into this branch.

  3. Ingress DNS latch (fixed here). After the port collision is fixed, a workload still gets stuck in status "starting" behind a persistent ingress 502, even though the MCP server is healthy and listening. The ingress Squid cache_peer targets the MCP container by name; under concurrent startup the container's Docker DNS record lags, Squid caches the failed lookup, and the peer never revives within the 2-minute readiness window. Ordering ingress creation after the MCP container (the existing mitigation) is not enough when DNS registration itself races. Fix: resolve the MCP container's IP and give it to the ingress proxy as the upstream address, so there is no DNS lookup to latch on — a transient failure becomes connection-refused, which Squid retries and recovers from in seconds. The Envoy backend's ingress cluster is pointed at the same IP for parity.

Together with #6069, this closes out all three concurrent-startup failure modes.

How this was found

Root-caused by reproducing each race directly: the network-create conflict fires 25/25 rounds under 8-way contention at the Docker API level (0/25 with the fix); the DNS latch was reproduced deterministically against a real Squid (upstream healthy + direct curl 200, yet Squid serves 502 HIER_NONE for 80s+), and a CI stress harness on the runner confirmed the status "error" (create/port) and status "starting" (DNS latch) signatures separately.

Type of change

  • Bug fix

Test plan

  • Unit tests (task test) — new real-daemon concurrency test for the network-create race (fails without the fix); pkg/container/docker package green including updated ingress-config and deploy tests asserting the ingress targets the MCP IP
  • Manual testing — reproduced all three modes against a real daemon (Docker 29.4.3) and a CI stress harness on ubuntu-8cores

Does this introduce a user-facing change?

No. Concurrent workload startup that previously failed intermittently (failed to create external networks, or a workload stuck starting/error) now succeeds.

Generated with Claude Code

Concurrently starting workloads share the "toolhive-external" Docker
network, and createNetwork checks for existence and then creates in two
non-atomic steps. When several `thv run` supervisors start at once (e.g.
multiple workloads in a group), more than one passes the existence check
and races on NetworkCreate. Docker rejects the duplicate name with a
conflict error, which propagated up as "failed to create external
networks" and left every losing workload in status "error".

Treat a conflict from NetworkCreate as success: a lost race means the
network already exists, which is exactly the desired state. The new
real-daemon test fires eight concurrent createNetwork calls for the same
name and asserts every caller succeeds with a single network created; it
fails against the old code and passes with the fix (skips without Docker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Jul 27, 2026
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 34.48276% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.18%. Comparing base (a099fa0) to head (887a909).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/container/docker/client.go 22.22% 13 Missing and 1 partial ⚠️
pkg/container/docker/squid.go 28.57% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6071      +/-   ##
==========================================
- Coverage   72.20%   72.18%   -0.03%     
==========================================
  Files         721      721              
  Lines       75062    75085      +23     
==========================================
+ Hits        54198    54199       +1     
- Misses      17003    17021      +18     
- Partials     3861     3865       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 28, 2026
After the ingress port collision (#6069) is fixed, concurrent isolated
startup still leaves a workload stuck in status "starting" behind a
persistent ingress 502, even though the MCP server is healthy and
listening. The ingress Squid cache_peer targets the MCP container by
name; under concurrent startup the container's Docker DNS record can lag,
Squid caches the failed lookup, and the peer never revives within the
2-minute readiness window. Ordering ingress creation after the MCP
container (the existing mitigation) is not enough when DNS registration
itself races.

Resolve the MCP container's IP on the internal network and give it to the
ingress proxy as the upstream address, so the cache_peer has no DNS
lookup to latch on. A bad IP just yields connection-refused, which Squid
retries and recovers from within seconds. The Envoy backend's ingress
cluster is pointed at the same IP for parity. UpstreamHost falls back to
the workload name when unset, so non-isolated paths are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChrisJBurns ChrisJBurns changed the title Make shared network creation idempotent under races Fix concurrent isolated-startup races (network create + ingress DNS latch) Jul 28, 2026
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Jul 28, 2026
@ChrisJBurns
ChrisJBurns merged commit 53127d0 into main Jul 28, 2026
48 checks passed
@ChrisJBurns
ChrisJBurns deleted the fix-external-network-create-race branch July 28, 2026 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants