@@ -15,11 +15,6 @@ import (
1515 "github.com/mudler/xlog"
1616)
1717
18- // backendStopRequest is the request payload for backend.stop (fire-and-forget).
19- type backendStopRequest struct {
20- Backend string `json:"backend"`
21- }
22-
2318// NodeCommandSender abstracts NATS-based commands to worker nodes.
2419// Used by HTTP endpoint handlers to avoid coupling to the concrete RemoteUnloaderAdapter.
2520//
@@ -78,28 +73,44 @@ func (a *RemoteUnloaderAdapter) InstallTimeout() time.Duration {
7873
7974// UnloadRemoteModel finds the node(s) hosting the given model and tells them
8075// to stop their backend process via NATS backend.stop event.
81- // The worker process handles: Free() → kill process.
76+ // The worker process handles a bounded Free() followed by process termination;
77+ // forced shutdown skips Free().
8278// This is called by ModelLoader.deleteProcess() when process == nil (remote model).
8379func (a * RemoteUnloaderAdapter ) UnloadRemoteModel (modelName string ) error {
84- ctx := context .Background ()
80+ return a .UnloadRemoteModelContext (context .Background (), modelName , false )
81+ }
82+
83+ // UnloadRemoteModelContext is the cancellation-aware extension used by the
84+ // model loader to preserve forced shutdown across the distributed boundary.
85+ func (a * RemoteUnloaderAdapter ) UnloadRemoteModelContext (ctx context.Context , modelName string , force bool ) error {
86+ if ctx == nil {
87+ ctx = context .Background ()
88+ }
8589 nodes , err := a .registry .FindNodesWithModel (ctx , modelName )
86- if err != nil || len (nodes ) == 0 {
90+ if err != nil {
91+ return fmt .Errorf ("finding nodes with model %q: %w" , modelName , err )
92+ }
93+ if len (nodes ) == 0 {
8794 xlog .Debug ("No remote nodes found with model" , "model" , modelName )
8895 return nil
8996 }
9097
98+ var unloadErr error
9199 for _ , node := range nodes {
92- xlog .Info ("Sending NATS backend.stop to node" , "model" , modelName , "node" , node .Name , "nodeID" , node .ID )
93- if err := a .StopBackend (node .ID , modelName ); err != nil {
100+ xlog .Info ("Sending NATS backend.stop to node" , "model" , modelName , "node" , node .Name , "nodeID" , node .ID , "force" , force )
101+ if err := a .stopBackend (node .ID , modelName , force ); err != nil {
94102 xlog .Warn ("Failed to send backend.stop" , "node" , node .Name , "error" , err )
103+ unloadErr = errors .Join (unloadErr , fmt .Errorf ("stopping model on node %s: %w" , node .ID , err ))
95104 continue
96105 }
97106 // Remove every replica of this model on the node — the worker will
98107 // handle the actual process cleanup.
99- a .registry .RemoveAllNodeModelReplicas (ctx , node .ID , modelName )
108+ if err := a .registry .RemoveAllNodeModelReplicas (ctx , node .ID , modelName ); err != nil {
109+ unloadErr = errors .Join (unloadErr , fmt .Errorf ("removing model replicas from node %s: %w" , node .ID , err ))
110+ }
100111 }
101112
102- return nil
113+ return unloadErr
103114}
104115
105116// InstallBackend sends a backend.install request-reply to a worker node.
@@ -142,7 +153,9 @@ func (a *RemoteUnloaderAdapter) InstallBackend(
142153 }, a .installTimeout )
143154
144155 if sub != nil {
145- _ = sub .Unsubscribe ()
156+ if unsubscribeErr := sub .Unsubscribe (); unsubscribeErr != nil {
157+ xlog .Warn ("Failed to unsubscribe from backend install progress" , "nodeID" , nodeID , "backend" , backendType , "opID" , opID , "error" , unsubscribeErr )
158+ }
146159 }
147160
148161 if err != nil && isNATSTimeout (err ) {
@@ -216,7 +229,9 @@ func (a *RemoteUnloaderAdapter) UpgradeBackend(nodeID, backendType, galleriesJSO
216229 }, a .upgradeTimeout )
217230
218231 if sub != nil {
219- _ = sub .Unsubscribe ()
232+ if unsubscribeErr := sub .Unsubscribe (); unsubscribeErr != nil {
233+ xlog .Warn ("Failed to unsubscribe from backend upgrade progress" , "nodeID" , nodeID , "backend" , backendType , "opID" , opID , "error" , unsubscribeErr )
234+ }
220235 }
221236
222237 if err != nil && isNATSTimeout (err ) {
@@ -250,7 +265,9 @@ func (a *RemoteUnloaderAdapter) installWithForceFallback(nodeID, backendType, ga
250265 }, a .upgradeTimeout )
251266
252267 if sub != nil {
253- _ = sub .Unsubscribe ()
268+ if unsubscribeErr := sub .Unsubscribe (); unsubscribeErr != nil {
269+ xlog .Warn ("Failed to unsubscribe from legacy backend install progress" , "nodeID" , nodeID , "backend" , backendType , "opID" , opID , "error" , unsubscribeErr )
270+ }
254271 }
255272
256273 if err != nil && isNATSTimeout (err ) {
@@ -272,14 +289,15 @@ func (a *RemoteUnloaderAdapter) ListBackends(nodeID string) (*messaging.BackendL
272289// If backend is empty, the worker stops ALL backends.
273290// The node stays registered and can receive another InstallBackend later.
274291func (a * RemoteUnloaderAdapter ) StopBackend (nodeID , backend string ) error {
292+ return a .stopBackend (nodeID , backend , false )
293+ }
294+
295+ func (a * RemoteUnloaderAdapter ) stopBackend (nodeID , backend string , force bool ) error {
275296 subject := messaging .SubjectNodeBackendStop (nodeID )
276- if backend == "" {
297+ if backend == "" && ! force {
277298 return a .nats .Publish (subject , nil )
278299 }
279- req := struct {
280- Backend string `json:"backend"`
281- }{Backend : backend }
282- return a .nats .Publish (subject , req )
300+ return a .nats .Publish (subject , messaging.BackendStopRequest {Backend : backend , Force : force })
283301}
284302
285303// DeleteBackend tells a worker node to delete a backend (stop + remove files).
0 commit comments