Keep what an archive unpacked in the engine's own state directory, never beside the archive, and stop no slower than the host waits (issue 1585, review round 1)
mesh/delivery merged: source.main moved to novox/mesh-host@998d8f737008
mesh/check being carried to 5dc3a8e17f97 on 998d8f737008 | from fabfdf04f78a

The journal of an unpack sat at <target>.unpacked.json, in a directory
the module's account may write: a forged one would make the root engine
remove or adopt any path it named. It now lives under
<statedir>/unpacked/<sha256 of the target>.json, mode 0600, names its
target, and is refused whole when an entry is not filepath.IsLocal or a
parent is not above the target. It goes when the archive is taken off,
whether its state record still said what it unpacked or not.

The launcher's stop grace for a host on trial rises from 20 s to 75 s and
the unit's TimeoutStopSec is 120 s (openrc the same), past the host's own
60 s wait for the apply in flight, so neither cuts a save off again.

Tests folded as rows into existing ones (the repository is over its unit
budget): the journal's rows, a forged entry and parent refused, the
journal gone with a record that lost its Unpacked; the test that passed
on main dropped; the stop test waits on its own line, not a sleep, and no
longer consumes the save it waits for.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 10:52:42 +01:00
parent 768f637bd0
commit 5dc3a8e17f
13 changed files with 234 additions and 175 deletions
+18 -4
View File
@@ -140,15 +140,25 @@ func TestOnlyOneApplyRunsAtATime(t *testing.T) {
applying.Lock()
var said []string
var mu sync.Mutex
say := func(line string) { mu.Lock(); said = append(said, line); mu.Unlock() }
waiting := make(chan struct{})
say := func(line string) {
mu.Lock()
said = append(said, line)
mu.Unlock()
if strings.Contains(line, "waiting up to") {
close(waiting)
}
}
stopped := make(chan func(), 1)
go func() { stopped <- stopBetweenApplies(say, time.Hour) }()
// Said before it blocks, and it cannot end before the apply lets go: so it has not ended now.
<-waiting
select {
case release := <-stopped:
release()
applying.Unlock()
t.Fatal("the stop did not wait for the apply in flight")
case <-time.After(50 * time.Millisecond):
default:
}
if err := store.Save(path, store.State{Resources: []store.Applied{{ID: "nats.program-snapshot", Type: "archive"}}}); err != nil {
t.Fatal(err)
@@ -157,13 +167,17 @@ func TestOnlyOneApplyRunsAtATime(t *testing.T) {
release := <-stopped
saved := make(chan error, 1)
go func() { saved <- store.Save(path, store.State{}) }()
// A save after the stop blocks; nothing says it blocks, so this one wait is a bounded look.
select {
case <-saved:
t.Error("a save started after the stop")
release()
t.Fatal("a save started after the stop")
case <-time.After(50 * time.Millisecond):
}
release()
<-saved
if err := <-saved; err != nil {
t.Fatal(err)
}
mu.Lock()
defer mu.Unlock()
if len(said) != 2 || !strings.Contains(said[0], "waiting up to 1h0m0s") || !strings.Contains(said[1], "its state is saved") {