From ab3722d3b35bb11103c8d9b192bd46ce8afbab30 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 22:53:39 +0200 Subject: [PATCH 1/2] Test that a fill the node-engine failed is tried again when its socket returns (issue 1873) --- .../socket_integration_test.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go index 094a7e29..049b0be7 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/socket_integration_test.go @@ -106,3 +106,38 @@ func TestWatchSocketWakesWhenTheSocketIsMadeAndWhenItsDirectoryReturns(t *testin defer l.Close() heard("the directory and socket back") } + +// Issue 1873: a fill the node-engine refused for a reason of its own (on ace, "read …/superuser.secret: is a +// directory", what a container runtime had left there) is tried again when a node-engine makes its socket again, as a +// newer engine may write what the older could not. Before, only a fill that failed for want of the socket was, so +// ace's postgres stayed unplaced after the engine that cures it was delivered, and its old failure was said every 30 s. +func TestNatsAFillTheEngineFailedIsTriedWhenTheSocketReturns(t *testing.T) { + b := startBus(t) + heard := b.listen(t) + e := newEngine(t, map[string]string{dunstrc: "owned"}) + if err := os.MkdirAll(e.at(dunstrc), 0o755); err != nil { + t.Fatal(err) + } + stop := e.listen(t) + b.config(t, "dunst", "font-size", "13") + b.assign(t, "dunst", dunst(t)) + aClient(t, b, e, t.TempDir()) + + eventually(t, "the engine's failure is said", func() bool { + r, _ := heard.last("dunst") + return r.Status == StatusFailed && strings.Contains(r.Reason, "is a directory") + }) + // The node-engine replaces itself with one that clears what the runtime left. + stop() + if err := os.Remove(e.at(dunstrc)); err != nil { + t.Fatal(err) + } + e.listen(t) + eventually(t, "dunstrc is written by the new engine", func() bool { + return strings.Contains(e.read(t, dunstrc), "Monospace 13") + }) + eventually(t, "and the report says applied", func() bool { + r, _ := heard.last("dunst") + return r.Status == StatusApplied + }) +} -- 2.54.0 From 16331cdfdce607063c6ad693a353bfb0679f7067 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 22:56:16 +0200 Subject: [PATCH 2/2] Fill a module again when the node-engine's socket returns after any failed fill, not only one for want of the socket (issue 1873) On ace postgres's fill of superuser.secret failed at 22:12 with the engine's own "is a directory", what a container runtime had left at the path. The engine delivered at 22:42 clears such a directory, but the client tried again only modules whose fill had failed for want of the socket (issue 1165), so postgres was never filled again and its old failure was said every 30 s. A newer engine may write what the older could not: any failed fill is now tried again when a node-engine makes its socket again. --- .../cmd/mesh-provisioner-client/client.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go index 7bd28b50..489d5d28 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client.go @@ -120,7 +120,7 @@ type Client struct { // Now dates the reports; nil is time.Now. Now func() time.Time // SocketUp wakes the client when the node-engine's write socket is made again (watchSocket); nil never does. - // A module whose write failed for want of the socket is filled again then (issue 1165), and nothing else is. + // A module whose write failed is filled again then (issue 1165, issue 1873), and nothing else is. SocketUp <-chan struct{} // watchBound watches a provider's bucket of entries for this machine's keys, once per run (consumers.go); set by @@ -150,7 +150,8 @@ type state struct { // of the consumers left out of one of its files. providing map[string]bool leftOut map[string]string - // unplaced is a module whose last fill failed for want of the node-engine's write socket (issue 1165). + // unplaced is a module whose last fill failed, for want of the node-engine's write socket (issue 1165) or by the + // engine's own refusal (issue 1873): filled again when the socket is made again. unplaced map[string]bool // bound is each provider's bucket of entries this client watches, by key: this machine's consumers' entries alone; // boundReady is a bucket whose current entries are all read, and boundUsers the modules filled from each. @@ -705,9 +706,10 @@ func (c *Client) reconcile(ctx context.Context, kvs map[string]jetstream.KeyValu err = errFill(a) } if err != nil { - if socketAbsent(err) { - s.unplaced[module] = true - } + // Filled again when a node-engine makes its socket again: one gone (issue 1165), or one that refused for a + // reason of its own, which a newer engine may write (issue 1873: "is a directory", what a container + // runtime left at the path, cleared by the engine that followed). + s.unplaced[module] = true failed = append(failed, fmt.Sprintf("%s: %v", f.Path, err)) continue } -- 2.54.0