Skip to content

Conversation

@mrdimidium
Copy link
Member

Signals in Unix can be asynchronous (interrupting one of the threads and calling the sighandle callback) or synchronous (blocking the process on the sigwait call).

Asynchronous signals seem easier to use, but they have a problem: since the system switches to sigaction at an arbitrary moment, the process can be stopped at an arbitrary point (for example, while acquiring a mutex or writing to a global buffer). Therefore, posix defines a specific list of functions that are safe to use in a handler. Using any other code (even the simplest ones like print) results in UB.

The code below circumvents this problem using blocking signal handling:

  • set a mask as early as possible to prevent receiving signals on the main thread (this property will be inherited when creating new threads);
  • create a new thread that specifies that it can receive SIGINT/SIGTERM;
  • block the new thread on sigwait, and call cleanup callbacks when a signal is received.

Since the thread is blocked on sigwait, it consumes no resources (except a few MB for its stack). If the program terminates, the thread will be killed when the main thread terminates.

Using atomic to get shutdown is necessary regardless of the thread I create here, posix states that the signal sent to the process can be handled on any of the threads.

Copy link
Collaborator

@karlseguin karlseguin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem to work on MacOS.

> zig build run
INFO  app : server running . . . . . . . . . . . . . . . . .  [+0ms]
      address = 127.0.0.1:9222

^Cinfo: Received termination signal...

> zig build run
FATAL app : server run error . . . . . . . . . . . . . . . .  [+0ms]
      err = AddressInUse

FATAL app : exit . . . . . . . . . . . . . . . . . . . . . .  [+0ms]
      err = AddressInUse

I have to kill it a couple times before it's gone.

const assert = std.debug.assert;
const Allocator = std.mem.Allocator;

const Self = @This();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use Self for normal struct, only for generics.

assert(@typeInfo(@TypeOf(func)).@"fn".return_type.? == void);

const Args = @TypeOf(args);
const TypeErased = struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fancy, and it's fine. But it would also be ok to have a shutdown function in App/Server/whatever that takes an *anyopaque and knows to cast it back to its own type. I guess you wouldn't need an allocation anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trick is only needed for type checking. You should copy the pointer to anyopaque anyway, so you won't waste any extra memory with .{server}. Since memory is allocated in the arena, the allocation here is almost equivalent to a pointer next to a pointer to a function.

@mrdimidium mrdimidium force-pushed the wp/mrdimidium/graceful-shutdown branch from 237b05d to 6159d73 Compare December 4, 2025 07:46
@krichprollsch
Copy link
Member

krichprollsch commented Dec 4, 2025

Does it work on macos? I tested it on my linux and in docker, it looks good to me in these cases.

@mrdimidium
Copy link
Member Author

@krichprollsch, sorry, I didn't have MacOS at hand, I'll set it up this evening and check.

@krichprollsch
Copy link
Member

np, we can also wait for a test from @karlseguin or @nikneym

@nikneym
Copy link
Contributor

nikneym commented Dec 4, 2025

Just tested on MacOS, seems I have to run kill 2 times after to properly exit.

@mrdimidium
Copy link
Member Author

Interesting, this is actually a problem on macos, it doesn't interrupt accept after calling posix.shutdown, it needs an explicit posix.close.

@mrdimidium mrdimidium force-pushed the wp/mrdimidium/graceful-shutdown branch 2 times, most recently from f41845e to f7c5921 Compare December 4, 2025 19:08
@mrdimidium mrdimidium force-pushed the wp/mrdimidium/graceful-shutdown branch from f7c5921 to 74eee75 Compare December 4, 2025 19:17
@mrdimidium
Copy link
Member Author

Sorry for the delay, I fixed it for Mac. The issue was related to different shutdown behavior on Linux and Mac (the handler itself works fine), so I left a comment in the code.

Screenshot 2025-12-04 at 19 18 32

@mrdimidium mrdimidium requested a review from karlseguin December 4, 2025 19:24
@karlseguin karlseguin merged commit cdd7399 into main Dec 4, 2025
10 checks passed
@karlseguin karlseguin deleted the wp/mrdimidium/graceful-shutdown branch December 4, 2025 23:34
@github-actions github-actions bot locked and limited conversation to collaborators Dec 4, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants