Skip to content

fix: hand off PHP requests during configuration reloads - #2648

Closed
henderkes wants to merge 4 commits into
mainfrom
fix/worker-reload-queue
Closed

henderkes wants to merge 4 commits into
mainfrom
fix/worker-reload-queue

Conversation

@henderkes

@henderkes henderkes commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

edit: just a reminder to myself to do it properly after api con

edit: prevent 503s from requests coming in during config reloads (complementary to nicolas' PR #2661)

@henderkes
henderkes marked this pull request as draft September 16, 2026 07:07
@henderkes
henderkes marked this pull request as ready for review September 28, 2026 21:13
@henderkes
henderkes requested review from AlliBalliBaba and dunglas and removed request for dunglas September 28, 2026 21:22
@dunglas

dunglas commented Sep 29, 2026

Copy link
Copy Markdown
Member

Could you run one of the benchmark to check the impact of the atomic.Pointer on the hot path?

@henderkes

Copy link
Copy Markdown
Contributor Author

The atomic pointer should be completely free on aarch64 and x86-64. If anything I should benchmark the check though.

@henderkes

Copy link
Copy Markdown
Contributor Author

+3% (Go) instructions per request. Not terrible, but also not perfect...

@henderkes
henderkes force-pushed the fix/worker-reload-queue branch from f965bcf to b7cccf2 Compare September 29, 2026 14:17
@henderkes

Copy link
Copy Markdown
Contributor Author

instead of old requests being rerouted through new route tree, we now take their existing state and execute it on the new runtime after the reload comes back. should be +/-0 instructions

Comment thread worker.go Outdated
Comment thread worker.go Outdated
maxThreads int
requestOptions []RequestOption
requestChan chan *frankenPHPContext
done <-chan struct{}

@AlliBalliBaba AlliBalliBaba Sep 29, 2026 •

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.

It probably would make sense to store and check readiness somewhere on the Server directly and not re-use them across reloads.

Would be a bit of a bigger refactor,.but IMO the behavior should be the same for regular threads and workers.

@henderkes
henderkes requested review from AlliBalliBaba and a balanced review from Copilot and removed request for AlliBalliBaba October 6, 2026 08:13

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

Retired worker requests can hang after draining stops, and queued regular requests can execute with stale configuration.

Review effort: Balanced
Findings: 2 High severity

Open (2)
What changed in this PR

Adds request handoff during FrankenPHP configuration reloads to reduce transient 503 responses.

Changes:

  • Drains retired worker queues and propagates runtime errors.
  • Redispatches requests to the newly active Caddy app.
  • Adds reload, queue-draining, and routing tests.
File Description
worker.go Drains retired worker queues and snapshots metrics.
worker_internal_test.go Tests worker queue draining and expiration.
threadregular_test.go Updates completion channel type.
testdata/​reload.php Provides reload request fixture.
scaling.go Prevents scaling retired workers.
options.go Adds worker drain timeout option.
frankenphp.go Retires servers before runtime shutdown.
context.go Changes completion channels to carry errors.
caddy/​servername.go Resolves hosts through nested routes.
caddy/​serveridx_test.go Tests stable reload module mapping.
caddy/​reload.go Implements request redispatch and startup waiting.
caddy/​reload_test.go Tests reload handoff behavior and limits.
caddy/​module.go Invokes handoff on retired servers.
caddy/​app.go Tracks the active app and reload identities.

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

Comment thread frankenphp.go Outdated
Comment thread worker.go Outdated
@henderkes
henderkes force-pushed the fix/worker-reload-queue branch 2 times, most recently from cbf6d7c to 1765184 Compare October 7, 2026 20:01
When Caddy swaps its configuration, the new frankenphp app replaces the
global PHP runtime while the old HTTP routes keep serving. Requests
arriving in that window were answered 503 instead of being handled by
the new runtime, and requests parked in worker queues could hang
forever.

Wait for the replacement app and replay such requests through its own
HTTP chain, so its matchers and middleware apply to the request the
client sent as if it arrived fresh. Requests parked in the old queues
are released back to the module, which replays them; late senders are
rejected by a sticky sign bit, and metrics are captured per runtime so
they stay balanced across the swap.

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

Handoff can corrupt responses already started by upstream middleware, and the worker retirement protocol lacks focused concurrency coverage.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread caddy/reload_capture.go Outdated
Comment thread worker.go
Comment on lines +342 to +345
func (worker *worker) retire() {
const retired = -1 << 31
worker.queuedRequests.Add(retired)
for worker.queuedRequests.Load() != retired {
The capture records what the client sent and the ingress writer before
any configured middleware runs, and the replay goes through the
replacement chain on that writer. When the old chain consumed body
bytes it kept no copy of, the handoff refuses rather than replay a
truncated body.
@henderkes
henderkes force-pushed the fix/worker-reload-queue branch from e496c90 to b0fc7d9 Compare October 8, 2026 07:49
@henderkes
henderkes requested a balanced review from Copilot October 8, 2026 07:54

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

Queued regular requests can hang without replacement regular threads, and retired middleware errors can corrupt replayed responses.

3 open findings
1 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread threadregular.go
Comment on lines +111 to +113
if fc.server.regularRequestsRetired.Load() {
fc.done <- ErrNotRunning
continue
Comment thread caddy/reload_capture.go
body: r.Body, contentLength: r.ContentLength,
writer: w, guard: guard,
})
return next.ServeHTTP(guard, r)
@AlliBalliBaba

Copy link
Copy Markdown
Contributor

Hmm this PR has gotten a lot more complex since I last took a look. Is the replaying of the request on the caddy side really necessary?

Would it work to just keep a reference to all the old workers and then after reload has finished pass all requests from the old requestChan into the workers (with the same name) from the new requestChan?

That would be a bit dangerous to do if the configuration actually meaningfully changed, but I guess the same thing happens here in this PR?

@henderkes

Copy link
Copy Markdown
Contributor Author

I'm currently looking for other approaches here and kind of use the PR here as a testing ground. Should be converted to draft.

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.

4 participants