A commit went into my CLI framework's root command a month before I noticed it. Someone else had to tell me.
It gave the root command a signal-aware context: catch SIGINT, cancel, wind up properly. A good change. Trouble is, my service supervisor library had been catching SIGINT and SIGTERM itself, by default, for most of its life. Also a good decision, in a bare main.
Go lets both happen. signal.Notify is additive, so a second registration doesn't replace the first, it just posts to both. No error, no warning. Two shutdown drivers, racing.
The duplicated log lines were the least of it. The library publishes a contract: context.Cause(ctx) tells a service why it was stopped. With two handlers that became a coin flip, decided by whichever goroutine the scheduler fancied.
The bit that smarted: two of my own downstream tools had already written the same workaround, with the same reasoning in the comments, independently. A third had been running both handlers the whole time.
So the default went the other way round. NewController no longer installs a signal handler; you pass WithSignals() when the controller really is the outermost layer. Process-global state belongs to whoever is outermost, and a library can't know that's it.
Also in the post: the cooperative-drain protocol I specced out and then binned after reading three call sites.
#golang #OpenSource #SoftwareEngineering #programming
https://phpboyscout.uk/the-library-that-appointed-itself-signal-handler/