chore(server): pre-shutdown delay component (S01 Step 4)

Summary

Introduces the S01 pre-shutdown delay component at internal/server/shutdown.go: a LabKit v2 app.Component that flips a readiness flag at the start of Shutdown, blocks for the configured delay (to let the kubelet → endpoints-controller propagation finish), and emits a structured WARN when the shared shutdown context preempts the wait. Leaf code — no callers on this branch. Step 5 (server.New) and Step 6 (composition root) will register the component on app.App and wire Check through httpserver.Server.AddReadinessCheck("shutdown", delay.Check).

Acceptance criteria

AC Covered by
#21 (closed) (preemption returns nil + single WARN with configured_delay/elapsed/error) TestShutdownDelay_Shutdown_TruncatedByCtxEmitsWarn

The remaining 20 ACs from S01 are covered by other steps per the plan's coverage table.

Test plan

  • go test -race -short -count=1 ./internal/server/... — green
  • golangci-lint v2.12.0 — 0 issues
  • goimports -l — clean
  • All 6 plan-listed tests present (Name, Start_NoOp, Shutdown_BlocksForDelay, Shutdown_TruncatedByCtxEmitsWarn, Check_NilBeforeShutdown, Check_ErrorAfterShutdown)
  • Compile-time app.Component contract assertion (var _ app.Component = (*ShutdownDelay)(nil))
  • Polish tests pin three additional invariants: TestNewShutdownDelay_NilLoggerPanics (constructor fail-fast), TestShutdownDelay_Shutdown_IdempotentOnSecondCall (Swap-based idempotency), TestShutdownDelay_ZeroDelay_NoBlockNoWarn (cross-layer contract pin)

Reviewability notes

  • No callers yet. Leaf code; Step 5 instantiates and registers the component, Step 6 wires delay.Check to /-/readiness. The reviewer for Step 5 will see the constructor + type names locked in by this MR (any rename at that point is breaking).
  • WARN attribute keys are operator-facing contract. configured_delay, elapsed, and error are documented in docs/dev/logging.md under "Component-specific keys" so the keys appear in spec/doc search. Renaming them post-merge breaks any downstream Kibana/Loki query.
  • Nil-logger fail-fast. NewShutdownDelay panics on a nil logger at boot rather than risking a nil-pointer panic on the SIGTERM truncation path. The composition root must inject a LabKit-built logger.
  • Idempotent Shutdown. A second Shutdown call short-circuits via atomic.Bool.Swap, returning nil without re-blocking for the delay. LabKit's app.App invokes each component's Shutdown exactly once; the guard protects against operator-driven re-invocation (runbook scripts, retries).
  • Chore framing. This MR introduces no user-visible behavior until Step 6 wires the binary entry point at cmd/artifact-registry/. The component sits unwired in internal/server/ after merge — hence chore(server) and type::maintenance rather than feat/type::feature.

Merge request reports

Loading
Loading