From 42c016801809734954479352a47d2c98e19a79aa Mon Sep 17 00:00:00 2001 From: Cory Snider Date: Tue, 13 Feb 2024 13:14:59 -0500 Subject: [PATCH] daemon: add SdNotifyMonotonicUsec helper function The synchronized service reload protocol added in systemd version 253 requires that the service provides a MONOTONIC_USEC field alongside the RELOADING=1 notification message for synchronization purposes. The value carried in this field must be the system CLOCK_MONOTONIC timestamp at the time the notification message was generated as systemd compares it to other CLOCK_MONOTONIC timestamps taken by pid1. While the Go standard library does utilize CLOCK_MONOTONIC in the implementation of package "time", the absolute monotonic timestamps in time.Time values are not made available to programmers. Users familiar with idiomatic usage of monotonic timestamps in Go might (incorrectly) try to implement MONOTONIC_USEC using process-relative monotonic timestamps, like so: var processStart = time.Now() func NotifyReloadingINCORRECT() { ts := time.Since(processStart)/time.Microsecond // WRONG msg := fmt.Sprintf( daemon.SdNotifyReload+"\nMONOTONIC_USEC=%d", ts, ) _, _ = daemon.SdNotify(false, msg) } Help users fall into the pit of success by providing a helper function SdNotifyMonotonicUsec() which returns a MONOTONIC_USEC string which encodes the system CLOCK_MONOTONIC timestamp in decimal microseconds, as systemd expects. Signed-off-by: Cory Snider --- daemon/sdnotify.go | 16 ++++++++ daemon/sdnotify_linux_test.go | 75 +++++++++++++++++++++++++++++++++++ go.mod | 5 ++- go.sum | 2 + 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 daemon/sdnotify_linux_test.go diff --git a/daemon/sdnotify.go b/daemon/sdnotify.go index ba4ae31f..ad5e3061 100644 --- a/daemon/sdnotify.go +++ b/daemon/sdnotify.go @@ -24,6 +24,9 @@ package daemon import ( "net" "os" + "strconv" + + "golang.org/x/sys/unix" ) const ( @@ -82,3 +85,16 @@ func SdNotify(unsetEnvironment bool, state string) (bool, error) { } return true, nil } + +// SdNotifyMonotonicUsec returns a MONOTONIC_USEC=... assignment for the current time. +// This is typically used with [SdNotifyReloading]. +// +// If the monotonic clock is not available on the system, the empty string is returned. +func SdNotifyMonotonicUsec() string { + var ts unix.Timespec + if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts); err != nil { + // Monotonic clock is not available on this system. + return "" + } + return "MONOTONIC_USEC=" + strconv.FormatInt(ts.Nano()/1000, 10) +} diff --git a/daemon/sdnotify_linux_test.go b/daemon/sdnotify_linux_test.go new file mode 100644 index 00000000..ea6786ee --- /dev/null +++ b/daemon/sdnotify_linux_test.go @@ -0,0 +1,75 @@ +// Copyright 2024 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "strconv" + "strings" + "testing" + "time" + + "golang.org/x/sys/unix" +) + +// TestUsec checks that SdNotifyMonotonicUsec is probably not returning complete garbage. +func TestUsec(t *testing.T) { + var resolution unix.Timespec + if err := unix.ClockGetres(unix.CLOCK_MONOTONIC, &resolution); err != nil { + if err == unix.EINVAL { + t.Log("CLOCK_MONOTONIC is not supported on this system") + if got := SdNotifyMonotonicUsec(); got != "" { + t.Errorf("SdNotifyMonotonicUsec() = %q; want empty string", got) + } + return + } + t.Fatalf("ClockGetres(CLOCK_MONOTONIC) failed: %v", err) + } + + now := func() uint64 { + got := SdNotifyMonotonicUsec() + t.Logf("SdNotifyMonotonicUsec() = %q", got) + if got == "" { + t.Fatal("SdNotifyMonotonicUsec() returned empty string on system which supports CLOCK_MONOTONIC") + } + fields := strings.SplitN(got, "=", 2) + if len(fields) != 2 { + t.Fatal("string is not a well-formed variable assignment") + } + tag, val := fields[0], fields[1] + if tag != "MONOTONIC_USEC" { + t.Errorf("expected tag MONOTONIC_USEC, got %q", tag) + } + ts, err := strconv.ParseUint(val, 10, 64) + if err != nil { + t.Fatalf("value %q is not well-formed: %v", val, err) + } + if ts == 0 { + // CLOCK_MONOTONIC is defined on Linux as the number of seconds + // since boot, per clock_gettime(2). A timestamp of zero is + // almost certainly bogus. + t.Fatal("timestamp is zero") + } + return ts + } + + start := now() + time.Sleep(time.Duration(resolution.Nano()) * 3) + ts := now() + if ts < start { + t.Errorf("timestamp went backwards: %d < %d", ts, start) + } else if ts == start { + t.Errorf("timestamp did not advance: %d == %d", ts, start) + } +} diff --git a/go.mod b/go.mod index ec23c709..e052c3b8 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/coreos/go-systemd/v22 go 1.12 -require github.com/godbus/dbus/v5 v5.1.0 +require ( + github.com/godbus/dbus/v5 v5.1.0 + golang.org/x/sys v0.0.0-20180927150500-dad3d9fb7b6e +) diff --git a/go.sum b/go.sum index 024b2693..c026c973 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +golang.org/x/sys v0.0.0-20180927150500-dad3d9fb7b6e h1:AqqkRhkzWTerVplYmF0GrGl2ri2S5+F5xhJfDSc/SJY= +golang.org/x/sys v0.0.0-20180927150500-dad3d9fb7b6e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=