diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 17:51:35 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 7de6d8304f563023c4a5fa9f8223f9ed626c5f91 (patch) | |
| tree | c406339964e6af5383d49cf8a23fe26dd8c634fb | |
| parent | 9f6de14934df03c2e3d521af78ce2e5d7362cc88 (diff) | |
| download | pardes-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.md | 5 | ||||
| -rw-r--r-- | build.zig.zon | 4 | ||||
| -rw-r--r-- | docs/fs.md | 6 | ||||
| -rw-r--r-- | src/9p_io.zig | 23 | ||||
| -rw-r--r-- | test/fs.py | 16 |
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 @@ -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); @@ -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') |
