From 6d028bb1a1e7090900cf53fa1ae95af438af65ad Mon Sep 17 00:00:00 2001 From: Ian Chechin Date: Wed, 13 May 2026 00:43:47 +0800 Subject: [PATCH] bridgev2/mxmain: add systemd Type=notify support Megabridge bridges started under a systemd unit with `Type=notify` currently never report readiness or accept watchdog supervision, which forces operators to fall back to `Type=simple` and lose the ability to distinguish startup failures from runtime failures. Add three small helpers in bridgev2/matrix/mxmain that wrap the already-available coreos/go-systemd v22 daemon package: sdNotifyReady - sends READY=1 once the bridge has finished starting up. sdNotifyStopping - sends STOPPING=1 just before the graceful shutdown begins. startSdWatchdog - pings WATCHDOG=1 at half the configured interval, returning a stop function that blocks until the pinger has exited. All three calls are no-ops when the process is not running under a systemd unit (the daemon package returns false without error in that case), so non-systemd deployments are unaffected. BridgeMain.Run is updated to call the helpers around the WaitForInterrupt block. coreos/go-systemd is already a transitive dependency, so go.mod is unchanged. Closes #437. --- bridgev2/matrix/mxmain/main.go | 4 ++ bridgev2/matrix/mxmain/systemd.go | 84 ++++++++++++++++++++++++++ bridgev2/matrix/mxmain/systemd_test.go | 39 ++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 bridgev2/matrix/mxmain/systemd.go create mode 100644 bridgev2/matrix/mxmain/systemd_test.go diff --git a/bridgev2/matrix/mxmain/main.go b/bridgev2/matrix/mxmain/main.go index f8303237..1467e13f 100644 --- a/bridgev2/matrix/mxmain/main.go +++ b/bridgev2/matrix/mxmain/main.go @@ -115,7 +115,11 @@ func (br *BridgeMain) Run() { br.PreInit() br.Init() br.Start() + sdNotifyReady(br.Log) + stopWatchdog := startSdWatchdog(br.Log) exitCode := br.WaitForInterrupt() + sdNotifyStopping(br.Log) + stopWatchdog() br.Stop() os.Exit(exitCode) } diff --git a/bridgev2/matrix/mxmain/systemd.go b/bridgev2/matrix/mxmain/systemd.go new file mode 100644 index 00000000..82573edf --- /dev/null +++ b/bridgev2/matrix/mxmain/systemd.go @@ -0,0 +1,84 @@ +// Copyright (c) 2026 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mxmain + +import ( + "time" + + "github.com/coreos/go-systemd/v22/daemon" + "github.com/rs/zerolog" +) + +// sdNotifyReady tells the systemd service manager that the bridge has finished +// starting up. The call is a no-op when the bridge is not running under a +// systemd unit with `Type=notify`. +// +// See https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html. +func sdNotifyReady(log *zerolog.Logger) { + if sent, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { + log.Warn().Err(err).Msg("Failed to send systemd READY=1 notification") + } else if sent { + log.Debug().Msg("Sent systemd READY=1 notification") + } +} + +// sdNotifyStopping tells the systemd service manager that the bridge is +// shutting down. The call is a no-op outside of systemd. +func sdNotifyStopping(log *zerolog.Logger) { + if sent, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil { + log.Warn().Err(err).Msg("Failed to send systemd STOPPING=1 notification") + } else if sent { + log.Debug().Msg("Sent systemd STOPPING=1 notification") + } +} + +// startSdWatchdog pings the systemd watchdog at half the configured interval +// for as long as the returned stop function has not been called. The returned +// stop function blocks until the watchdog goroutine has exited. +// +// Returns a no-op stop function when the process is not running under a +// systemd unit with `WatchdogSec=` set (so the call is always safe). +func startSdWatchdog(log *zerolog.Logger) (stop func()) { + interval, err := daemon.SdWatchdogEnabled(false) + if err != nil { + log.Warn().Err(err).Msg("Failed to query systemd watchdog setting") + return func() {} + } + if interval == 0 { + // No watchdog configured by the unit file. + return func() {} + } + // systemd recommends pinging at half the configured interval to leave + // headroom for scheduling jitter. + ping := interval / 2 + log.Info(). + Stringer("watchdog_interval", interval). + Stringer("ping_interval", ping). + Msg("Starting systemd watchdog pinger") + + done := make(chan struct{}) + stopped := make(chan struct{}) + go func() { + defer close(stopped) + ticker := time.NewTicker(ping) + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + if _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog); err != nil { + log.Warn().Err(err).Msg("Failed to send systemd WATCHDOG=1 notification") + } + } + } + }() + return func() { + close(done) + <-stopped + } +} diff --git a/bridgev2/matrix/mxmain/systemd_test.go b/bridgev2/matrix/mxmain/systemd_test.go new file mode 100644 index 00000000..e9fa67cc --- /dev/null +++ b/bridgev2/matrix/mxmain/systemd_test.go @@ -0,0 +1,39 @@ +// Copyright (c) 2026 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mxmain + +import ( + "os" + "testing" + + "github.com/rs/zerolog" +) + +// TestSdNotifyHelpersAreNoOpWithoutSystemd verifies that the systemd helpers +// are safe to call when the process is not running under a systemd unit (i.e. +// NOTIFY_SOCKET is not set). They must not panic, must not block, and the +// watchdog stop function must be idempotent and return promptly. +func TestSdNotifyHelpersAreNoOpWithoutSystemd(t *testing.T) { + t.Setenv("NOTIFY_SOCKET", "") + t.Setenv("WATCHDOG_USEC", "") + t.Setenv("WATCHDOG_PID", "") + // Some test runners inherit a parent NOTIFY_SOCKET; explicitly unset it + // so the helpers really do see a non-systemd environment. + _ = os.Unsetenv("NOTIFY_SOCKET") + _ = os.Unsetenv("WATCHDOG_USEC") + _ = os.Unsetenv("WATCHDOG_PID") + + log := zerolog.Nop() + sdNotifyReady(&log) + sdNotifyStopping(&log) + + stop := startSdWatchdog(&log) + if stop == nil { + t.Fatal("startSdWatchdog returned a nil stop function") + } + stop() +}