Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

a tiny daemon in go, and the bits that aren't the code

Shipping a small Go daemon taught me that the interesting work was signals, config and the unit file, not the loop in the middle.

A close-up of source code on a screen

I shipped a small daemon this week. The job it does is dull and I'll spare you the details, but the shape of it is the same shape every long-running service has, and the lesson was that the actual work was the easy part. The loop in the middle took an afternoon. Everything around it took the rest of the time, and that's where all the value was.

The core is what you'd expect: read config, set up, then loop until told to stop. In Go that "told to stop" is the bit worth getting right.

func main() {
	cfg := loadConfig()
	srv := newServer(cfg)

	stop := make(chan os.Signal, 1)
	signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

	go srv.Run()

	<-stop
	log.Println("shutting down")
	srv.Shutdown()
}

A wider shot of code on a monitor

The buffered channel of size one matters. If it's unbuffered and the signal arrives before you're blocked on the receive, you can drop it and never shut down cleanly. One byte of buffer, a whole class of "why won't it die" gone.

the unwritten requirements

Nobody asks for these in a ticket, and yet a daemon that ignores them is a daemon that pages you at three in the morning.

Logging to stdout, not a file. The temptation is to open a logfile and manage rotation yourself. Don't. Log to stdout and let the init system or the journal capture it. systemd does this for free, you get rotation and filtering for nothing, and your binary stays a binary rather than turning into a half-baked log manager.

Config that fails loudly at start, not lazily at use. I parse and validate the whole config up front and refuse to start if anything is wrong. A daemon that boots happily and then falls over an hour later because of a typo in a setting it only reads on the first request is a special kind of cruel.

A real exit code. Exit non-zero when you fail to start. It sounds obvious, but it's the difference between a supervisor noticing the failure and a supervisor cheerfully reporting "active" over a process that did nothing.

the unit file

The last piece was the systemd unit, and it's short enough to read in full:

[Unit]
Description=A tiny daemon
After=network.target

[Service]
ExecStart=/usr/local/bin/tinyd run
Restart=on-failure
User=tinyd

[Install]
WantedBy=multi-user.target

Restart=on-failure and a non-root User cover the two things I'd otherwise have hand-rolled badly: restart-on-crash and not running the thing as root for no reason. Between the signal handling and that unit file, the daemon now starts on boot, stops cleanly, restarts if it falls over, and logs somewhere I can actually find.

The code in the middle is maybe a hundred lines. The hundred lines around the edges are the ones that decide whether you ever think about it again. I'd rather not think about it again, so it was worth the afternoon.