summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 14:41:52 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit8598a5fbe6ffecad46891957356d47a37649e547 (patch)
tree777230263f4214720a4e1dcbb2f9ea4a8a0bb4fd /src
parentb9eefda75ab9a63fc510de781d02b92fc04ad31d (diff)
downloadpardes-8598a5fbe6ffecad46891957356d47a37649e547.tar.gz
pardes-8598a5fbe6ffecad46891957356d47a37649e547.zip
A write of a multiple of 4096 bytes holds the partial line it ends with, as one that fills its Twrite does: a burst through a mount no longer runs its cut lines as two commands
selfmount's 1000-line burst reported 943 lines, on main too. The log ring was not losing records: a follower opened before the burst heard exactly 943 and no `lost`, and the burst itself exited 1 with `err 1 exec: wrong #args in control message "Msg"`. `seq ... > exec` through the mount arrives as stdio's 4096-byte writes, each shorter than a Twrite. The rule that such a write is whole ran each one's cut last line at once, so both halves of the line ran as commands (hence `exit 127` records). At 12288 bytes the cut left a bare `Msg`, which was refused and failed the write and the rest of the burst. A write of a multiple of 4096 bytes is where a writer's buffer (stdio, a page cache) filled, so it may go on: its unended tail now waits for the next write or the close, like one that fills its Twrite. `printf Save > exec` stays whole at once. fs.md says so, the 9P fuzzer's model of served lines follows it, and a unit test cuts a `Msg` at 4096 bytes. A follower does not lose records silently: a ring that outruns one already reads it `lost N` first (documented, tested). selfmount now follows the log from before the burst, so it checks every line heard once, in order, or a loss said, not a fresh open of a 64 KiB ring. It passed 3 in 3 and joins the gates with the new 9ns. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ninep/tree.zig26
1 files changed, 25 insertions, 1 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 17941e10..322728da 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -1169,7 +1169,12 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
// and its failure is this write's, as acme takes each write whole. Held
// only when it is not whole yet -- an open `{` block, an `a`/`c`/`i`
// text awaiting its `.` -- or the write filled its Twrite and may go on.
- const cut = p.fs.write_room != 0 and req.data.len >= p.fs.write_room;
+ // So may one cut where a writer's buffer filled: stdio flushes, and a
+ // mount's page cache writes back, in multiples of 4096 bytes. `seq ...
+ // > exec` through a mount arrived in 4096-byte writes, and the line cut
+ // at each end ran as two commands (the half `Msg` refused, failing the
+ // write and the rest of the burst).
+ const cut = (p.fs.write_room != 0 and req.data.len >= p.fs.write_room) or req.data.len % 4096 == 0;
if (end < o.pending.items.len and !cut) {
o.pending.append(p.gpa, '\n') catch return Reply.fail(req.tag, E.NOMEM);
const whole = ctl.completeEnd(p, o.pending.items);
@@ -1826,6 +1831,25 @@ test "look and exec read back what their own open's write touched; a fresh open
for ([_]u32{ mine, theirs, fresh }) |hh| _ = call(p, .{ .tag = 10, .op = .release, .node = exec, .handle = hh, .opened = true });
}
+test "a write cut where a writer's 4096-byte buffer filled holds its partial line for the next write" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const exec_node = Node.of(serialOf(p), .exec);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = exec_node, .omode = 1 }).reply.handle;
+ // 4096 bytes: whole `Msg first` lines, then a line cut after `Msg`.
+ var buf: [4096]u8 = undefined;
+ const line = "Msg first\n";
+ var n: usize = 0;
+ while (n + line.len <= buf.len - 3) : (n += line.len) @memcpy(buf[n..][0..line.len], line);
+ @memset(buf[n..], ' ');
+ @memcpy(buf[buf.len - 3 ..], "Msg");
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = exec_node, .handle = h, .data = &buf }).reply.status);
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 3, .op = .write, .node = exec_node, .handle = h, .data = " last\n" }).reply.status);
+ _ = call(p, .{ .tag = 4, .op = .release, .node = exec_node, .handle = h, .opened = true });
+ try testing.expect(th.logHas(p, "last"));
+ try testing.expect(!th.logHas(p, "wrong #args"));
+}
+
test "no placement leaves a pane shorter than its tag and two rows; a full column refuses the next" {
const p = try th.withFile(testing.allocator, "x\n");
defer p.deinit();