Skip to content

Commit c77b716

Browse files
committed
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 <[email protected]>
1 parent 7d375ec commit c77b716

File tree

4 files changed

+97
-1
lines changed

4 files changed

+97
-1
lines changed

daemon/sdnotify.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ package daemon
2424
import (
2525
"net"
2626
"os"
27+
"strconv"
28+
29+
"golang.org/x/sys/unix"
2730
)
2831

2932
const (
@@ -82,3 +85,16 @@ func SdNotify(unsetEnvironment bool, state string) (bool, error) {
8285
}
8386
return true, nil
8487
}
88+
89+
// SdNotifyMonotonicUsec returns a MONOTONIC_USEC=... assignment for the current time.
90+
// This is typically used with [SdNotifyReloading].
91+
//
92+
// If the monotonic clock is not available on the system, the empty string is returned.
93+
func SdNotifyMonotonicUsec() string {
94+
var ts unix.Timespec
95+
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts); err != nil {
96+
// Monotonic clock is not available on this system.
97+
return ""
98+
}
99+
return "MONOTONIC_USEC=" + strconv.FormatInt(ts.Nano()/1000, 10)
100+
}

daemon/sdnotify_linux_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2024 CoreOS, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package daemon
16+
17+
import (
18+
"errors"
19+
"strconv"
20+
"strings"
21+
"testing"
22+
"time"
23+
24+
"golang.org/x/sys/unix"
25+
)
26+
27+
// TestUsec checks that SdNotifyMonotonicUsec is probably not returning complete garbage.
28+
func TestUsec(t *testing.T) {
29+
var resolution unix.Timespec
30+
if err := unix.ClockGetres(unix.CLOCK_MONOTONIC, &resolution); err != nil {
31+
if errors.Is(err, unix.EINVAL) {
32+
t.Log("CLOCK_MONOTONIC is not supported on this system")
33+
if got := SdNotifyMonotonicUsec(); got != "" {
34+
t.Errorf("SdNotifyMonotonicUsec() = %q; want empty string", got)
35+
}
36+
return
37+
}
38+
t.Fatalf("ClockGetres(CLOCK_MONOTONIC) failed: %v", err)
39+
}
40+
41+
now := func() uint64 {
42+
got := SdNotifyMonotonicUsec()
43+
t.Logf("SdNotifyMonotonicUsec() = %q", got)
44+
if got == "" {
45+
t.Fatal("SdNotifyMonotonicUsec() returned empty string on system which supports CLOCK_MONOTONIC")
46+
}
47+
tag, val, ok := strings.Cut(got, "=")
48+
if !ok {
49+
t.Fatal("string is not a well-formed variable assignment")
50+
}
51+
if tag != "MONOTONIC_USEC" {
52+
t.Errorf("expected tag MONOTONIC_USEC, got %q", tag)
53+
}
54+
ts, err := strconv.ParseUint(val, 10, 64)
55+
if err != nil {
56+
t.Fatalf("value %q is not well-formed: %v", val, err)
57+
}
58+
if ts == 0 {
59+
// CLOCK_MONOTONIC is defined on Linux as the number of seconds
60+
// since boot, per clock_gettime(2). A timestamp of zero is
61+
// almost certainly bogus.
62+
t.Fatal("timestamp is zero")
63+
}
64+
return ts
65+
}
66+
67+
start := now()
68+
time.Sleep(time.Duration(resolution.Nano()) * 3)
69+
ts := now()
70+
if ts < start {
71+
t.Errorf("timestamp went backwards: %d < %d", ts, start)
72+
} else if ts == start {
73+
t.Errorf("timestamp did not advance: %d == %d", ts, start)
74+
}
75+
}

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/coreos/go-systemd/v22
22

33
go 1.12
44

5-
require github.com/godbus/dbus/v5 v5.1.0
5+
require (
6+
github.com/godbus/dbus/v5 v5.1.0
7+
golang.org/x/sys v0.17.0
8+
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
22
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
3+
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
4+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

0 commit comments

Comments
 (0)