milestone 7: serving pipeline, client tracking, pause and lifecycle

This commit is contained in:
2026-08-01 21:43:52 +02:00
parent 8c50b6617f
commit a8092bb1b9
17 changed files with 5916 additions and 131 deletions
+118
View File
@@ -0,0 +1,118 @@
//! SIGINT and SIGTERM, turned into one `std.Io.Event`.
//!
//! No signalfd, no self-pipe, no epoll: the handler does exactly one thing, and
//! `std.Io.Event.set` is async-signal-safe on the Threaded Linux backend — a
//! raw `futex` wake with no allocation and no lock (`Io.zig:1855` →
//! `Threaded.futexWake`). The `.mask`/`.flags` shape is the one Threaded uses
//! for its own `SIG.IO`/`SIG.PIPE` handlers (`Threaded.zig:1653`): an empty
//! mask and no `SA_RESTART`, so a blocking syscall returns `EINTR` and the
//! backend's retry loop re-reads the cancellation state.
//!
//! Everything a shutdown actually has to do — drain the query log, cancel the
//! task group, close the databases — happens on the task blocked in `wait`.
//!
//! The previous handlers are not restored. The process is leaving, and a
//! second SIGTERM during teardown should still terminate it the default way
//! only if the operator sends it before this module is armed.
const std = @import("std");
const posix = std.posix;
var event: std.Io.Event = .unset;
/// Read by the signal handler, written by `install` before the handler exists.
/// A `std.Io` is two pointers and cannot be stored atomically, so ordering is
/// what makes the read safe: the store precedes the `sigaction` syscall that
/// arms the handler, and no signal can reach the handler before that call
/// returns.
var handler_io: ?std.Io = null;
var installed: bool = false;
/// Arms the handlers for INT and TERM. Calling it again is a no-op: the process
/// has one event and one pair of handlers, and a second boot inside one process
/// (which only a test does) must not re-arm anything.
pub fn install(io: std.Io) void {
if (installed) return;
handler_io = io;
installed = true;
const act: posix.Sigaction = .{
.handler = .{ .handler = onSignal },
.mask = posix.sigemptyset(),
.flags = 0,
};
posix.sigaction(.INT, &act, null);
posix.sigaction(.TERM, &act, null);
}
fn onSignal(_: posix.SIG) callconv(.c) void {
const io = handler_io orelse return;
event.set(io);
}
/// Blocks until a shutdown is requested. A canceled wait is the caller's cue to
/// tear down as well, which is why `app.run` treats both results the same.
pub fn wait(io: std.Io) std.Io.Cancelable!void {
return event.wait(io);
}
/// The programmatic equivalent of the signal: what a test uses to shut the app
/// down, and what a Phase 8 restart endpoint would call.
pub fn trigger(io: std.Io) void {
event.set(io);
}
pub fn isRequested() bool {
return event.isSet();
}
/// Clears the request so the next `wait` blocks again. Only a test that boots
/// the app more than once in one process needs this; a served process shuts
/// down once.
pub fn reset() void {
event.reset();
}
const testing = std.testing;
test "trigger releases a waiter and isRequested reports it" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
reset();
try testing.expect(!isRequested());
trigger(io);
try testing.expect(isRequested());
// Already set, so this returns without blocking.
try wait(io);
reset();
try testing.expect(!isRequested());
}
test "a waiting task is released by a later trigger" {
var threaded: std.Io.Threaded = .init(testing.allocator, .{});
defer threaded.deinit();
const io = threaded.io();
reset();
var group: std.Io.Group = .init;
try group.concurrent(io, waitThenSet, .{ io, &done });
trigger(io);
try group.await(io);
try testing.expect(done.isSet());
reset();
done.reset();
}
var done: std.Io.Event = .unset;
fn waitThenSet(io: std.Io, flag: *std.Io.Event) std.Io.Cancelable!void {
try wait(io);
flag.set(io);
}