Files
go-admin/tools/checksilent/main.go
T
zhangwenjian 8faa8d2aed feat✨: check the shutdown budget against every stop deadline
The budget is one number in config/settings.yml. The deadlines that have to
cover it are in four other files, none of which anybody edits while thinking
about shutdown - so raising the budget passes every test, deploys, and has the
cleanup callbacks killed on the next release.

Two checks share one arithmetic and one five-second margin.

shutdown-budget-overruns-grace compares preStop + drain + server + cleanup
against terminationGracePeriodSeconds in the shipped manifest. Those two files
are not merely adjacent examples: scripts/k8s/prerun.sh builds the
settings-admin ConfigMap out of config/settings.yml and the Deployment mounts
it, so the manifest deploys that file.

docker-stop-cuts-shutdown-short covers the three ways this container is
stopped: `docker stop` in the release workflow, the same in the Makefile, and
stop_grace_period on a compose service that runs this repository's own image. A
service running a database is not this process and is left alone. The duration
is parsed rather than scanned for digits - compose accepts 1m30s, and reading
the first number out of it would call ninety seconds one.

All three spellings of the deadline are read: --timeout, the deprecated --time,
and the short -t. A deadline the check cannot read is reported as no deadline at
all, so recognising only one of them would call a correct command broken and
send whoever fixed it towards the spelling docker is retiring. The message
quotes the flag back in the spelling it was written in, for the same reason:
suggesting a flag the line does not use is how a tool teaches people to
disbelieve it.

What neither covers is `docker rm -f`, which has no deadline to compare
against: it is SIGKILL by definition. That gap is deliberate, and it is why the
previous commit changed the one place that used it on a container that might
still be running.

Both report at two levels. A budget that already overruns is an ERROR; one that
fits with nothing to spare is a WARN, because it works today and failing the
build on a working configuration is how a project teaches people to ignore its
warnings. The two are exclusive: an overrun satisfies the headroom condition as
well, and an ERROR that always drags a duplicate WARN behind it teaches the same
lesson.

preStop is in the sum although the shipped manifest has no hook. That is the
point - a hook added later is spent before the process is told anything, and a
self-check that could not see it would understate the real budget by however
long somebody set it to, which is worse than not checking. A hook whose duration
cannot be read is reported rather than counted as zero.

The fallbacks for fields the settings file leaves out are read from the
constants in the scanned tree, not copied here; if they are renamed the run
stops instead of going quiet with the wrong numbers.

The wording differs by audience on purpose. At run time this is somebody else's
deployment under constraints the process cannot see, so the log states a
minimum. These checks read files this repository owns, where there is standing
to ask for headroom, so they name a target.

The table in AGENTS.md is relisted while it is being touched: the two new
checks, plus datascope-route-unguarded, which has been missing since it was
added. The hard-coded count is gone - it said seven and there were ten, which is
what a written-down count does. AGENTS.md and docs/contract.md both sent readers
to `go run ./tools/checksilent -h` for the list of checks; that prints
command-line flags and has never printed a check, so both now point at
runChecks.

The yaml parser moves from an indirect requirement to a direct one - it was
already in the module graph - and tidy drops four go.sum lines left over from
two older releases of core.
2026-09-06 22:07:08 +08:00

128 lines
4.0 KiB
Go

// Command checksilent reports the failures in this repository that do not
// announce themselves: no error, no log line, behaviour quietly wrong.
//
// An ERROR fails the run; a WARN prints and does not. The split is not about
// how bad the consequence is - every one of these is bad - but about how much
// room is left to act.
//
// Most of them report only ERROR: each is decided from this repository's own
// files and is either true or not. The menu-name check reports only WARN,
// because it compares against a second repository through a regular
// expression, and a check that can be wrong must not be able to stop a build
// or the first response to it will be an ignore comment. The two
// shutdown-budget checks report at both levels from one arithmetic: a budget
// that already overruns is an ERROR, and one that fits with no headroom left
// is a WARN - it works today, so failing the build on it would be failing a
// correct configuration.
//
// The list of checks is runChecks in checks.go. It is deliberately not
// repeated here as a count: the two places that carried one were both wrong by
// the time anybody looked.
//
// Usage:
//
// go run ./tools/checksilent
// go run ./tools/checksilent -ui-dir ../go-admin-ui/src
// go run ./tools/checksilent -json
//
// Or through the Makefile, which is what CI runs:
//
// make checksilent
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"strings"
)
func main() {
var (
root = flag.String("root", ".", "repository root to scan")
uiDir = flag.String("ui-dir", "", "go-admin-ui src directory; enables the menu-name check, which is skipped without it")
asJSON = flag.Bool("json", false, "print findings as JSON")
)
flag.Parse()
code, err := run(os.Stdout, *root, options{UIDir: *uiDir}, *asJSON)
if err != nil {
fmt.Fprintln(os.Stderr, "checksilent:", err)
os.Exit(2)
}
os.Exit(code)
}
// run returns the process exit code: 0 when nothing or only warnings were
// found, 1 when at least one error was.
func run(w io.Writer, root string, opt options, asJSON bool) (int, error) {
s, err := load(root)
if err != nil {
return 0, err
}
findings, err := runChecks(s, opt)
if err != nil {
return 0, err
}
if asJSON {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err = enc.Encode(findings); err != nil {
return 0, err
}
} else {
for _, f := range findings {
fmt.Fprintln(w, f)
}
printSummary(w, findings, opt, s)
}
if hasError(findings) {
return 1, nil
}
return 0, nil
}
func printSummary(w io.Writer, findings []Finding, opt options, s *snapshot) {
var errors, warnings int
for _, f := range findings {
if f.severity == Error {
errors++
} else {
warnings++
}
}
if len(findings) > 0 {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "checksilent: %d error(s), %d warning(s)\n", errors, warnings)
if warnings > 0 {
fmt.Fprintln(w, "Warnings do not affect the exit code.")
}
if opt.UIDir == "" {
fmt.Fprintf(w, "The %s check was skipped: pass -ui-dir <go-admin-ui>/src to run it.\n", checkMenuName)
}
// Said out loud so nobody reads a clean run as "the boundary holds
// everywhere it was declared". core/ is a separate module and has no
// directory here, so this repository's copy of the check cannot cover it.
if scanned, absent := ScannedContractRoots(s); len(absent) > 0 {
fmt.Fprintf(w, "The %s check covered %s; %s does not exist here and was not scanned.\n",
checkImportBoundary, strings.Join(scanned, ", "), strings.Join(absent, ", "))
}
// Same reason: a tree with no alias into core's contract packages gives
// this check nothing to look at, and its silence must not be read as a
// pass. That is now the interesting case rather than the expected one -
// the shims exist, so a count of zero means they stopped being aliases,
// or stopped being here.
if n := ScannedShimAliases(s); n == 0 {
fmt.Fprintf(w, "The %s check found no type alias into core's contract packages and guarded nothing.\n",
checkShimAlias)
} else {
fmt.Fprintf(w, "The %s check covered %d type alias(es) into core's contract packages.\n",
checkShimAlias, n)
}
}