Refuse an event heard during a reload instead of panicking, and judge a bundle by its entrypoint alone (issue 1945, review)

An event a module's consumer heard after the module was dropped for a
reload read a deleted entry and panicked node-tools, taking every tool on
the machine down. The handler now refuses an event of a consumer that is no
longer the module's, and a stopped hearing hands nothing on and offers the
event again at once.

A bundle that writes beside itself (a cache, a socket) loaded itself again
after every quiet spell, because the signature took in every file in its
directory. A delivery puts every file in place anew, so the entrypoint alone
tells a new build. The watch also looks once as it begins, so a bundle that
came between its load and the watch is not missed.

Tests: 1945/AC23 and AC24 in internal/runtime/reload_test.go, and a stopped
hearing in internal/bus/hearing_stopped_test.go.
This commit is contained in:
2026-10-25 15:26:02 +01:00
parent b0ded57294
commit d8ac69904d
5 changed files with 246 additions and 37 deletions
+8 -13
View File
@@ -87,8 +87,9 @@ func (w *bundleWatch) arm() {
func (w *bundleWatch) loop() {
defer close(w.done)
buf := make([]byte, 64*1024)
pending := false
var quietAt time.Time
// Looked at once as the watch begins: a bundle that came between its load and the watch is not missed.
pending := true
quietAt := time.Now()
for {
timeout := -1
if pending {
@@ -149,8 +150,10 @@ func (w *bundleWatch) drain(buf []byte) {
}
// signatureOf is what the runtime compares to tell that an entrypoint changed: the file's identity,
// mode, size and times, and those of every file beside it (a launcher starts what its build wrote
// next to it). "" is a file that does not exist.
// mode, size and times; "" is a file that does not exist. Only the entrypoint: a delivery puts every file
// of a bundle in place anew (a directory swapped in, or each file renamed in), so the entrypoint is new
// whenever its build is, while a file beside it may be one the bundle itself writes (a cache, a socket),
// which would load it again after every quiet spell, and modules sharing a directory each other.
func signatureOf(path string) string {
info, err := os.Stat(path)
if errors.Is(err, fs.ErrNotExist) {
@@ -159,15 +162,7 @@ func signatureOf(path string) string {
if err != nil {
return "unreadable: " + err.Error()
}
sig := statSignature(info)
if entries, err := os.ReadDir(filepath.Dir(path)); err == nil {
for _, e := range entries {
if i, err := e.Info(); err == nil {
sig += "|" + e.Name() + " " + statSignature(i)
}
}
}
return sig
return statSignature(info)
}
func statSignature(info fs.FileInfo) string {