package main import ( "testing" "time" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/server" ) // dockerStopGrace is Docker's default `docker stop` grace period. // The Dockerfile sets no STOPSIGNAL or grace override, so this is // the deadline the container is actually held to, and the fx stop // timeout has to fit inside it with room for signal delivery and // process exit. const dockerStopGrace = 10 * time.Second // TestNewApp_StopTimeout pins the fx stop timeout. Without the // explicit fx.StopTimeout option the app reads fx's 15s // DefaultTimeout, which exceeds dockerStopGrace: the container is // SIGKILLed before the bound fires and every shutdown hook bounded // by it — including the operator-facing timeout log — becomes // unreachable in the image this repo produces. // // fx.New applies options before it executes invokes, so the timeout // is set whether or not the graph itself can be constructed here. func TestNewApp_StopTimeout(t *testing.T) { t.Setenv("DATA_DIR", t.TempDir()) got := newApp().StopTimeout() require.Equal(t, stopTimeout, got) require.Less(t, got, dockerStopGrace) } // tailHeadroom is the slack the fx stop budget must keep beyond the // server stop hook. The hooks that run after the server — the // delivery engine, the healthcheck, the webhook DB manager and the // database close — are microsecond-scale in normal operation, so // this is generous for them. const tailHeadroom = 2 * time.Second // TestStopTimeout_LeavesHeadroomForTailHooks pins the relationship // between the server's stop hook and the fx stop budget. fx bounds // the whole stop sequence, and returns without running its // remaining hooks once the stop context has expired. If the hook // could use the entire budget, every later hook — the database close // included — would be skipped in exactly the case where the drain // mattered. // // The hook is not just the HTTP drain: a Sentry flush follows it in // the same hook, and sentry.Flush honours no context, so both halves // have to be counted. The sweep walks every drain length the hook // can produce, since a shorter drain leaves the flush more room and // the worst case is not necessarily at either extreme. // // Shrinking either budget, or unbounding the flush again, must fail // here rather than silently recreating a hook that swallows the // whole sequence. func TestStopTimeout_LeavesHeadroomForTailHooks(t *testing.T) { t.Parallel() require.Less(t, server.ShutdownTimeout, stopTimeout) const step = 10 * time.Millisecond for drain := time.Duration(0); drain <= server.ShutdownTimeout; drain += step { hook := drain + server.SentryFlushBudget(stopTimeout-drain) require.LessOrEqual( t, hook+tailHeadroom, stopTimeout, "a %s drain leaves the tail hooks short", drain, ) } }