Add 80% limit warning, stop repeat notifications, fix review findings
Notifications: - Warn once when a counter reaches warn_percent of its limit, through separate warn_*_url endpoints. - Write ntf/ markers so a lost Redis reply cannot re-send a breach. - Retry only when the request never left or the web app answered 429/5xx. Fixes: - Retry transactions Redis rejected; treat pool timeouts as definite. - Install the signal handler before startup work. - Stop logging the login suffix; redact notify URL credentials. - Reject corrupt or empty CDBs, truncated hash tables included. - Require a Sentinel password, refuse example placeholders, and default admin_addr to localhost. - Normalise /counters domains, count on the arrival day, log rejections, hide internals in errors and exit 1 on a failed drain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c43ff17627
commit
54f9626121
@@ -44,6 +44,9 @@ var (
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
// beforeStartup lets tests act right after the signal handler is installed.
|
||||
var beforeStartup func()
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr, nil))
|
||||
}
|
||||
@@ -63,6 +66,18 @@ func run(args []string, stdout, stderr io.Writer, sigs <-chan os.Signal) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Installed before anything slow: a SIGHUP's default action is to kill the
|
||||
// process. Signals that arrive during startup wait in the channel.
|
||||
if sigs == nil {
|
||||
ch := make(chan os.Signal, 8)
|
||||
signal.Notify(ch, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(ch)
|
||||
sigs = ch
|
||||
}
|
||||
if beforeStartup != nil {
|
||||
beforeStartup()
|
||||
}
|
||||
|
||||
path, err := config.Discover(*configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
@@ -93,12 +108,6 @@ func run(args []string, stdout, stderr io.Writer, sigs <-chan os.Signal) int {
|
||||
}
|
||||
a.Serve(apiLn, adminLn)
|
||||
|
||||
if sigs == nil {
|
||||
ch := make(chan os.Signal, 2)
|
||||
signal.Notify(ch, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(ch)
|
||||
sigs = ch
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigs:
|
||||
@@ -115,6 +124,7 @@ func run(args []string, stdout, stderr io.Writer, sigs <-chan os.Signal) int {
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "shutdown: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case err := <-a.fatal:
|
||||
@@ -166,6 +176,11 @@ func newApp(cfg *config.Config) (*app, error) {
|
||||
logWriter.Close()
|
||||
return nil, err
|
||||
}
|
||||
if db.Count() == 0 {
|
||||
db.Close()
|
||||
logWriter.Close()
|
||||
return nil, fmt.Errorf("domains db %s holds no records", cfg.DomainsCDB)
|
||||
}
|
||||
a.db.Store(db)
|
||||
logger.Info("domains db loaded", "path", cfg.DomainsCDB, "domains", db.Count(), "max_cached_domains", cfg.MaxCachedDomains)
|
||||
|
||||
@@ -209,6 +224,7 @@ func newApp(cfg *config.Config) (*app, error) {
|
||||
|
||||
a.notifier = notify.New(notify.Config{
|
||||
OrgURL: cfg.Notify.OrgURL, AddressURL: cfg.Notify.AddressURL, DomainURL: cfg.Notify.DomainURL,
|
||||
WarnOrgURL: cfg.Notify.WarnOrgURL, WarnAddressURL: cfg.Notify.WarnAddressURL, WarnDomainURL: cfg.Notify.WarnDomainURL,
|
||||
User: cfg.Notify.User, Password: cfg.Notify.Password, Timeout: cfg.Notify.Timeout(),
|
||||
}, server.NotifyHooks(tx, a.m))
|
||||
|
||||
@@ -220,8 +236,9 @@ func newApp(cfg *config.Config) (*app, error) {
|
||||
}
|
||||
return db.Lookup(domain)
|
||||
}),
|
||||
Exempt: cfg.ExemptSet(),
|
||||
CountJunk: cfg.CountJunk,
|
||||
Exempt: cfg.ExemptSet(),
|
||||
CountJunk: cfg.CountJunk,
|
||||
WarnPercent: cfg.Notify.EffectiveWarnPercent(),
|
||||
}
|
||||
a.srv = server.New(server.Deps{
|
||||
AuthToken: cfg.AuthToken,
|
||||
|
||||
@@ -415,3 +415,50 @@ func TestRun_SignalLoop(t *testing.T) {
|
||||
t.Errorf("log:\n%s", e.logs())
|
||||
}
|
||||
}
|
||||
|
||||
// A SIGHUP while the daemon starts must not kill it, and the reload it asked
|
||||
// for runs once the daemon is ready.
|
||||
func TestRun_SIGHUPDuringStartupIsQueued(t *testing.T) {
|
||||
e := newEnv(t, "")
|
||||
beforeStartup = func() { syscall.Kill(os.Getpid(), syscall.SIGHUP) }
|
||||
t.Cleanup(func() { beforeStartup = nil })
|
||||
done := make(chan int, 1)
|
||||
var out, errb bytes.Buffer
|
||||
go func() { done <- run([]string{"-config", e.cfgPath}, &out, &errb, nil) }()
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for !strings.Contains(e.logs(), "domains db reloaded") {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("the early SIGHUP was not honoured; log:\n%s", e.logs())
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
syscall.Kill(os.Getpid(), syscall.SIGTERM)
|
||||
select {
|
||||
case code := <-done:
|
||||
if code != 0 {
|
||||
t.Errorf("exit code %d, stderr %q", code, errb.String())
|
||||
}
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("run did not return after SIGTERM")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewApp_EmptyCDBIsFatal(t *testing.T) {
|
||||
e := newEnv(t, "")
|
||||
w, err := cdb.Create(e.cdbPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := config.Load(e.cfgPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a, err := newApp(cfg); err == nil {
|
||||
a.Shutdown(context.Background())
|
||||
t.Fatal("an empty domains db must be fatal at startup")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user