summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 12:11:23 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commita8248e91873f040eed1fd24d5af7c0ca285483ef (patch)
treec958a35a41a1b95f6b1df27b83e0b026f816b7fa
parent8b18331f5e3f6c346b992019163ec3f5b6e6b721 (diff)
downloadpardes-a8248e91873f040eed1fd24d5af7c0ca285483ef.tar.gz
pardes-a8248e91873f040eed1fd24d5af7c0ca285483ef.zip
A repeated err record in the log is the newest record counted, not another line
A client retrying a write that fails the same way pushed one err line after another until the log's ring held nothing else. The same err as the newest record is now that record with a count, (x4), as +Messages counts repeats; a follower that has already read the record still gets each repeat as its own line, since it may be waiting on exactly that. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md5
-rw-r--r--src/ninep/events.zig50
2 files changed, 53 insertions, 2 deletions
diff --git a/docs/fs.md b/docs/fs.md
index ffb509cf..bd8ca055 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -335,7 +335,10 @@ includes each builtin announcing itself as it runs), and `err <serial|->
<file>: <why>` for every write or truncation the tree refused or that
failed -- through a mount a shell sees only the errno its kernel mapped the
reply to, usually `Invalid argument`, and this is the reason (`err 3 addr:
-no match for regexp`). There is no per-pane error file to read instead:
+no match for regexp`). The same err again, before any follower has read
+the first, is that record counted (`err 3 addr: no match for regexp (x4)`),
+so a client retrying a failing write does not push the rest out of the
+ring; a follower that read it gets each repeat. There is no per-pane error file to read instead:
acme's `errors` only takes text, and one record stream is simpler to watch
than a file per pane. A `msg` said while a
pane is being made can precede that pane's `new`; panes present at boot are
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index 8ee25b74..69dc271d 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -177,7 +177,42 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void {
};
}
var buf: [256]u8 = undefined;
- pushLog(p, std.fmt.bufPrint(&buf, "err {s} {s}: {s}\n", .{ serial, file, why[0..@min(why.len, 200)] }) catch return);
+ const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}\n", .{ serial, file, why[0..@min(why.len, 200)] }) catch return;
+ for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') {
+ c.* = ' ';
+ };
+ // A client retrying a write that fails the same way would fill the ring
+ // with one line, so a repeat of the newest record is that record counted,
+ // `(x3)`, as +Messages counts its repeats; unless a follower has read it
+ // already and so waits on the repeat as a line of its own.
+ const q = &p.fs.log;
+ var at = q.head;
+ var last: ?usize = null;
+ var seq = q.dropped;
+ while (at + 4 <= q.buf.items.len) : (seq += 1) {
+ last = at;
+ at += 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little);
+ }
+ const last_at = last orelse return pushLog(p, record);
+ const last_seq = seq - 1;
+ var said = q.buf.items[last_at + 4 ..];
+ said = said[0 .. said.len - 1];
+ var times: u32 = 1;
+ if (std.mem.endsWith(u8, said, ")")) if (std.mem.lastIndexOf(u8, said, " (x")) |x| {
+ if (std.fmt.parseInt(u32, said[x + 3 .. said.len - 1], 10)) |n| {
+ times = n;
+ said = said[0..x];
+ } else |_| {}
+ };
+ if (!std.mem.eql(u8, said, record[0 .. record.len - 1])) return pushLog(p, record);
+ for (p.fs.opens) |o| if (o.node != 0 and o.what == .log) {
+ const l = o.what.log;
+ if (l.follow and (l.next > last_seq or (l.next == last_seq and l.part > 0))) return pushLog(p, record);
+ };
+ var counted: [280]u8 = undefined;
+ const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return;
+ q.buf.shrinkRetainingCapacity(last_at);
+ pushLog(p, again);
}
/// The log is one ring that records whether or not anyone reads it. A record
@@ -819,7 +854,20 @@ test "a refused or failed write is an err record in the log, saying which file a
// A write that succeeds says nothing.
_ = wr(p, Node.of(serial, .addr), "#0");
try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status);
+ // A follower that has read an err waits on its repeat: a line of its own.
+ _ = wr(p, Node.of(serial, .addr), "/nowhere/");
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen));
+ _ = wr(p, Node.of(serial, .addr), "/nowhere/");
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen));
_ = call(p, .{ .tag = 6, .op = .release, .node = log, .handle = f });
+
+ // With the follower gone, repeats are the newest record counted: the
+ // one it read, and three more.
+ for (0..3) |_| _ = wr(p, Node.of(serial, .addr), "/elsewhere/");
+ const g = call(p, .{ .tag = 7, .op = .open, .node = log }).reply.handle;
+ const text = call(p, .{ .tag = 8, .op = .read, .node = log, .handle = g, .size = 1 << 16 }).bytes;
+ try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "regexp\nerr {d} addr: no match for regexp (x4)\n", .{serial})));
+ _ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = g });
}
test "opens of the log share the open records, and a closed one frees its record" {