Skip to content

Commit bdf9303

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 bdf9303

File tree

5 files changed

+144
-1
lines changed

5 files changed

+144
-1
lines changed

daemon/sdnotify_linux_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2025 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+
"strconv"
19+
"strings"
20+
"testing"
21+
"time"
22+
23+
"golang.org/x/sys/unix"
24+
)
25+
26+
// TestSdNotifyMonotonicUsec checks that SdNotifyMonotonicUsec is probably not returning complete garbage.
27+
func TestSdNotifyMonotonicUsec(t *testing.T) {
28+
var resolution unix.Timespec
29+
if err := unix.ClockGetres(unix.CLOCK_MONOTONIC, &resolution); err != nil {
30+
if err == unix.EINVAL {
31+
t.Log("CLOCK_MONOTONIC is not supported on this system")
32+
if got := SdNotifyMonotonicUsec(); got != "" {
33+
t.Errorf("SdNotifyMonotonicUsec() = %q; want empty string", got)
34+
}
35+
return
36+
}
37+
t.Fatalf("ClockGetres(CLOCK_MONOTONIC) failed: %v", err)
38+
}
39+
40+
now := func() uint64 {
41+
got := SdNotifyMonotonicUsec()
42+
t.Logf("SdNotifyMonotonicUsec() = %q", got)
43+
if got == "" {
44+
t.Fatal("SdNotifyMonotonicUsec() returned empty string on system which supports CLOCK_MONOTONIC")
45+
}
46+
fields := strings.SplitN(got, "=", 2)
47+
if len(fields) != 2 {
48+
t.Fatal("string is not a well-formed variable assignment")
49+
}
50+
tag, val := fields[0], fields[1]
51+
if tag != "MONOTONIC_USEC" {
52+
t.Errorf("expected tag MONOTONIC_USEC, got %q", tag)
53+
}
54+
if val[len(val)-1] != '\n' {
55+
t.Errorf("expected value to end with newline, got %q", val)
56+
}
57+
ts, err := strconv.ParseUint(val[:len(val)-1], 10, 64)
58+
if err != nil {
59+
t.Fatalf("value %q is not well-formed: %v", val, err)
60+
}
61+
if ts == 0 {
62+
// CLOCK_MONOTONIC is defined on Linux as the number of seconds
63+
// since boot, per clock_gettime(2). A timestamp of zero is
64+
// almost certainly bogus.
65+
t.Fatal("timestamp is zero")
66+
}
67+
return ts
68+
}
69+
70+
start := now()
71+
time.Sleep(time.Duration(resolution.Nano()) * 3)
72+
ts := now()
73+
if ts < start {
74+
t.Errorf("timestamp went backwards: %d < %d", ts, start)
75+
} else if ts == start {
76+
t.Errorf("timestamp did not advance: %d == %d", ts, start)
77+
}
78+
}

daemon/sdnotify_other.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2025 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+
//go:build !unix
16+
// +build !unix
17+
18+
package daemon
19+
20+
// SdNotifyMonotonicUsec returns the empty string on unsupported platforms.
21+
func SdNotifyMonotonicUsec() string {
22+
return ""
23+
}

daemon/sdnotify_unix.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 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+
//go:build unix
16+
// +build unix
17+
18+
package daemon
19+
20+
import (
21+
"strconv"
22+
23+
"golang.org/x/sys/unix"
24+
)
25+
26+
// SdNotifyMonotonicUsec returns a MONOTONIC_USEC=... assignment for the current time
27+
// with a trailing newline included. This is typically used with [SdNotifyReloading].
28+
//
29+
// If the monotonic clock is not available on the system, the empty string is returned.
30+
func SdNotifyMonotonicUsec() string {
31+
var ts unix.Timespec
32+
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts); err != nil {
33+
// Monotonic clock is not available on this system.
34+
return ""
35+
}
36+
return "MONOTONIC_USEC=" + strconv.FormatInt(ts.Nano()/1000, 10) + "\n"
37+
}

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.0.0-20190907184412-d223b2b6db03
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.0.0-20190907184412-d223b2b6db03 h1:b3JiLYVaG9kHjTcOQIoUh978YMCO7oVTQQBLudU47zY=
4+
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

0 commit comments

Comments
 (0)