2f43441a
dial.detach: the goodbye, for symmetry
a73x 2026-08-31 17:15
Commit message
src/dial.zig
| Old | New | ||
|---|---|---|---|
| @@ -46,6 +46,25 @@ pub fn dialAttachNamed(sock_path: []const u8, cols: u16, rows: u16, name: []cons | |||
| 46 | return s; | 46 | return s; |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | /// The goodbye for a connection this module opened. Fire-and-forget by | ||
| 50 | /// design: there is no acknowledgement on the wire. The daemon answers a | ||
| 51 | /// `detach` by dropping the client and sending nothing back, and a client | ||
| 52 | /// that has said goodbye never redials — so the close that follows is final | ||
| 53 | /// rather than a network event to recover from. (The wall's `detach_ack` is | ||
| 54 | /// an intra-process atomic its pump sets for its own thread-join ordering. | ||
| 55 | /// No frame carries it.) | ||
| 56 | /// | ||
| 57 | /// This takes the fd rather than the `std.net.Stream`, because that is what | ||
| 58 | /// a caller which kept only the handle has, and every connection this module | ||
| 59 | /// hands back is one. Which is also the limit of its reach: the client's own | ||
| 60 | /// detach goes through `client.Transport` and `mux a`'s through that module's | ||
| 61 | /// agent connection, and either of those may be a QUIC link with no fd to | ||
| 62 | /// write to. Those carriers frame the same goodbye themselves; a wrapper over | ||
| 63 | /// a file descriptor is not what they hold. | ||
| 64 | pub fn detach(fd: std.posix.fd_t) !void { | ||
| 65 | return proto.writeFrame(fd, .detach, ""); | ||
| 66 | } | ||
| 67 | |||
| 49 | /// Dial, ask one question, read the one answer, hang up. The observer verbs' | 68 | /// Dial, ask one question, read the one answer, hang up. The observer verbs' |
| 50 | /// round trip: `stats_req`, `sessions_req`, `debug_dump`, `endpoint_req`, | 69 | /// round trip: `stats_req`, `sessions_req`, `debug_dump`, `endpoint_req`, |
| 51 | /// `upgrade_req` — every one of them a question asked by a caller that holds | 70 | /// `upgrade_req` — every one of them a question asked by a caller that holds |
| @@ -131,6 +150,28 @@ test "the attach helpers fail at the dial, before any frame" { | |||
| 131 | try std.testing.expectError(error.FileNotFound, dialAttachNamed("/nonexistent-dir/mux-dial-test.sock", 80, 24, "work")); | 150 | try std.testing.expectError(error.FileNotFound, dialAttachNamed("/nonexistent-dir/mux-dial-test.sock", 80, 24, "work")); |
| 132 | } | 151 | } |
| 133 | 152 | ||
| 153 | test "detach writes one empty frame and waits for nothing" { | ||
| 154 | // Pinned on a socketpair because the goodbye has no reply to wait for: | ||
| 155 | // what is checkable is that exactly one empty `detach` reaches the peer | ||
| 156 | // and the call returns without reading anything back. | ||
| 157 | var pair: [2]i32 = undefined; | ||
| 158 | try std.testing.expectEqual(@as(usize, 0), std.os.linux.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &pair)); | ||
| 159 | defer std.posix.close(pair[1]); | ||
| 160 | |||
| 161 | try detach(pair[0]); | ||
| 162 | // Close the writing end — not deferred, since it must be shut before the | ||
| 163 | // reads below — so a second frame would show up as a second read rather | ||
| 164 | // than as a block. | ||
| 165 | std.posix.close(pair[0]); | ||
| 166 | |||
| 167 | const alloc = std.testing.allocator; | ||
| 168 | const frame = (try proto.readFrame(alloc, pair[1])).?; | ||
| 169 | defer frame.deinit(alloc); | ||
| 170 | try std.testing.expectEqual(proto.MsgType.detach, frame.type); | ||
| 171 | try std.testing.expectEqual(@as(usize, 0), frame.payload.len); | ||
| 172 | try std.testing.expect((try proto.readFrame(alloc, pair[1])) == null); | ||
| 173 | } | ||
| 174 | |||
| 134 | test "ask: a path nothing is bound at is error.NoDaemon, not a connect errno" { | 175 | test "ask: a path nothing is bound at is error.NoDaemon, not a connect errno" { |
| 135 | try std.testing.expectError( | 176 | try std.testing.expectError( |
| 136 | error.NoDaemon, | 177 | error.NoDaemon, |
src/server/server_test_attach.zig
| Old | New | ||
|---|---|---|---|
| @@ -907,7 +907,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a | |||
| 907 | try std.testing.expect(last_seq > 0); | 907 | try std.testing.expect(last_seq > 0); |
| 908 | // 0 is reserved for "I hold no epoch", so a live daemon never has it. | 908 | // 0 is reserved for "I hold no epoch", so a live daemon never has it. |
| 909 | try std.testing.expect(epoch != 0); | 909 | try std.testing.expect(epoch != 0); |
| 910 | try proto.writeFrame(c1.handle, .detach, ""); | 910 | try dial.detach(c1.handle); |
| 911 | } | 911 | } |
| 912 | 912 | ||
| 913 | // Session 2: the seq is real but the epoch belongs to some other daemon | 913 | // Session 2: the seq is real but the epoch belongs to some other daemon |
| @@ -925,7 +925,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a | |||
| 925 | // seq session 1 learned. | 925 | // seq session 1 learned. |
| 926 | last_seq = first.?.seq; | 926 | last_seq = first.?.seq; |
| 927 | try std.testing.expectEqual(epoch, first.?.epoch); | 927 | try std.testing.expectEqual(epoch, first.?.epoch); |
| 928 | try proto.writeFrame(c2.handle, .detach, ""); | 928 | try dial.detach(c2.handle); |
| 929 | } | 929 | } |
| 930 | 930 | ||
| 931 | // Session 3: a real seq with have_epoch = 0 — a pre-epoch client, or one | 931 | // Session 3: a real seq with have_epoch = 0 — a pre-epoch client, or one |
| @@ -938,7 +938,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a | |||
| 938 | try std.testing.expect(first != null); | 938 | try std.testing.expect(first != null); |
| 939 | try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type); | 939 | try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type); |
| 940 | last_seq = first.?.seq; | 940 | last_seq = first.?.seq; |
| 941 | try proto.writeFrame(c3.handle, .detach, ""); | 941 | try dial.detach(c3.handle); |
| 942 | } | 942 | } |
| 943 | 943 | ||
| 944 | // Session 4: right seq, right epoch — we really are up to date, so the | 944 | // Session 4: right seq, right epoch — we really are up to date, so the |
| @@ -950,7 +950,7 @@ test "Server: reattach needs a recent have_seq AND this daemon's epoch to get a | |||
| 950 | const first = try firstStateFrame(alloc, c4.handle, 10_000); | 950 | const first = try firstStateFrame(alloc, c4.handle, 10_000); |
| 951 | try std.testing.expect(first != null); | 951 | try std.testing.expect(first != null); |
| 952 | try std.testing.expectEqual(proto.MsgType.delta, first.?.type); | 952 | try std.testing.expectEqual(proto.MsgType.delta, first.?.type); |
| 953 | try proto.writeFrame(c4.handle, .detach, ""); | 953 | try dial.detach(c4.handle); |
| 954 | } | 954 | } |
| 955 | 955 | ||
| 956 | // Session 5: right epoch, but have_seq predates the last discontinuity, | 956 | // Session 5: right epoch, but have_seq predates the last discontinuity, |
| @@ -991,7 +991,7 @@ test "Server: a daemon restart invalidates have_seq even with the old epoch pres | |||
| 991 | held_a = try drainHeld(alloc, c.handle, 10_000); | 991 | held_a = try drainHeld(alloc, c.handle, 10_000); |
| 992 | try std.testing.expect(held_a.seq > 0); | 992 | try std.testing.expect(held_a.seq > 0); |
| 993 | try std.testing.expect(held_a.epoch != 0); | 993 | try std.testing.expect(held_a.epoch != 0); |
| 994 | try proto.writeFrame(c.handle, .detach, ""); | 994 | try dial.detach(c.handle); |
| 995 | } | 995 | } |
| 996 | // A is gone: thread joined, deinit ran, socket unlinked. | 996 | // A is gone: thread joined, deinit ran, socket unlinked. |
| 997 | 997 | ||
| @@ -1016,7 +1016,7 @@ test "Server: a daemon restart invalidates have_seq even with the old epoch pres | |||
| 1016 | try std.testing.expect(held_b.seq > 0); | 1016 | try std.testing.expect(held_b.seq > 0); |
| 1017 | try std.testing.expect(held_b.epoch != 0); | 1017 | try std.testing.expect(held_b.epoch != 0); |
| 1018 | try std.testing.expect(held_b.epoch != held_a.epoch); | 1018 | try std.testing.expect(held_b.epoch != held_a.epoch); |
| 1019 | try proto.writeFrame(c.handle, .detach, ""); | 1019 | try dial.detach(c.handle); |
| 1020 | } | 1020 | } |
| 1021 | 1021 | ||
| 1022 | // A seq B issued moments ago, quoted back with A's epoch. Everything | 1022 | // A seq B issued moments ago, quoted back with A's epoch. Everything |
| @@ -1031,7 +1031,7 @@ test "Server: a daemon restart invalidates have_seq even with the old epoch pres | |||
| 1031 | try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type); | 1031 | try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type); |
| 1032 | // Read off the wire, not off srv_b: the server thread is live. | 1032 | // Read off the wire, not off srv_b: the server thread is live. |
| 1033 | try std.testing.expectEqual(held_b.epoch, first.?.epoch); | 1033 | try std.testing.expectEqual(held_b.epoch, first.?.epoch); |
| 1034 | try proto.writeFrame(c.handle, .detach, ""); | 1034 | try dial.detach(c.handle); |
| 1035 | } | 1035 | } |
| 1036 | 1036 | ||
| 1037 | // And the literal restart case: the exact pair the pre-restart client | 1037 | // And the literal restart case: the exact pair the pre-restart client |