@@ -11,6 +11,7 @@ import (
1111 "os"
1212 "os/exec"
1313 "strings"
14+ "syscall"
1415 "time"
1516
1617 "github.com/containerd/log"
@@ -28,6 +29,11 @@ import (
2829
2930const endpointGetBackend string = "/api/v1/daemons/%s/backend"
3031
32+ // defaultDaemonTerminationTimeout is how long we wait for a failed nydusd to
33+ // exit after SIGTERM before escalating to SIGKILL. It is a var so tests can
34+ // shorten it.
35+ var defaultDaemonTerminationTimeout = 5 * time .Second
36+
3137// Spawn a nydusd daemon to serve the daemon instance.
3238//
3339// When returning from `StartDaemon()` with out error:
@@ -81,22 +87,42 @@ func (m *Manager) StartDaemon(d *daemon.Daemon) error {
8187 log .L .Errorf ("Fail to update daemon info (%+v) to DB: %v" , d , err )
8288 }
8389
84- // If nydusd fails startup, manager can't subscribe its death event.
85- // So we can ignore the subscribing error.
90+ // Verify the daemon actually comes up in the background. If it does not,
91+ // terminate the spawned process so it cannot linger as an orphan that keeps
92+ // pulling from the registry (see #771). `proc` is captured here so we act on
93+ // exactly the process we started, regardless of any concurrent pid changes.
94+ proc := cmd .Process
8695 go func () {
8796 if err := daemon .WaitUntilSocketExisted (d .GetAPISock (), d .States .ProcessID ); err != nil {
88- // FIXME: Should clean the daemon record in DB if the nydusd fails starting
89- log . L . Errorf ( "Nydusd %s probably not started" , d . ID () )
97+ log . L . Errorf ( "Nydusd %s probably not started, terminating it: %v" , d . ID (), err )
98+ m . terminateFailedDaemon ( d , proc )
9099 return
91100 }
92101
93102 if err = m .SubscribeDaemonEvent (d ); err != nil {
94- log .L .Errorf ("Nydusd %s probably not started" , d .ID ())
103+ log .L .Errorf ("Failed to subscribe nydusd %s events, terminating it: %v" , d .ID (), err )
104+ m .terminateFailedDaemon (d , proc )
95105 return
96106 }
97107
98108 if err := d .WaitUntilState (types .DaemonStateRunning ); err != nil {
99- log .L .WithError (err ).Errorf ("daemon %s is not managed to reach RUNNING state" , d .ID ())
109+ // A failover-managed daemon (recover_policy=failover) legitimately
110+ // stays in INIT/READY while the failover/upgrade flow drives
111+ // INIT -> TakeOver -> Start after StartDaemon returns. Do not treat
112+ // that as a failed startup: killing or unsubscribing it here would
113+ // wreck the takeover. Its cleanup belongs to the failover flow.
114+ if d .Supervisor != nil {
115+ log .L .WithError (err ).Warnf ("daemon %s did not reach RUNNING yet, leaving it to the failover flow" , d .ID ())
116+ return
117+ }
118+ log .L .WithError (err ).Errorf ("daemon %s is not managed to reach RUNNING state, terminating it" , d .ID ())
119+ // Unsubscribe before killing, otherwise the liveness monitor would
120+ // observe the kill as a death event and (with recover_policy=restart)
121+ // immediately respawn what we just decided to reap.
122+ if uerr := m .UnsubscribeDaemonEvent (d ); uerr != nil {
123+ log .L .WithError (uerr ).Warnf ("unsubscribe daemon %s after startup failure" , d .ID ())
124+ }
125+ m .terminateFailedDaemon (d , proc )
100126 return
101127 }
102128
@@ -119,6 +145,71 @@ func (m *Manager) StartDaemon(d *daemon.Daemon) error {
119145 return nil
120146}
121147
148+ // terminateFailedDaemon best-effort stops a nydusd that failed startup
149+ // verification, so it stops pulling from the registry instead of lingering as
150+ // an orphan. SIGTERM is tried first and escalated to SIGKILL if the process,
151+ // for example one wedged on a slow registry, does not exit in time. The process
152+ // is finally reaped to avoid leaving a zombie. It operates on the captured
153+ // process handle, so it is safe against pid reuse and concurrent teardown.
154+ func (m * Manager ) terminateFailedDaemon (d * daemon.Daemon , proc * os.Process ) {
155+ if proc == nil {
156+ return
157+ }
158+
159+ // A failover-managed daemon (recover_policy=failover) legitimately stays in
160+ // INIT/READY while the failover flow drives INIT -> TakeOver -> Start after
161+ // StartDaemon returns, so a "not RUNNING yet" verdict here may be a takeover
162+ // in progress rather than a failed startup. Killing it would destroy the
163+ // takeover, so leave its cleanup to the failover flow.
164+ if d .Supervisor != nil {
165+ log .L .Warnf ("nydusd %s (pid %d) failed startup verification, leaving cleanup to the failover flow" , d .ID (), proc .Pid )
166+ return
167+ }
168+
169+ // For a shared daemon, if it fails to reach RUNNING, it won't be retained
170+ // by TryRetainSharedDaemon. We must kill it here so it doesn't linger.
171+ // But if it is already retained (e.g., this is a spurious timeout or
172+ // TryRetainSharedDaemon was called concurrently), killing it would drop
173+ // the active shared daemon. We check IsSharedDaemon() and whether it is
174+ // the currently active one.
175+ if d .IsSharedDaemon () && m .isDaemonRetainedAsShared (d ) {
176+ log .L .Warnf ("nydusd %s (pid %d) failed startup verification but is already retained as shared daemon, not killing it" , d .ID (), proc .Pid )
177+ return
178+ }
179+
180+ log .L .Warnf ("terminating nydusd %s (pid %d) that failed to start" , d .ID (), proc .Pid )
181+
182+ done := make (chan struct {})
183+ go func () {
184+ _ , _ = proc .Wait ()
185+ close (done )
186+ }()
187+
188+ if err := proc .Signal (syscall .SIGTERM ); err != nil && ! errors .Is (err , os .ErrProcessDone ) {
189+ log .L .WithError (err ).Warnf ("send SIGTERM to nydusd %s (pid %d)" , d .ID (), proc .Pid )
190+ }
191+
192+ select {
193+ case <- done :
194+ case <- time .After (defaultDaemonTerminationTimeout ):
195+ log .L .Warnf ("nydusd %s (pid %d) did not exit after SIGTERM, sending SIGKILL" , d .ID (), proc .Pid )
196+ if err := proc .Kill (); err != nil && ! errors .Is (err , os .ErrProcessDone ) {
197+ log .L .WithError (err ).Warnf ("send SIGKILL to nydusd %s (pid %d)" , d .ID (), proc .Pid )
198+ }
199+ <- done
200+ }
201+ }
202+
203+ // isDaemonRetainedAsShared checks if the daemon is currently retained as the
204+ // active shared daemon in the filesystem manager. This is a callback provided
205+ // by the manager to avoid circular dependencies.
206+ func (m * Manager ) isDaemonRetainedAsShared (d * daemon.Daemon ) bool {
207+ if m .IsSharedDaemonRetained != nil {
208+ return m .IsSharedDaemonRetained (d )
209+ }
210+ return false
211+ }
212+
122213// Build commandline according to nydusd daemon configuration.
123214func (m * Manager ) BuildDaemonCommand (d * daemon.Daemon , bin string , upgrade bool ) (* exec.Cmd , error ) {
124215 var cmdOpts []command.Opt
0 commit comments