Bind the app port deliberately and document the proxy deployment (closes #268) (closes #226)
All checks were successful
check / check (push) Successful in 3m4s
All checks were successful
check / check (push) Successful in 3m4s
This commit was merged in pull request #277.
This commit is contained in:
91
internal/server/early_shutdown_test.go
Normal file
91
internal/server/early_shutdown_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package server_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/webhooker/internal/globals"
|
||||
"sneak.berlin/go/webhooker/internal/server"
|
||||
)
|
||||
|
||||
// earlyStopIterations is how many start/stop cycles the race test
|
||||
// runs. The window it aims at is the gap between the OnStart hook
|
||||
// returning and the serving goroutine reaching its first field
|
||||
// access, which is microseconds wide. The race detector reports an
|
||||
// unsynchronised pair whenever it observes one, but it has to observe
|
||||
// one, so a single cycle can miss purely on scheduling. Repetition
|
||||
// makes the observation reliable; the collaborators are built once,
|
||||
// so the cycles themselves are cheap.
|
||||
const earlyStopIterations = 25
|
||||
|
||||
// TestEarlyShutdown_NoPanicAndNoRace stops the application
|
||||
// immediately after starting it, before the serving goroutine has
|
||||
// necessarily run at all.
|
||||
//
|
||||
// Two defects live in that window. The OnStart hook returns as soon
|
||||
// as it has spawned the serving goroutine, so fx runs the stop
|
||||
// sequence against a Server whose serving goroutine may not have
|
||||
// executed a single line. cleanShutdown called Shutdown on an
|
||||
// httpServer that goroutine was supposed to assign, which was a nil
|
||||
// dereference on an early SIGTERM; and it read httpServer and
|
||||
// sentryEnabled with nothing ordering those reads against the
|
||||
// goroutine's writes, which is a data race that only surfaces once
|
||||
// something both starts and stops the server. Nothing did before this
|
||||
// test: the listen-failure test never binds, and the router tests
|
||||
// bypass the lifecycle entirely.
|
||||
//
|
||||
// httpServer is now built in New, on the constructing goroutine, so
|
||||
// it is written before any hook exists and can never be nil.
|
||||
// sentryEnabled is atomic. This test is what catches either one
|
||||
// coming back — under -race, which is how the suite runs.
|
||||
func TestEarlyShutdown_NoPanicAndNoRace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Built once: the collaborators are not what is under test, and
|
||||
// standing up a database per iteration would make repetition too
|
||||
// expensive to be worth having.
|
||||
env := newTestEnv(t)
|
||||
env.cfg.BindAddress = loopbackV4
|
||||
|
||||
for range earlyStopIterations {
|
||||
requireStartStopIsClean(t, env)
|
||||
}
|
||||
}
|
||||
|
||||
// requireStartStopIsClean runs one start/stop cycle with no wait in
|
||||
// between, failing the test if either half errors.
|
||||
//
|
||||
// Each cycle gets a fresh fx app, so the Server under test is
|
||||
// constructed anew every time — that construction is where the
|
||||
// httpServer write now happens, and reusing one Server would test it
|
||||
// only once.
|
||||
func requireStartStopIsClean(t *testing.T, env *testEnv) {
|
||||
t.Helper()
|
||||
|
||||
env.cfg.Port = freePort(t)
|
||||
|
||||
app := fx.New(
|
||||
fx.NopLogger,
|
||||
fx.Supply(env.log, env.cfg, env.mw, env.hnd),
|
||||
fx.Provide(globals.New, server.New),
|
||||
fx.Invoke(func(*server.Server) {}),
|
||||
)
|
||||
|
||||
startCtx, cancelStart := context.WithTimeout(
|
||||
context.Background(), lifecycleTimeout,
|
||||
)
|
||||
defer cancelStart()
|
||||
|
||||
require.NoError(t, app.Start(startCtx))
|
||||
|
||||
// No sleep and no readiness wait: stopping while the serving
|
||||
// goroutine is still in flight is the whole point.
|
||||
stopCtx, cancelStop := context.WithTimeout(
|
||||
context.Background(), lifecycleTimeout,
|
||||
)
|
||||
defer cancelStop()
|
||||
|
||||
require.NoError(t, app.Stop(stopCtx))
|
||||
}
|
||||
Reference in New Issue
Block a user