milestone 7: serving pipeline, client tracking, pause and lifecycle
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user