Non-worker mode: thread-pool deadlock ~5s after startup under concurrent load (1.12.6, ZTS) — threads wedged in regularThread.waitForRequest / transitionToNewHandler
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
threadregular.go の waitForRequest と handleRequestWithRegularPHPThreads から始め、続いて phpthread.go の transitionToNewHandler と internal/state/state.go の ThreadState.WaitFor を追跡してください。可能であれば起動時のトラフィックバーストを再現し、参照されている goroutine の経路を調査してください。この負荷で PHP プールが行き詰まらなくなり、その障害を回帰テストでカバーできれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Summary
On FrankenPHP 1.12.6 (PHP 8.4.23 ZTS, Caddy 2.11.4), in classic non-worker mode, a container intermittently deadlocks its entire PHP thread pool a few seconds after a clean startup, under the burst of concurrent traffic that a rolling deploy sends the instant the server binds. Once wedged: :8000 stops serving all requests, the Caddy admin endpoint :2019 stays fully responsive, the container never recovers (a curl :8000/up health check hangs forever), and only a restart clears it. It's probabilistic — one of two identical hosts wedges on a given deploy while the other is fine.
This is in the same area as #2250 (ThreadState.WaitFor / go_frankenphp_main_thread_is_ready), but the trigger is different: here the server binds, logs server running, and serves real traffic for ~5 s before the whole pool deadlocks — not a never-binds init hang.
Environment
- FrankenPHP v1.12.6, Caddy v2.11.4, PHP 8.4.23 (ZTS) — image
dunglas/frankenphp:1.12.6-php8.4-bookworm(amd64) - Classic / regular mode (
php_server; noworkerdirective) frankenphp { num_threads 300 max_threads 300 max_wait_time 10s }— fixed thread count. Pinningnum_threads == max_threadsto disable dynamic scaling did not prevent it.- Docker, 32-core / ~60 GB host, non-privileged, listening on
:8000; a reverse proxy cuts live traffic over to the new container immediately on deploy. - Also reproduced on 1.12.4; upgrading 1.12.4 → 1.12.6 did not fix it.
Timeline (from the container's own logs)
"FrankenPHP started 🐘" php_version=8.4.23 num_threads=300 max_threads=300 max_requests=0
"server running" name=srv0
... ~5 seconds of normal request handling (HTTP 200s) ...
<no request handled after ~T+5s; :8000 wedged; :2019 still answering; never recovers>
Goroutine profile at the deadlock (462 goroutines)
Two dominant clusters (?debug=1):
~151 PHP threads stuck in waitForRequest, reached via a cgo callback (locked to thread):
151 @ ...
# github.com/dunglas/frankenphp.(*regularThread).waitForRequest+0x346 /go/src/app/threadregular.go:110
# github.com/dunglas/frankenphp.(*regularThread).beforeScriptExecution+0x584 /go/src/app/threadregular.go:53
# github.com/dunglas/frankenphp.(*phpThread).transitionToNewHandler+0x66 /go/src/app/phpthread.go:177
# github.com/dunglas/frankenphp.(*inactiveThread).beforeScriptExecution+0x10e /go/src/app/threadinactive.go:26
# github.com/dunglas/frankenphp.(*inactiveThread).beforeScriptExecution+0xfe /go/src/app/threadinactive.go:36
# github.com/dunglas/frankenphp.go_frankenphp_before_script_execution+0x16f /go/src/app/phpthread.go:237
# _cgoexp_..._go_frankenphp_before_script_execution+0x1a /go/src/app/phpmainthread.go:280
# runtime.cgocallbackg1 / runtime.cgocallbackg / runtime.cgocallback
~148 incoming requests parked waiting for a PHP thread:
148 @ ...
# github.com/dunglas/frankenphp.handleRequestWithRegularPHPThreads+0x828 /go/src/app/threadregular.go:148
# github.com/dunglas/frankenphp.ServeHTTP+0x710 /go/src/app/frankenphp.go:427
# ...caddyhttp request pipeline...
Representative full stacks (?debug=2):
goroutine 17 [chan receive, 8 minutes, locked to thread]:
github.com/dunglas/frankenphp/internal/state.(*ThreadState).WaitFor(0x..., {0x..., 0x2, 0x2})
/go/src/app/internal/state/state.go:167 +0x158
github.com/dunglas/frankenphp.go_frankenphp_main_thread_is_ready()
/go/src/app/phpmainthread.go:238 +0x7d
goroutine 98 [select, 8 minutes, locked to thread]:
github.com/dunglas/frankenphp.(*regularThread).waitForRequest(0x...)
/go/src/app/threadregular.go:110 +0x347
github.com/dunglas/frankenphp.(*regularThread).beforeScriptExecution(0x...)
/go/src/app/threadregular.go:53 +0x585
github.com/dunglas/frankenphp.(*phpThread).transitionToNewHandler(0x...)
/go/src/app/phpthread.go:177 +0x67
github.com/dunglas/frankenphp.(*inactiveThread).beforeScriptExecution(0x...)
/go/src/app/threadinactive.go:26 +0x10f
github.com/dunglas/frankenphp.(*inactiveThread).beforeScriptExecution(0x...)
/go/src/app/threadinactive.go:36 +0xff
github.com/dunglas/frankenphp.go_frankenphp_before_script_execution(0x0?)
/go/src/app/phpthread.go:237 +0x170
Interpretation (tentative)
Looks like a thread-pool state-machine deadlock: ~half the PHP threads are wedged mid-transitionToNewHandler → waitForRequest (inside a cgo callback, locked to thread), so they never pick up the ~148 requests parked in handleRequestWithRegularPHPThreads, while go_frankenphp_main_thread_is_ready is still blocked in ThreadState.WaitFor. The trigger appears to be the burst of concurrent requests arriving in the first seconds after server running, while threads are still transitioning inactive → regular.
Notes
- Mutex profiling isn't enabled in this build (
/debug/pprof/mutexsampling period = 0), so I couldn't capture the lock holder. Happy to enableSetMutexProfileFractionand re-capture, or share the full 462-goroutine?debug=2dump (~14k lines). - Reproduction is probabilistic on deploy under live traffic. I can try to build a minimal repro (bind, then immediately hit with N concurrent requests during thread warm-up) if useful.
- Possibly related: #2250 (same
WaitFor/main_thread_is_readycode), #2205 (worker starvation after restarts).
- 主要言語
- Go
- スター
- 11.4k
- フォーク
- 490
- 平均マージ
- 2日 21時間
- マージ済み PR(30日)
- 29
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
php/frankenphp のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
php/frankenphp#2671 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Include frankenphp version in logs対応中かも @ousamabenyounes が 83 日前に担当しました。 オープンenhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
php/frankenphp#2483 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
suggestion about compile.md再び着手できるかも このイシューのプルリクエストはマージされずにクローズされました。 オープンenhancement
難易度 1/5 1〜3時間 初心者へのやさしさ 68/100
php/frankenphp#601 · コメント 5 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 4/5 3〜5日 初心者へのやさしさ 42/100
php/frankenphp#2711 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 52/100
php/frankenphp#2710 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
-
agent-review-finding bug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
jordansmall/spindrift#5281 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
github/github-mcp-server#3489 ·
メンテナーはふだん 4 日以内に返信
-
L: java:gradle L: java:maven L: ruby:bundler T: bug 🐞
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
dependabot/dependabot-core#16547 ·
メンテナーはふだん 1 日以内に返信
-
Docs/examples still create key ranges in ascending order, which fails in 3.1.0 (follow-up to #1754)オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
pg-sharding/spqr#3083 ·
メンテナーはふだん 1 日以内に返信