@@ -310,11 +310,13 @@ func main() {
310310 // evalMu (inside swapRuntime) only guards the swap; applyMu is strictly outside it.
311311 var applyMu sync.Mutex
312312 // seedFn seeds a freshly built engine's alert windows from durable history on a reload,
313- // so a target that is newly alerted or redefined isn't left dark until X fresh samples
314- // arrive (CODE_REVIEW #4). Assigned once the store exists (below); the SIGHUP goroutine and
315- // the API apply both pass it into swapRuntime. It reads the store, so swapRuntime runs it
316- // before taking evalMu. nil until assigned — an early reload simply skips seeding.
317- var seedFn func (* runtime )
313+ // so a target with no live window to inherit (e.g. one that just gained an alert) isn't
314+ // left dark until X fresh samples arrive (CODE_REVIEW #4). swapRuntime picks which targets
315+ // it may seed (reloadSeedTargets). Assigned once the store exists (below); the SIGHUP
316+ // goroutine and the API apply both pass it into swapRuntime. It reads the store, so
317+ // swapRuntime runs it before taking evalMu. nil until assigned — an early reload simply
318+ // skips seeding.
319+ var seedFn func (* runtime , []model.Monitor )
318320 if * configPath != "" {
319321 fmt .Printf ("config: %d targets from %s\n " , len (rt .jobs ), * configPath )
320322 }
@@ -347,11 +349,11 @@ func main() {
347349 }
348350
349351 // Now that the store exists, wire the reload seed: on a config reload, swapRuntime seeds
350- // the new engine from durable history for targets it can't inherit (redefined or newly
351- // alerted) , then InheritStateFrom overwrites the seed for same-identity targets (#4).
352- seedFn = func ( nrt * runtime ) {
353- warmStartAlerts ( ctx , nrt . engine , nrt . monitors , nrt . metricByName , st , time . Now ())
354- }
352+ // the new engine from durable history for every target whose measurement identity is
353+ // unchanged , then InheritStateFrom overwrites the seed with the live window where one
354+ // exists, so the seed only fills windows the running engine doesn't have, e.g. a newly
355+ // alerted target's (#4).
356+ seedFn = historySeed ( ctx , st )
355357
356358 // SIGHUP reloads the config; on error the running config is kept (a bad edit can't take the
357359 // collector down). Alert firing state and sample windows carry over from the running engine
@@ -385,7 +387,10 @@ func main() {
385387 // that is already breaching at startup fires on its first new round instead of
386388 // waiting X fresh samples (the durable-store replacement for SmokePing's S sentinel).
387389 // Boot uses this directly; a SIGHUP/API reload runs the same seed via seedFn inside
388- // swapRuntime and then carries unchanged windows/state via InheritStateFrom.
390+ // swapRuntime and then carries unchanged windows/state via InheritStateFrom. Unlike a
391+ // reload, boot has no previous fingerprint to compare (the store keeps none), so a target
392+ // redefined while smoked was down can still be seeded from its old measurement's rounds
393+ // if they are recent enough to pass recentContiguous.
389394 if rt := current .Load (); rt .engine != nil {
390395 warmStartAlerts (ctx , rt .engine , rt .monitors , rt .metricByName , st , time .Now ())
391396 }
@@ -822,6 +827,27 @@ func main() {
822827 }
823828}
824829
830+ // createOwnerOnly opens path for writing (created or truncated) readable by its owner only, for a
831+ // file that will hold a secret. A new file is created 0600 whatever the umask; an existing regular
832+ // file with any group/other permission bits is tightened to 0600 before the caller writes to it, so
833+ // overwriting a world-readable file can't leave the secret world-readable. A non-regular path (a
834+ // FIFO, /dev/stdout) is left alone: its mode isn't this file's to change.
835+ func createOwnerOnly (path string ) (* os.File , error ) {
836+ f , err := os .OpenFile (path , os .O_WRONLY | os .O_CREATE | os .O_TRUNC , 0o600 )
837+ if err != nil {
838+ return nil , err
839+ }
840+ fi , err := f .Stat ()
841+ if err == nil && fi .Mode ().IsRegular () && fi .Mode ().Perm ()& 0o077 != 0 {
842+ err = f .Chmod (0o600 )
843+ }
844+ if err != nil {
845+ f .Close ()
846+ return nil , err
847+ }
848+ return f , nil
849+ }
850+
825851// vantageCmd implements `smoked vantage <add NAME|ls|revoke NAME> [-dsn DSN]` — provisioning
826852// against the same TimescaleDB the daemon uses. The subcommand and the vantage NAME are
827853// positional (git-style), so flags may follow them: `vantage add nyc -dsn X`. -dsn defaults
@@ -886,7 +912,8 @@ func vantageCmd(args []string) int {
886912
887913 switch {
888914 case * out != "" :
889- f , err := os .Create (* out )
915+ // The bundle embeds the client private key: owner-only, never os.Create's 0666-umask.
916+ f , err := createOwnerOnly (* out )
890917 if err != nil {
891918 fmt .Fprintf (os .Stderr , "vantage add: %v\n " , err )
892919 return 1
@@ -1257,16 +1284,17 @@ func (rt *runtime) commitRemote(ctx context.Context, ing store.ResultIngester, o
12571284// state + sample windows over from the running engine (so a reload/apply doesn't
12581285// re-fire alerts already firing or drop hysteresis history). Serialized against a
12591286// round's alert eval via evalMu. Shared by the SIGHUP reload and the config-apply API.
1260- func swapRuntime (current * atomic.Pointer [runtime ], evalMu * sync.Mutex , nrt * runtime , seed func (* runtime )) {
1287+ func swapRuntime (current * atomic.Pointer [runtime ], evalMu * sync.Mutex , nrt * runtime , seed func (nrt * runtime , monitors []model. Monitor )) {
12611288 // Seed the new engine's windows from durable history BEFORE the swap and OUTSIDE evalMu.
1262- // A target whose measurement identity changed, or one that just gained its first alert,
1263- // won't inherit a window below — without seeding it would start dark and fire late
1264- // (CODE_REVIEW #4). recentContiguous filters the seed to the current host/probe, so a
1265- // redefined target pulls nothing stale. nrt.engine isn't live yet, so this needs no lock,
1266- // keeping the store read off the evalMu critical path. Inherit then overwrites the seed for
1267- // same-identity targets with their live window.
1289+ // A target that just gained its first alert has no window to inherit below — without
1290+ // seeding it would start dark and fire late (CODE_REVIEW #4). A target whose measurement
1291+ // identity changed is not seeded at all (reloadSeedTargets): its stored rounds measured the
1292+ // old definition. nrt.engine isn't live yet, so this needs no lock, keeping the store read
1293+ // off the evalMu critical path. Inherit then overwrites the seed for same-identity targets
1294+ // with their live window. Reading current here, outside evalMu, is safe because every caller
1295+ // holds applyMu, so no other swap can land before the locked Load below.
12681296 if seed != nil && nrt .engine != nil {
1269- seed (nrt )
1297+ seed (nrt , reloadSeedTargets ( current . Load (), nrt ) )
12701298 }
12711299 evalMu .Lock ()
12721300 old := current .Load ()
@@ -1277,6 +1305,26 @@ func swapRuntime(current *atomic.Pointer[runtime], evalMu *sync.Mutex, nrt *runt
12771305 evalMu .Unlock ()
12781306}
12791307
1308+ // reloadSeedTargets returns the monitors a reload may warm-start from durable history: all of
1309+ // nrt's except a target whose measurement identity (targetFP) changed since old. The store keeps
1310+ // no fingerprint and warmStartAlerts matches history only on host/probe/metric/cadence, so the
1311+ // recent rounds it would find for a redefined target were measured under the old definition:
1312+ // moving a TCPConnect target from port 443 to 22 would count port-443 failures toward the port-22
1313+ // consecutive-loss matcher and fire it early. A redefined target starts with an empty window and
1314+ // waits for X rounds of its new measurement. A same-identity target stays seedable (it inherits
1315+ // its live window over the seed, or needs the seed if it just gained an alert), and so does a
1316+ // target absent from old, which has no previous identity to compare (as at boot).
1317+ func reloadSeedTargets (old , nrt * runtime ) []model.Monitor {
1318+ out := make ([]model.Monitor , 0 , len (nrt .monitors ))
1319+ for _ , m := range nrt .monitors {
1320+ if was , ok := old .targetFP [m .ID ]; ok && was != nrt .targetFP [m .ID ] {
1321+ continue
1322+ }
1323+ out = append (out , m )
1324+ }
1325+ return out
1326+ }
1327+
12801328// reloadIdentity builds the per-target/per-alert identity InheritStateFrom uses to decide what
12811329// survives a reload: which targets exist (ValidTarget) with an unchanged measurement identity
12821330// (SameTarget), which alerts are attached to each target now (Attached), and whether each
@@ -1325,8 +1373,9 @@ func sameStringSet(x, y []string) bool {
13251373// the old and new runtime, using the same federation.Fingerprint (probe/host/params/pings/
13261374// probe-config) the ingest and in-flight-drop paths key on — so the reload inherit-gate can't
13271375// disagree with them. A target that is new, removed, or redefined in ANY of those fields is
1328- // absent (false), so InheritStateFrom carries neither its window nor its firing/visible state;
1329- // it is seeded fresh from durable history instead (CODE_REVIEW #4).
1376+ // absent (false), so InheritStateFrom carries neither its window nor its firing/visible state
1377+ // (CODE_REVIEW #4). A new target is seeded from durable history; a redefined one is not
1378+ // (reloadSeedTargets), since that history measured its old definition.
13301379func sameTargetIdentity (old , nrt * runtime ) map [string ]bool {
13311380 same := make (map [string ]bool , len (nrt .targetFP ))
13321381 for name , fp := range nrt .targetFP {
@@ -1342,7 +1391,7 @@ func sameTargetIdentity(old, nrt *runtime) map[string]bool {
13421391// runtime in. Validate → persist → swap: an invalid doc never persists, a stale
13431392// version never swaps. Returns api.ErrConfigInvalid / api.ErrConfigConflict.
13441393func applyConfig (cfgStore * configstore.Store , current * atomic.Pointer [runtime ], evalMu * sync.Mutex ,
1345- build func (dbFragment func () ([]byte , error )) (* runtime , error ), doc json.RawMessage , expectedVersion int , seed func (* runtime )) error {
1394+ build func (dbFragment func () ([]byte , error )) (* runtime , error ), doc json.RawMessage , expectedVersion int , seed func (* runtime , []model. Monitor )) error {
13461395 nrt , berr := build (func () ([]byte , error ) { return doc , nil })
13471396 if berr != nil {
13481397 return fmt .Errorf ("%w: %v" , api .ErrConfigInvalid , berr )
@@ -1378,6 +1427,14 @@ func alertRTT(o scheduler.Outcome) float64 {
13781427 return o .Computed .Median
13791428}
13801429
1430+ // historySeed returns the reload seed swapRuntime runs: it warm-starts the given monitors' alert
1431+ // windows in nrt's not-yet-live engine from st's durable history, as of when it runs.
1432+ func historySeed (ctx context.Context , st store.Store ) func (nrt * runtime , monitors []model.Monitor ) {
1433+ return func (nrt * runtime , monitors []model.Monitor ) {
1434+ warmStartAlerts (ctx , nrt .engine , monitors , nrt .metricByName , st , time .Now ())
1435+ }
1436+ }
1437+
13811438// warmStartLookback bounds how far back a remote vantage's warm-start history read goes.
13821439// SeedWindow trims to the deepest matcher window anyway; this just caps the query.
13831440const warmStartLookback = 24 * time .Hour
0 commit comments