Skip to content

Instantly share code, notes, and snippets.

@Maximilan4
Created March 10, 2025 09:51
Show Gist options
  • Save Maximilan4/84679393fd6ab4af907ea897f0209fb7 to your computer and use it in GitHub Desktop.
Save Maximilan4/84679393fd6ab4af907ea897f0209fb7 to your computer and use it in GitHub Desktop.
Reloadable go app with tableflip and systemd notifications
module tft
go 1.23
require (
github.com/cloudflare/tableflip v1.2.3 // indirect
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
)
github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CSfmHMw=
github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/cloudflare/tableflip"
"github.com/coreos/go-systemd/daemon"
)
func main() {
var (
listenAddr = flag.String("listen", "localhost:8081", "`Address` to listen on")
pidFile = flag.String("pid-file", "", "`Path` to pid file")
)
flag.Parse()
log.SetPrefix(fmt.Sprintf("[%d] ", os.Getpid()))
upg, err := tableflip.New(tableflip.Options{
PIDFile: *pidFile,
})
if err != nil {
panic(err)
}
defer upg.Stop()
done := make(chan struct{}, 1)
// Do an upgrade on SIGHUP
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR2)
for sg := range sig {
switch sg {
case syscall.SIGUSR2:
log.Println("got signal", sg.String())
uErr := upg.Upgrade()
if uErr != nil {
log.Println("Upgrade failed:", err)
}
done <- struct{}{}
ok, sdErr := daemon.SdNotify(false, daemon.SdNotifyReloading)
log.Println("sd notify:", ok, sdErr)
case syscall.SIGTERM:
ok, sdErr := daemon.SdNotify(false, daemon.SdNotifyStopping)
log.Println("sd notify:", ok, sdErr)
}
}
}()
// Listen must be called before Ready
ln, err := upg.Listen("tcp", *listenAddr)
if err != nil {
log.Fatalln("Can't listen:", err)
}
server := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.String())
w.Write([]byte("{}"))
return
}),
}
go func() {
log.Println("Listening on", ln.Addr())
err := server.Serve(ln)
if err != http.ErrServerClosed {
log.Println("HTTP server:", err)
}
}()
if err = upg.Ready(); err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err = upg.WaitForParent(ctx); err != nil {
log.Println("WaitForParent:", err)
}
log.Printf("running")
ok, sdErr := daemon.SdNotify(false, daemon.SdNotifyReady)
log.Println("sd notify:", ok, sdErr)
if upg.HasParent() {
os.Setenv("WATCHDOG_PID", strconv.Itoa(os.Getpid()))
}
if wdInt, wdErr := daemon.SdWatchdogEnabled(false); wdErr == nil && wdInt != 0 {
log.Println("Watchdog started:", os.Environ())
log.Println(os.Getenv("WATCHDOG_USEC"), os.Getenv("WATCHDOG_PID"))
timer := time.NewTimer(time.Duration(wdInt) / 2)
defer timer.Stop()
go func() {
var (
wdOk bool
tick time.Time
)
for {
select {
case <-done:
return
case tick = <-timer.C:
wdOk, wdErr = daemon.SdNotify(false, daemon.SdNotifyWatchdog)
log.Println("sd wd notify:", tick, wdOk, wdErr)
timer.Reset(time.Duration(wdInt) / 2)
}
}
}()
} else {
log.Println("Watchdog skipped:", wdInt, sdErr, os.Environ())
log.Println(os.Getenv("WATCHDOG_USEC"), os.Getenv("WATCHDOG_PID"))
}
<-upg.Exit()
// Make sure to set a deadline on exiting the process
// after upg.Exit() is closed. No new upgrades can be
// performed if the parent doesn't exit.
time.AfterFunc(30*time.Second, func() {
log.Println("Graceful shutdown timed out")
os.Exit(1)
})
// Wait for connections to drain.
server.Shutdown(context.Background())
time.Sleep(1 * time.Second)
log.Println("server shutdown")
}
[Unit]
Description=test daemon.
After=network.target
After=nss-lookup.target
[Service]
Type=notify
ExecStart=/usr/bin/tft --pid-file=/run/tft.pid
ExecReload=/bin/kill -USR2 $MAINPID
ExecStop=/bin/kill -15 $MAINPID
Restart=always
PIDFile=/run/tft.pid
WatchdogSec=30
[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment