119 lines
3.7 KiB
Zig
119 lines
3.7 KiB
Zig
//! 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);
|
|
}
|