mirror of
https://github.com/valyala/fasthttp.git
synced 2026-06-25 17:45:28 +03:00
2c1590038f
* feat(prefork): graceful shutdown, leak fixes, hook robustness Addresses outstanding review concerns and several adjacent issues surfaced during a follow-up review pass. Lifecycle / supervision - Track every per-child Wait goroutine via sync.WaitGroup and unblock pending sigCh sends through a context.Cancel so early-return paths (OnChildSpawn / OnMasterReady error, recovery doCommand error, ErrOverRecovery) can no longer leak goroutines or stall children. - Install signal.Notify(SIGTERM, SIGINT) in the master so deploy/ rolling-restart signals enter the shutdown path instead of killing the master without graceful teardown. - Replace the unconditional SIGKILL defer with a SIGTERM-then-SIGKILL sequence gated by a configurable ShutdownGracePeriod (defaults to 5s, Windows path stays SIGKILL since Signal(SIGTERM) is unsupported). API - OnChildRecover now returns error so callers can implement recovery policies (circuit-breaker etc.); panic in any hook is recovered and surfaced as the returned error, with diagnostic logging. - Add RecoverInterval (optional crash-loop backoff) and ShutdownGracePeriod fields with safe zero-value defaults. - Export ErrCommandProducerNilCmd and ErrCommandProducerNotStarted sentinel errors so callers can errors.Is them. - Rename oldPid/newPid to oldPID/newPID per Go initialism convention. - Logger interface now declares an explicit compile-time compatibility check with fasthttp.Logger. Resource hygiene - Master closes both the original tcpListener and the duped fd in p.files when prefork() returns; previously the duped fd leaked once per call. - doCommand wraps every error path with %w + fmt.Errorf so caller-side diagnostics keep stage context. - Strip pre-existing FASTHTTP_PREFORK_CHILD entries before appending so child env never carries duplicate keys. - Extract magic numbers as package constants (inheritedListenerFD, masterPollInterval, defaultShutdownGracePeriod, preforkChildEnvValue). - Rename the inherited listener fd via os.NewFile so net.FileListener errors are diagnosable. Tests - Migrate to t.Setenv (drop the global setUp/tearDown helpers) — fixes the env-mutation-vs-parallel race. - Replace rand.Intn port helper with `:0` + Listener.Addr() to remove port-collision flakes under -count and parallel runs. - Collapse the three near-identical Test_ListenAndServe* tests into a single table-driven subtest that actually asserts the args forwarded to ServeFunc/ServeTLSFunc/ServeTLSEmbedFunc. - Add coverage for the previously untested branches: CommandProducer returning err / nil cmd / unstarted cmd, initial OnChildSpawn error, OnMasterReady error, hook panic surfacing, RecoverInterval enforcement. - noopChildProducer helper kills + waits any spawned child binaries during cleanup so failed tests no longer leave subprocesses around. * fix(prefork): address Copilot review on #2199 - listen(): the *os.File wrapping the inherited fd was never closed. net.FileListener dups the fd, so the original was leaking on every child startup. Close it explicitly and return the dup'd listener. - setTCPListenerFiles(): if tcpListener.File() failed, the bound net.Listener stayed open and p.ln pointed at it. Close the listener on the error path and only assign p.ln after the dup succeeds. - prefork(): replace time.After in the RecoverInterval branch with a time.NewTimer that we Stop+drain when a shutdown signal wins the select, so the timer goroutine and channel allocation don't linger during crash-loop shutdown. - invokeHook(): drop the panic log line. The hook caller logs the returned error already, so logging in the recover block produced duplicate output for the same panic. * fix(prefork): harden recovery timer cleanup * fix(prefork): align follow-up with review feedback * fix(prefork): simplify child exit loop * fix(prefork): address review on slice copy and recover backoff - OnMasterReady now receives the internal childPIDs slice directly instead of a defensive copy; the doc states the slice is only valid for the call so callers copy it if they need to keep it. - RecoverInterval backoff moves from an inline time.Sleep in the supervision loop into the per-child Wait goroutine. Concurrent crashes now each restart RecoverInterval after they exit instead of serializing the wait through the loop. The wait is interruptible via ctx so shutdown does not have to outlast a full interval. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(prefork): silence modernize WaitGroup.Go suggestion The CI linter analyzes at go1.26 and modernize suggests sync.WaitGroup.Go, but that API needs go1.25 while the module targets go1.24 (so it would fail the build/vet there). Suppress the suggestion with an inline nolint instead of bumping the module's minimum Go version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(prefork): cancel Wait goroutines up front so shutdown is not blocked Move cancel() to the top of shutdownChildren so a child that already exited while parked on its RecoverInterval backoff (or a sigCh send) cannot delay teardown for a full RecoverInterval or ShutdownGracePeriod. cmd.Wait() is independent of the context, so the graceful SIGTERM wait still tracks the real process exits. Return early on the graceful path so the kill loop is skipped when every child is already gone. Follow-up review polish: - Clarify the RecoverInterval and OnChildRecover doc comments and document the previously undocumented Serve* fields. - Use sync.WaitGroup.Go (module targets go1.25) and drop the now-stale nolint:modernize. - Tests: replace the unreachable listener-close assertion with the real p.ln==nil contract, restore GOMAXPROCS in the child-path test, and add Test_childEnv plus Test_Prefork_ShutdownDoesNotBlockOnRecoverInterval. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prefork
Server prefork implementation.
Preforks master process between several child processes increases performance, because Go doesn't have to share and manage memory between cores.
WARNING: using prefork prevents the use of any global state!. Things like in-memory caches won't work.
- How it works:
import (
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/prefork"
)
server := &fasthttp.Server{
// Your configuration
}
// Wraps the server with prefork
preforkServer := prefork.New(server)
if err := preforkServer.ListenAndServe(":8080"); err != nil {
panic(err)
}
Benchmarks
Environment:
- Machine: MacBook Pro 13-inch, 2017
- OS: MacOS 10.15.3
- Go: go1.13.6 darwin/amd64
Handler code:
func requestHandler(ctx *fasthttp.RequestCtx) {
// Simulates some hard work
time.Sleep(100 * time.Millisecond)
}
Test command:
$ wrk -H 'Host: localhost' -H 'Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive' --latency -d 15 -c 512 --timeout 8 -t 4 http://localhost:8080
Results:
- prefork
Running 15s test @ http://localhost:8080
4 threads and 512 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 4.75ms 4.27ms 126.24ms 97.45%
Req/Sec 26.46k 4.16k 71.18k 88.72%
Latency Distribution
50% 4.55ms
75% 4.82ms
90% 5.46ms
99% 15.49ms
1581916 requests in 15.09s, 140.30MB read
Socket errors: connect 0, read 318, write 0, timeout 0
Requests/sec: 104861.58
Transfer/sec: 9.30MB
- non-prefork
Running 15s test @ http://localhost:8080
4 threads and 512 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 6.42ms 11.83ms 177.19ms 96.42%
Req/Sec 24.96k 5.83k 56.83k 82.93%
Latency Distribution
50% 4.53ms
75% 4.93ms
90% 6.94ms
99% 74.54ms
1472441 requests in 15.09s, 130.59MB read
Socket errors: connect 0, read 265, write 0, timeout 0
Requests/sec: 97553.34
Transfer/sec: 8.65MB