summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 13:19:44 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit92ef244c8a24583896feb47c8c48ec40aa04ebb9 (patch)
treef254f4eb97dcc24cd0ae9560b54b27d63c7563c6
parentd51e1cd8d7344738ca04578beb8731ab72235a07 (diff)
downloadpardes-92ef244c8a24583896feb47c8c48ec40aa04ebb9.tar.gz
pardes-92ef244c8a24583896feb47c8c48ec40aa04ebb9.zip
A connection holds up to 128 reads, the next refused "too many reads waiting: 128", and a mount beside 40 held event reads still answers
Two limits met. pardes parked 32 reads per connection and refused the next with EAGAIN's C string, "Resource temporarily unavailable". 9ns kept 32 tags and 31 workers, each pinned by a held read. So 31 followers through one mount took every worker, and `cat layout` then queued for ever: the whole mount deadlocked. cloud9 (705be665, pushed to sr.ht and pinned here) now refuses in words past the cap. Its 9ns window is 256, so a mount always has workers past what pardes holds. pardes raises its cap to 128. fs.md documents the limit beside held reads. selfmount.py holds 40 event reads through the mount and reads layout beside them. It times out with the 9ns installed before this change and passes with the new one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--build.zig.zon4
-rw-r--r--docs/fs.md7
-rw-r--r--src/9p.zig8
-rw-r--r--src/9p_io.zig5
-rw-r--r--test/selfmount.py7
5 files changed, 26 insertions, 5 deletions
diff --git a/build.zig.zon b/build.zig.zon
index e80961ef..4d661759 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -9,8 +9,8 @@
// read-only HTTPS URL is what a manifest can carry. Re-pin with
// `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`.
.cloud9 = .{
- .url = "git+https://git.sr.ht/~gbrls/cloud9#f35b7ed09d0a170e4ffe55b5286c28f74c9a8760",
- .hash = "cloud9-0.1.0-yt86quEKHgCwewf5XRlEZji1JCmdsfufsrZYGZqzHj5_",
+ .url = "git+https://git.sr.ht/~gbrls/cloud9#705be665742fa0332585a4321640b5e2245610cb",
+ .hash = "cloud9-0.1.0-yt86qt8NHgDuS2WNp0TgW03h-eVIgWmkznn0t0nMQKbr",
},
// Pinned to the exact libvaxis commit ghostty is built against, so it
// is compatible with ghostty's uucode configuration (the one we share
diff --git a/docs/fs.md b/docs/fs.md
index 2660bada..fe78fb19 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -179,8 +179,11 @@ files`.
**Held reads.** A read with nothing to give yet (a followed `log`, `event`,
`pty/data`, `pty/run` before its answer) waits in the editor and is answered
when news comes. A second read on that open meanwhile fails `file in use`.
-A read the client flushed is dropped. Through a FUSE mount bash's `read -t`
-cannot time out: wrap the loop in `timeout N`.
+A read the client flushed is dropped. One connection holds at most 128
+reads at once; the next is refused `too many reads waiting: 128`. A mount
+(9ns) is one connection, so that is 128 followers through it, and the mount
+keeps answering everything else beside them. Through a FUSE mount bash's
+`read -t` cannot time out: wrap the loop in `timeout N`.
**Stats.** Lengths are real (for `event` and `pty/data` the next record's,
zero when none waits; for `log` what an open would freeze). The qid
diff --git a/src/9p.zig b/src/9p.zig
index 837525cb..67b3b39a 100644
--- a/src/9p.zig
+++ b/src/9p.zig
@@ -61,7 +61,13 @@ pub const msize: u32 = 65536;
/// (src/9p_io.zig, `serve`), and a write it cannot keep would fail instead.
/// The largest write is msize less the Twrite header, 23 bytes (the test
/// below asks a client), one byte more than `iohdrsz` allows for.
-pub const editor: cloud9.fs.Options = .{ .fid_capacity = max_fids, .name_capacity = 255, .park_data_max = msize - 23 };
+/// Up to 128 reads held on one connection (a follower each of the log or
+/// an event file); the next is refused `too many reads waiting: 128`. A
+/// mount (9ns) keeps a wider window, 256, so held reads never take every
+/// one of its workers.
+pub const editor: cloud9.fs.Options = .{ .fid_capacity = max_fids, .name_capacity = 255, .park_data_max = msize - 23, .slot_capacity = max_held };
+
+pub const max_held: usize = 128;
/// The board's engine: the GPIO firmware's budget (src/esp32p4_9p.zig).
pub const board: cloud9.fs.Options = .{ .fid_capacity = board_fids, .name_capacity = board_name_capacity };
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 5a68238c..0eaa2d7e 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -1542,6 +1542,11 @@ test "a change waits while the editor is out mid-step, a read does not, and the
try testing.expectEqualStrings("after\n", pane.file.?.content);
}
+test "a connection holds up to 128 reads, and the next is refused in words" {
+ try testing.expectEqual(@as(usize, 128), ninep.max_held);
+ if (comptime supported) try testing.expectEqualStrings("too many reads waiting: 128", Runner.Engine.e_waiting);
+}
+
test "a held read is answered when the log has news, and a flushed one spends nothing" {
if (comptime !supported) return error.SkipZigTest;
const gpa = testing.allocator;
diff --git a/test/selfmount.py b/test/selfmount.py
index 55809740..6534aa25 100644
--- a/test/selfmount.py
+++ b/test/selfmount.py
@@ -138,6 +138,13 @@ def run(binary):
check(run(f'python3 -c "{maker}" > {mount}/pane/{scratch}/ctl\n'.encode()) == '0', 'a 50 KB Edit block written through the mount')
body = client.read(f'/pane/{scratch}/body').decode().splitlines()
check(body == ['line %05d of the block' % i for i in range(2500)], f'it ran once, whole ({len(body)} lines)')
+ # Forty event reads held through the mount, a follower each, and
+ # the mount still answers the rest: 9ns keeps workers past what
+ # the editor holds (128), so `cat layout` is not queued behind
+ # them for ever.
+ held = run(f"for i in $(seq 40); do cat {mount}/pane/{first}/event > /dev/null & done; "
+ f"sleep 1; timeout 10 cat {mount}/layout > /dev/null; st=$?; kill $(jobs -p); exit $st\n".encode())
+ check(held == '0', f'cat layout answers beside 40 held event reads ({held!r})')
client.write('/exec', b'Msg still here\n')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('still here' in text, 'the editor answers after all of that')