From a8248e91873f040eed1fd24d5af7c0ca285483ef Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 12:11:23 -0300 Subject: 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 --- docs/fs.md | 5 ++++- src/ninep/events.zig | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 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 : ` 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" { -- cgit v1.3