Skip to content

fix(proxy): keep serving briefly after SIGTERM so the gateway moves off a draining pod - #5474

Merged
kojiwakayama merged 14 commits into
mainfrom
koji/proxy-health-drain
Nov 7, 2026
Merged

kojiwakayama merged 14 commits into
mainfrom
koji/proxy-health-drain

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requests failed with 503 during proxy rollouts and node drains.

The gateway reaches the proxy over pooled keep-alive connections through the Service. When a proxy pod got SIGTERM it returned the draining 503 to every new request at once, including requests that were already arriving on the gateway's open connections, and the gateway passed those 503s to users. The Service itself stops routing new connections to a terminating pod, so the gap is the reused connections.

Changes

  • Admission grace window. For SHUTDOWN_ADMISSION_GRACE_MS after SIGTERM (default 10s, capped at the drain budget), the proxy keeps serving new requests and sends Connection: close on each response. The gateway reconnects through the Service and lands on a pod that stays. After the window, new requests get the draining 503 as before. A new WebSocket upgrade gets the 503 right away.
  • Drain order. Shutdown waits out the grace window, then waits for in-flight requests with the remaining drain budget (SHUTDOWN_DRAIN_TIMEOUT_MS - grace), so the total stays within the Kubernetes grace period. The routing invalidation subscription stays open throughout, as before.
  • /_proxy/ready. A readiness endpoint that returns 503 once shutdown starts. /_proxy/health is unchanged and stays the liveness and startup target. The chart can switch readiness to it once a server image contains it (an older image would answer 404); with this PR it is optional.

I checked that Deno.serve closes an HTTP/1.1 keep-alive connection after a response carrying Connection: close (one response, then EOF; without the header the same connection serves both requests).

Tests

  • request-drain.test.ts: the admission window opens on the first signal and isn't extended by a second; withConnectionClose works on mutable and immutable (fetched) responses; the env name in parse errors; readiness before and after shutdown.
  • main.test.ts: the ordering of readiness, refusal and tracking; Connection: close on admitted drain responses; grace before drain wait; the reduced drain budget.
  • deno test src/proxy/main.test.ts src/proxy/request-drain.test.ts: 4 passed (33 steps). deno check src/proxy/main.ts passes.

(drafted by Claude for Koji)

/_proxy/health is a static 200 and is used for readiness as well as
liveness, so a draining proxy stays in Service endpoints while it answers
every new request with 503. Add /_proxy/ready, which returns the draining
503 once shutdown starts, so Kubernetes stops routing to the replica.
/_proxy/health stays static so liveness never restarts a pod mid-drain.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T01:49:52.263577Z 88da613 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 66 366 KiB ✅ 0
src/react/components/Head.tsx 5 74 KiB ✅ 0
src/react/router/index.tsx 5 74 KiB ✅ 0
src/react/context/index.tsx 5 74 KiB ✅ 0
src/react/fonts/index.ts 6 81 KiB ✅ 0
src/react/components/ui/index.ts 111 717 KiB ✅ 0
src/react/components/ui/adapter/contract.ts 1 18 KiB ✅ 0
src/react/components/ui/icons/index.ts 3 17 KiB ✅ 0
src/chat/index.ts 305 2046 KiB ✅ 0
src/chat/ag-ui.ts 61 459 KiB ✅ 0
src/chat/protocol.ts 1 11 KiB ✅ 0
src/chat/types.ts 46 246 KiB ✅ 0
src/chat/message-prep.ts 68 517 KiB ✅ 0
src/markdown/index.ts 6 30 KiB ✅ 0
src/mdx/index.ts 2 2 KiB ✅ 0
src/agent/identity-contracts.ts 1 3 KiB ✅ 0
src/schemas/index.ts 43 220 KiB ✅ 0
src/platform/env.ts 5 53 KiB ✅ 0
src/workflow/claude-code/react/index.ts 9 91 KiB ✅ 0
src/events/index.ts 31 567 KiB ✅ 0
src/events/ag-ui/public.ts 30 561 KiB ✅ 0
src/run-events/index.ts 54 321 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

Copy link
Copy Markdown
Contributor

Score: 90/100. Small, correct change that adds a readiness endpoint which fails while draining.

  • Strength: /_proxy/ready is checked before the shuttingDown short-circuit, and /_proxy/health is unchanged, so liveness does not restart draining pods. The draining 503 is reused through createProxyDrainingResponse(), so retry-after and the response headers stay consistent.
  • Strength: The rollout plan is sound. Switching the Helm probe only after an image ships this endpoint avoids a 404-driven mass unready. The Cache-Control: no-store header is appropriate.
  • Strength: The diff is minimal and keeps the logic in request-drain.ts next to the drain code. It adds a unit test for the ready response and the 503 response.
  • Concern (minor): The ordering check in main.test.ts asserts an exact source string, including the \n and spacing. A formatter change or a reorder of the neighbouring lines will break it. This follows the existing pattern in that file, so it is not a blocker. A behavioral test of router would be sturdier if one is cheap to build.
  • Concern (minor): The new endpoint is not mentioned in any docs or deployment reference. If probes are documented anywhere, add /_proxy/ready. I did not run the tests locally.

Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 565db35252

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@codecov

