Skip to content

Commit 2e42d0e

Browse files
fix(manager): surface liveness subscribe failures instead of reporting success
livenessMonitor.Subscribe armed the epoll subscription inside a syscall.RawConn.Control callback that assigned failures to the function's named error return -- which the enclosing `err = rawConn.Control(...)` statement then overwrote with Control's own nil result. A failed unix.SetNonblock or unix.EpollCtl was therefore logged, "Subscribe daemon ... liveness event" was still printed, and Subscribe returned nil. The caller then believes the daemon is monitored when it was never added to the epoll interest list: when that nydusd later dies no death event fires, so restart/failover recovery never starts and the daemon's containers are left with dead FUSE mounts -- with nothing in the logs explaining why. The dialed unix connection also leaked on this path. Introduce a controlFD helper that returns the first error from either Control itself or the callback, make Subscribe all-or-nothing (close the connection and register nothing on failure), and use the same helper in unsubscribe, where an EPOLL_CTL_DEL failure previously left a stale fd entry in the interest set that could collide with a future subscription reusing the same fd number. Signed-off-by: Iaroslav Geraskin <iaroslav@reflection.ai>
1 parent 3285775 commit 2e42d0e

2 files changed

Lines changed: 67 additions & 15 deletions

File tree

pkg/manager/monitor.go

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,39 +115,56 @@ func (m *livenessMonitor) Subscribe(id string, path string, notifier chan<- deat
115115
}
116116

117117
if rawConn, err = uc.SyscallConn(); err != nil {
118+
uc.Close()
118119
return
119120
}
120121

121-
err = rawConn.Control(func(fd FD) {
122-
err = unix.SetNonblock(int(fd), true)
123-
if err != nil {
124-
log.L.Errorf("Failed to set file. daemon id %s path %s. %v", id, path, err)
125-
return
122+
if err = controlFD(rawConn, func(fd FD) error {
123+
if err := unix.SetNonblock(int(fd), true); err != nil {
124+
return errors.Wrapf(err, "set connection non-blocking, daemon id %s path %s", id, path)
126125
}
127126

128127
event := unix.EpollEvent{
129128
Fd: int32(fd),
130129
Events: unix.EPOLLHUP | unix.EPOLLERR | unix.EPOLLET,
131130
}
132131

133-
err = unix.EpollCtl(m.epollFd, unix.EPOLL_CTL_ADD, int(fd), &event)
134-
if err != nil {
135-
log.L.Errorf("Failed to control epoll. daemon id %s path %s. %v", id, path, err)
136-
return
132+
if err := unix.EpollCtl(m.epollFd, unix.EPOLL_CTL_ADD, int(fd), &event); err != nil {
133+
return errors.Wrapf(err, "add connection to epoll interest list, daemon id %s path %s", id, path)
137134
}
138135
target := &target{uc: uc, id: id, path: path}
139136

140137
// Only add subscribed target when everything is OK.
141138
m.set[fd] = target
142139
m.subscribers[id] = target
143140
target.notifier = notifier
144-
})
141+
return nil
142+
}); err != nil {
143+
// Leave nothing behind on failure: reporting success for a daemon that
144+
// was never added to the epoll interest list means its death would
145+
// never be noticed and recovery would never start.
146+
uc.Close()
147+
return err
148+
}
145149

146150
log.L.Infof("Subscribe daemon %s liveness event, path=%s.", id, path)
147151

148152
return
149153
}
150154

155+
// controlFD runs fn on the raw connection's file descriptor and returns the
156+
// first error from either the Control call itself or from fn.
157+
// syscall.RawConn.Control only reports its own failure; an error assigned
158+
// inside the callback to a variable that Control's result is later assigned to
159+
// would be silently overwritten.
160+
func controlFD(rc syscall.RawConn, fn func(fd FD) error) error {
161+
var fnErr error
162+
if err := rc.Control(func(fd uintptr) { fnErr = fn(fd) }); err != nil {
163+
return err
164+
}
165+
return fnErr
166+
}
167+
151168
func (m *livenessMonitor) Unsubscribe(id string) (err error) {
152169
m.mu.Lock()
153170
defer m.mu.Unlock()
@@ -170,14 +187,18 @@ func (m *livenessMonitor) unsubscribe(id string) (err error) {
170187
}
171188

172189
// No longer wait for event, delete it from interest list.
173-
if err = rawConn.Control(func(fd uintptr) {
190+
if err = controlFD(rawConn, func(fd FD) error {
191+
// The set entry must go even if EPOLL_CTL_DEL fails: the connection is
192+
// closed below, which removes the fd from the interest list anyway, and
193+
// a stale entry would collide with a future subscription reusing the
194+
// same fd number.
195+
delete(m.set, fd)
174196
if err := unix.EpollCtl(m.epollFd, unix.EPOLL_CTL_DEL, int(fd), &unix.EpollEvent{}); err != nil {
175-
log.L.Errorf("Fail to delete event fd %d for supervisor %s", int(fd), id)
176-
return
197+
return errors.Wrapf(err, "delete event fd %d for supervisor %s", int(fd), id)
177198
}
178-
delete(m.set, fd)
199+
return nil
179200
}); err != nil {
180-
return errors.Wrapf(err, "remove target FD in the interested list, id=%s", id)
201+
log.L.WithError(err).Warnf("remove target FD from the interest list, id=%s", id)
181202
}
182203

183204
if err = target.uc.Close(); err != nil {

pkg/manager/monitor_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
"github.com/stretchr/testify/assert"
18+
"golang.org/x/sys/unix"
1819
)
1920

2021
func startUnixServer(ctx context.Context, sock string) {
@@ -44,6 +45,36 @@ func startUnixServer(ctx context.Context, sock string) {
4445

4546
}
4647

48+
func TestLivenessMonitorSubscribeReportsArmFailure(t *testing.T) {
49+
sock, err := os.CreateTemp("", "liveness_monitor_sock")
50+
assert.Nil(t, err)
51+
sock.Close()
52+
t.Cleanup(func() { os.Remove(sock.Name()) })
53+
54+
ctx, cancel := context.WithCancel(context.Background())
55+
t.Cleanup(cancel)
56+
go startUnixServer(ctx, sock.Name())
57+
58+
monitor, err := newMonitor()
59+
assert.Nil(t, err)
60+
assert.NotNil(t, monitor)
61+
time.Sleep(time.Millisecond * 200)
62+
63+
// Sabotage the epoll fd so that arming the subscription (EPOLL_CTL_ADD)
64+
// fails. Subscribe must report the failure instead of pretending the
65+
// daemon is monitored: an unmonitored daemon would never produce a death
66+
// event, so its recovery would never start.
67+
assert.Nil(t, unix.Close(monitor.epollFd))
68+
69+
notifier := make(chan deathEvent, 1)
70+
err = monitor.Subscribe("daemon_1", sock.Name(), notifier)
71+
assert.NotNil(t, err)
72+
73+
// All-or-nothing: no half-registered state may remain.
74+
assert.Equal(t, 0, len(monitor.subscribers))
75+
assert.Equal(t, 0, len(monitor.set))
76+
}
77+
4778
func TestLivenessMonitor(t *testing.T) {
4879
sockPattern := "liveness_monitor_sock"
4980

0 commit comments

Comments
 (0)