summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 17:51:35 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit7de6d8304f563023c4a5fa9f8223f9ed626c5f91 (patch)
treec406339964e6af5383d49cf8a23fe26dd8c634fb
parent9f6de14934df03c2e3d521af78ce2e5d7362cc88 (diff)
downloadpardes-7de6d8304f563023c4a5fa9f8223f9ed626c5f91.tar.gz
pardes-7de6d8304f563023c4a5fa9f8223f9ed626c5f91.zip
Sixteen 9P connections, and the next one is told there is no room instead of being reset
Four slots ran out under scripts plus a mount, and the fifth client was closed without a word, which through a mount looks exactly like a dead session. pardes now serves sixteen, and builds against cloud9 09b77cf, whose runner answers the Tversion of a client it cannot seat with an Rerror "too many connections" and tells pardes, which logs `err - 9p: too many connections`. QUIC keeps its own sixteen and still just closes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--.agents/skills/pardes-9p/SKILL.md5
-rw-r--r--build.zig.zon4
-rw-r--r--docs/fs.md6
-rw-r--r--src/9p_io.zig23
-rw-r--r--test/fs.py16
5 files changed, 45 insertions, 9 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 87cbbe96..6651a9db 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -258,8 +258,9 @@ write with ECONNRESET although the Restore went ahead; trust the log. `Dump` wri
`DumpDir` (`$XDG_DATA_HOME/pardes`, else `~/.local/share/pardes`) and logs
`dump <path>`.
Read the event implementation before building an interceptor. Close handles in
-`finally`, and disconnect after a socket timeout. The service shares four
-connection slots and 32 screen/terminal-history snapshot handles.
+`finally`, and disconnect after a socket timeout. The service shares sixteen
+connection slots (the next client's version gets `too many connections`)
+and 32 screen/terminal-history snapshot handles.
A file's qid version is the pane's revision for `body`, `data` and `xdata`, so
`stat` sees an edit land without reading the text; it stays zero elsewhere.
diff --git a/build.zig.zon b/build.zig.zon
index f9dff0ea..781571aa 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#2a7137c644eb955d39ce3f07d75bf9c8fd9c932e",
- .hash = "cloud9-0.1.0-yt86qhn8HQDWL3p4nttyOx-iqHcNYsukfOIq0cuEeQxW",
+ .url = "git+https://git.sr.ht/~gbrls/cloud9#09b77cf7c13c3384a53594e76f51161cab64e5dd",
+ .hash = "cloud9-0.1.0-yt86qmwFHgDi2ZU-ZRIyVZshMZ4Ce6A6Iaxu-FtJG2pI",
},
// ZLS as a LIBRARY, not a language server: src/lsp_zls.zig imports the
// `zls` module its build.zig publishes and calls the analyser in
diff --git a/docs/fs.md b/docs/fs.md
index e989aba8..12e94b8d 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -45,8 +45,10 @@ IPv4/IPv6 addresses, not DNS names. Listener port zero chooses a free port;
All connections have session access, including `os`. TCP is unencrypted.
QUIC uses an ephemeral TLS identity without peer verification or login.
It carries 9P2000 on one bidirectional stream with ALPN `pardes-9p`.
-Unix and TCP connections share four slots served by cloud9's `std.Io`
-runner; QUIC has four of its own on the editor's poll loop. OpenSSL's
+Unix and TCP connections share sixteen slots served by cloud9's `std.Io`
+runner; QUIC has sixteen of its own on the editor's poll loop. A client
+that finds every Unix/TCP slot taken gets an Rerror `too many connections`
+to its Tversion, and the log an `err - 9p: too many connections` record. OpenSSL's
internal buffers are separate, dynamically allocated memory.
[Plan9port's client](https://9fans.github.io/plan9port/man/man1/9p.html) can
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 6bd03049..8f56ff13 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -64,7 +64,9 @@ const prefix = "pardes-9p-";
pub const msize = ninep.msize;
-pub const max_conns = 4;
+/// Scripts, a mount and an agent or two at once; each slot holds its
+/// buffers (a few msize) whether used or not.
+pub const max_conns = 16;
extern "c" fn inet_pton(family: c_int, src: [*:0]const u8, dst: *anyopaque) c_int;
@@ -176,6 +178,8 @@ pub const Listener = struct {
stopping: std.atomic.Value(bool) = .init(false),
/// A connection wrote the Restore and has yet to answer it.
restore_writer: std.atomic.Value(bool) = .init(false),
+ /// Clients turned away with every slot taken, since the log last said so.
+ refused: std.atomic.Value(u32) = .init(0),
/// Connections accepted so far, and the count each slot's connection
/// was accepted at: a Restore cuts those accepted before it (`reset`),
/// and a new client in a freed slot has a later stamp. Written on the
@@ -217,6 +221,14 @@ pub const Listener = struct {
/// answers. A request that would change a pane while the editor is out
/// in a syscall mid-step is parked in the engine instead, and retried
/// when the turn is next given up between steps (`wakeParked`).
+ /// On the accepting task, without the turn: counted, and logged with
+ /// the turn (`answerHeld`).
+ fn onRefused(ctx: ?*anyopaque) void {
+ const l = of(ctx);
+ _ = l.refused.fetchAdd(1, .acq_rel);
+ l.kick();
+ }
+
fn onOpened(ctx: ?*anyopaque, conn: *Runner.Conn) void {
const l = of(ctx);
l.accepted[conn.index].store(l.accepts.fetchAdd(1, .acq_rel) + 1, .release);
@@ -331,6 +343,11 @@ pub const Listener = struct {
const l = of(ctx);
if (l.stopping.load(.acquire)) return;
const core = l.core;
+ const refused = l.refused.swap(0, .acq_rel);
+ if (refused != 0) {
+ var why: [64]u8 = undefined;
+ pardes.ctlfs.events.notePath(core, "err -", std.fmt.bufPrint(&why, "9p: too many connections ({d} turned away)", .{refused}) catch "9p: too many connections");
+ }
if (!core.fs.news) return;
core.fs.news = false;
for (&core.fs.opens) |*o| {
@@ -824,7 +841,7 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [
l.runner.init(.{
.io = io,
.root = pardes.ctlfs.root,
- .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened },
+ .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened, .refused = Listener.onRefused },
.greet_timeout_ms = Listener.greet_deadline_ms,
});
const entry_name = if (named.len != 0) named else fallback;
@@ -1356,7 +1373,7 @@ test "Unix TCP and QUIC share one listener through reads writes reconnects and r
}
try testing.expect(l.path().len != 0);
try testing.expectEqual(tcp, l.tcp_address != null);
- try testing.expectEqual(@as(usize, 4), l.runner.conns.len);
+ try testing.expectEqual(@as(usize, max_conns), l.runner.conns.len);
try testing.expect(l.watcher == null);
const port = (if (tcp) l.tcp_address else l.quic_address).?.getPort();
try testing.expect(port != 0);
diff --git a/test/fs.py b/test/fs.py
index 72c52b7e..62e78655 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -371,6 +371,22 @@ def discovery(binary, embedded=False):
assert f'err {unsaved} ctl: Save /nonexistent-pardes-dir/x.txt: '.encode() in client.read('/log'), client.read('/log')
client.remove(f'/pane/{unsaved}')
+ # Sixteen connections fill the slots; the next is told why.
+ extra = [Client(address) for _ in range(15)]
+ try:
+ try:
+ Client(address).__exit__()
+ raise AssertionError('a seventeenth connection was served')
+ except OSError as refused:
+ assert 'too many connections' in str(refused), refused
+ finally:
+ for other in extra:
+ other.__exit__()
+ deadline = time.monotonic() + 5
+ while b'err - 9p: too many connections' not in client.read('/log'):
+ assert time.monotonic() < deadline, client.read('/log')
+ time.sleep(.01)
+
print('9P discovery: listing/stat/find are inert; new, remove, look, exec, name, sel, log, ctl lock, focus, the ctl split and commands behave')