codecov Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.70073% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/proxy/request-drain.ts 92.43% 6 Missing and 3 partials ⚠️
cli/commands/serve/split-mode.ts 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The gateway reaches the proxy over pooled keep-alive connections through the Service. Requests already queued on those connections got the draining 503 the moment shutdown started, and the gateway passed it to users.

For SHUTDOWN_ADMISSION_GRACE_MS (default 10s, within the drain budget) a draining proxy still serves new requests and closes each connection after the response, so the gateway reconnects through the Service, which no longer routes to the terminating pod. New WebSocket upgrades and requests after the window still get the 503. The request drain then waits for the remaining budget.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@kojiwakayama kojiwakayama changed the title fix(proxy): add a readiness endpoint that fails while draining fix(proxy): keep serving briefly after SIGTERM so the gateway moves off a draining pod Nov 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0913cdc2d0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/main.ts Resolved
Comment thread src/proxy/main.ts Resolved
Comment thread src/proxy/main.ts Resolved
Comment thread src/proxy/main.ts Resolved
- Refuse new SSE streams (Accept: text/event-stream and the control-plane
  run stream POST) as well as WebSocket upgrades from the start of the
  admission grace window; they could outlive the remaining drain budget.
- Grant the admission grace only for SIGTERM; Ctrl-C (SIGINT) shuts down
  without the 10s wait and keeps the full drain budget.
- Wait out the grace window, and poll the drain, on a setTimeout captured
  before extensions load, so a replaced timer cannot skip cleanup.
- Document SHUTDOWN_ADMISSION_GRACE_MS (default, max, interaction with
  SHUTDOWN_DRAIN_TIMEOUT_MS) beside the other proxy settings.
- Give the memory smoke a 1s grace so the idle proxy exits inside
  `docker stop --time 5` instead of being SIGKILLed.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

Moves the draining Connection: close decision out of the router into
ProxyDrainAdmission.closeIfDraining, which brings the router back under
Sonar's cognitive-complexity limit and puts the branch under unit test.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a301557b2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
Comment thread src/proxy/main.ts Resolved
- Capture performance.now (and Reflect.apply) at module evaluation so a
  replaced Performance.prototype.now cannot make the signal handler
  throw in drainAdmission.begin before draining and cleanup run.
- Run the five SIGTERMed binary smoke probes with
  SHUTDOWN_ADMISSION_GRACE_MS=0 so each idle proxy exits at once.
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 4ad52b9208

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 4ad52b9208

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Bun defines now on the performance instance, not Performance.prototype,
so the captured-clock test patches whichever object owns it.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31af7efc96

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
Comment thread src/proxy/request-drain.ts Resolved
Comment thread src/proxy/main.ts Resolved
…hile draining

- A GET/HEAD admitted during the drain whose response is an SSE stream
  (negotiated by the response, without Accept: text/event-stream) gets
  the draining 503 so the client reconnects to a replica that stays; its
  upstream body is cancelled. Non-idempotent streams are kept.
- Router error responses (the Host 400 and, while draining, unexpected
  failures as a reported 500) also close their connection, so the
  gateway does not stay pinned to the terminating pod.
- Capture Math.max/Math.ceil before extensions load and move the drain
  budget arithmetic into ProxyDrainAdmission.remainingDrainBudgetMs.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 866cd8f9bf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
useChat and useAgent POST /api/ag-ui and /api/agents/:id without
Accept: text/event-stream, so admission now classifies those routes as
long-lived and refuses them before their run starts.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cac5aba06

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
POST /api/agents/:id returns JSON (useAgent parses response.json()), so
only the AG-UI SSE endpoint is refused before it starts.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85e7f15cd0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
waitForDrain and getInFlightRequests now use Math.min, Number.isSafeInteger,
Array.from and Map.prototype.values captured before extensions load, so a
replaced intrinsic cannot cut the drain short or skip cleanup.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6193627cc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
Comment thread src/proxy/request-drain.ts Resolved
…t mode

- useChat sends Accept: text/event-stream on every chat POST (callers can
  still override it), so a draining proxy refuses new chat streams on any
  configured `api` path, not only /api/ag-ui.
- Local `serve --split` starts the proxy child with
  SHUTDOWN_ADMISSION_GRACE_MS=0: it has no gateway to wait for and its
  signal handler forwards SIGTERM.
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

@codex review

@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d4bf1d1c6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/proxy/request-drain.ts Resolved
Both hooks POST and read a streamed body; they now send an Accept that
lists text/event-stream (with text/plain and */* fallbacks), so a
draining proxy refuses them before the work starts. Swept the other
framework React hooks: useChat already declares it, useAgent reads JSON.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a03cd9547

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agent/react/use-completion.ts Resolved
useCompletion and useStreaming decode raw text, so an endpoint that
negotiates on Accept must keep choosing plain text. SSE stays listed at
q=0.9 so a draining proxy still recognises the request as a stream.
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 88da613634

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@kojiwakayama
kojiwakayama added this pull request to the merge queue Nov 7, 2026
@sonarqubecloud

sonarqubecloud Bot commented Nov 7, 2026

Copy link
Copy Markdown

Merged via the queue into main with commit 7e0e201 Nov 7, 2026
439 checks passed
@kojiwakayama
kojiwakayama deleted the koji/proxy-health-drain branch November 7, 2026 02:20
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