Skip to content

Commit a2664b9

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 a2664b9

2 files changed

Lines changed: 71 additions & 15 deletions

File tree

pkg/manager/monitor.go

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

117117
if rawConn, err = uc.SyscallConn(); err != nil {
118+
if closeErr := uc.Close(); closeErr != nil {
119+
log.L.WithError(closeErr).Warnf("close liveness connection for daemon %s", id)
120+
}
118121
return
119122
}
120123

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
124+
if err = controlFD(rawConn, func(fd FD) error {
125+
if err := unix.SetNonblock(int(fd), true); err != nil {
126+
return errors.Wrapf(err, "set connection non-blocking, daemon id %s path %s", id, path)
126127
}
127128

128129
event := unix.EpollEvent{
129130
Fd: int32(fd),
130131
Events: unix.EPOLLHUP | unix.EPOLLERR | unix.EPOLLET,
131132
}
132133

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
134+
if err := unix.EpollCtl(m.epollFd, unix.EPOLL_CTL_ADD, int(fd), &event); err != nil {
135+
return errors.Wrapf(err, "add connection to epoll interest list, daemon id %s path %s", id, path)
137136
}
138137
target := &target{uc: uc, id: id, path: path}
139138

140139
// Only add subscribed target when everything is OK.
141140
m.set[fd] = target
142141
m.subscribers[id] = target
143142
target.notifier = notifier
144-
})
143+
return nil
144+
}); err != nil {
145+
// Leave nothing behind on failure: reporting success for a daemon that
146+
// was never added to the epoll interest list means its death would
147+
// never be noticed and recovery would never start.
148+
if closeErr := uc.Close(); closeErr != nil {
149+
log.L.WithError(closeErr).Warnf("close liveness connection for daemon %s", id)
150+
}
151+
return err
152+
}
145153

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

148156
return
149157
}
150158

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

172193
// No longer wait for event, delete it from interest list.
173-
if err = rawConn.Control(func(fd uintptr) {
194+
if err = controlFD(rawConn, func(fd FD) error {
195+
// The set entry must go even if EPOLL_CTL_DEL fails: the connection is
196+
// closed below, which removes the fd from the interest list anyway, and
197+
// a stale entry would collide with a future subscription reusing the
198+
// same fd number.
199+
delete(m.set, fd)
174200
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
201+
return errors.Wrapf(err, "delete event fd %d for supervisor %s", int(fd), id)
177202
}
178-
delete(m.set, fd)
203+
return nil
179204
}); err != nil {
180-
return errors.Wrapf(err, "remove target FD in the interested list, id=%s", id)
205+
log.L.WithError(err).Warnf("remove target FD from the interest list, id=%s", id)
181206
}
182207

183208
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)