@@ -48,6 +48,7 @@ type ResumeInput struct {
4848// ResumeState holds the mutable state loaded and modified during execution.
4949type ResumeState struct {
5050 Actor * ateapipb.Actor
51+ Worker * ateapipb.Worker
5152 ActorTemplate * atev1alpha1.ActorTemplate
5253}
5354
@@ -80,6 +81,36 @@ func (s *LoadActorForResumeStep) Execute(ctx context.Context, input *ResumeInput
8081 }
8182 state .ActorTemplate = actorTemplate
8283
84+ // If the Actor is in Resuming state, it means a previous attempt crashed after AssignWorkerStep.
85+ // We don't need to repeat the AssignWorkerStep, load the Worker now.
86+ if actor .Status == ateapipb .Actor_STATUS_RESUMING {
87+ allPopulated := actor .AteomPodUid != "" && actor .WorkerPoolName != "" && actor .AteomPodName != ""
88+ if ! allPopulated {
89+ slog .ErrorContext (ctx , "expected all of AteomPodUid, WorkerPoolName and AteomPodName to be populated, found" ,
90+ slog .String ("AteomPodUid" , actor .AteomPodUid ),
91+ slog .String ("WorkerPoolName" , actor .WorkerPoolName ),
92+ slog .String ("AteomPodName" , actor .AteomPodName ))
93+
94+ // Crash the actor if its worker assignment is corrupted. We should never be in this state.
95+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
96+ return cerr
97+ }
98+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
99+ }
100+
101+ wk , err := s .store .GetWorker (ctx , actor .AteomPodNamespace , actor .WorkerPoolName , actor .AteomPodName )
102+ if err != nil {
103+ // Crash the actor if it was assigned to a deleted pod.
104+ if errors .Is (err , ErrWorkerPodNotFound ) {
105+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
106+ return cerr
107+ }
108+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
109+ }
110+ return fmt .Errorf ("failed to get already assigned worker for actor %w" , err )
111+ }
112+ state .Worker = wk
113+ }
83114 return nil
84115}
85116
@@ -116,19 +147,16 @@ type AssignWorkerStep struct {
116147func (s * AssignWorkerStep ) Name () string { return "AssignWorker" }
117148
118149func (s * AssignWorkerStep ) IsComplete (ctx context.Context , input * ResumeInput , state * ResumeState ) (bool , error ) {
119- // Only RUNNING is past this step. RESUMING intentionally re-runs because
120- // a retry must be able to release a stale worker whose pool became
121- // ineligible and pick a fresh one.
122- return state .Actor .GetStatus () == ateapipb .Actor_STATUS_RUNNING , nil
150+ // RESUMING means a previous attempt already assigned a worker (loaded by
151+ // LoadActorForResumeStep); RUNNING is past this step entirely.
152+ return state .Actor .GetStatus () == ateapipb .Actor_STATUS_RESUMING || state .Actor .GetStatus () == ateapipb .Actor_STATUS_RUNNING , nil
123153}
124154func (s * AssignWorkerStep ) CheckPrerequisite (ctx context.Context , input * ResumeInput , state * ResumeState ) error {
125- // The resume edge exists from SUSPENDED and PAUSED.
126- // RESUMING is allowed for retrying this step.
127155 switch state .Actor .GetStatus () {
128- case ateapipb .Actor_STATUS_SUSPENDED , ateapipb .Actor_STATUS_PAUSED , ateapipb . Actor_STATUS_RESUMING :
156+ case ateapipb .Actor_STATUS_SUSPENDED , ateapipb .Actor_STATUS_PAUSED :
129157 return nil
130158 default :
131- return status .Errorf (codes .FailedPrecondition , "AssignWorkerStep prerequisite not met for Actor: %s (got: %v, want %s, %s or %s)" , input .ActorName , state .Actor .GetStatus (), ateapipb .Actor_STATUS_SUSPENDED , ateapipb .Actor_STATUS_PAUSED , ateapipb . Actor_STATUS_RESUMING )
159+ return status .Errorf (codes .FailedPrecondition , "AssignWorkerStep prerequisite not met for Actor: %s (got: %v, want %s or %s)" , input .ActorName , state .Actor .GetStatus (), ateapipb .Actor_STATUS_SUSPENDED , ateapipb .Actor_STATUS_PAUSED )
132160 }
133161}
134162
@@ -138,13 +166,9 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
138166 return fmt .Errorf ("while listing workers: %w" , err )
139167 }
140168
141- var assignedWorker * ateapipb.Worker
142-
143169 // Check if we already have a worker assigned from a previous failed attempt.
144- // If that worker's pool is no longer eligible (e.g. the actor's
145- // worker_selector was updated after the failed attempt), release it back
146- // to the free pool instead of leaving it claimed forever — nothing else
147- // reclaims a healthy worker whose actor moved on to a different pool.
170+ // This can happen if ateapi crashed after updating worker with actor assignment,
171+ // but has not yet updated the actor.
148172 for _ , worker := range workers {
149173 if worker .Assignment == nil {
150174 continue
@@ -157,20 +181,28 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
157181 return fmt .Errorf ("while checking worker eligibility: %w" , err )
158182 }
159183 if eligible {
160- assignedWorker = worker
184+ state . Worker = worker
161185 break
162186 }
163187 // Workers() returns pointers directly from the cache so we need to clone before
164188 // mutating so that the cache is not corrupted if UpdateWorker fails.
165- release := proto .Clone (worker ).(* ateapipb.Worker )
166- release .Assignment = nil
167- if err := s .store .UpdateWorker (ctx , release , release .Version ); err != nil {
168- return fmt .Errorf ("while releasing stale worker assignment: %w" , err )
169- }
189+ releaseWorker := proto .Clone (worker ).(* ateapipb.Worker )
190+ releaseWorker .Assignment = nil
191+ // The claimed worker is no longer eligible (e.g. the actor's
192+ // worker_selector changed after the failed attempt); release it back
193+ // to the free pool — nothing else reclaims a healthy worker whose
194+ // actor moved on to a different pool. Best effort in the background.
195+ go func (release * ateapipb.Worker ) {
196+ bgCtx , cancel := context .WithTimeout (ctx , 10 * time .Second )
197+ defer cancel ()
198+ if err := s .store .UpdateWorker (bgCtx , release , release .Version ); err != nil {
199+ slog .ErrorContext (bgCtx , "Failed to release stale worker assignment" ,
200+ slog .String ("worker" , release .GetWorkerNamespace ()+ "/" + release .GetWorkerPod ()),
201+ slog .Any ("err" , err ))
202+ }
203+ }(releaseWorker )
170204 }
171-
172- // If not, find a free one using randomized shuffling
173- if assignedWorker == nil {
205+ if state .Worker == nil {
174206 pickedWorker , err := s .findFreeWorker (workers , state .ActorTemplate .Spec .SandboxClass , state .ActorTemplate .Spec .WorkerSelector , state .Actor .GetWorkerSelector (), state .Actor .GetLatestSnapshotInfo ().GetLocal ().GetNodeVmsWithLocalSnapshots ())
175207 if err != nil {
176208 return err
@@ -179,14 +211,14 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
179211 return status .Errorf (codes .FailedPrecondition , "no free workers available" )
180212 }
181213
182- assignedWorker = pickedWorker
214+ state . Worker = pickedWorker
183215 slog .InfoContext (ctx , "Picked worker" , slog .Any ("worker" , pickedWorker .String ()))
184216 }
185217
186218 // Workers() returns pointers directly from the cache so we need to clone before
187219 // mutating so that the cache is not corrupted if UpdateWorker fails.
188- assignedWorker = proto .Clone (assignedWorker ).(* ateapipb.Worker )
189- assignedWorker .Assignment = & ateapipb.Assignment {
220+ state . Worker = proto .Clone (state . Worker ).(* ateapipb.Worker )
221+ state . Worker .Assignment = & ateapipb.Assignment {
190222 ActorTemplate : & ateapipb.KubeNamespacedObjectRef {
191223 Namespace : state .Actor .GetActorTemplateNamespace (),
192224 Name : state .Actor .GetActorTemplateName (),
@@ -197,16 +229,16 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
197229 },
198230 }
199231
200- if err := s .store .UpdateWorker (ctx , assignedWorker , assignedWorker .Version ); err != nil {
232+ if err := s .store .UpdateWorker (ctx , state . Worker , state . Worker .Version ); err != nil {
201233 return err
202234 }
203235
204236 state .Actor .Status = ateapipb .Actor_STATUS_RESUMING
205- state .Actor .AteomPodNamespace = assignedWorker .GetWorkerNamespace ()
206- state .Actor .AteomPodName = assignedWorker .GetWorkerPod ()
207- state .Actor .AteomPodIp = assignedWorker .GetIp ()
208- state .Actor .AteomPodUid = assignedWorker .GetWorkerPodUid ()
209- state .Actor .WorkerPoolName = assignedWorker .GetWorkerPool ()
237+ state .Actor .AteomPodNamespace = state . Worker .GetWorkerNamespace ()
238+ state .Actor .AteomPodName = state . Worker .GetWorkerPod ()
239+ state .Actor .AteomPodIp = state . Worker .GetIp ()
240+ state .Actor .AteomPodUid = state . Worker .GetWorkerPodUid ()
241+ state .Actor .WorkerPoolName = state . Worker .GetWorkerPool ()
210242
211243 updatedActor , err := s .store .UpdateActor (ctx , state .Actor , state .Actor .GetMetadata ().GetVersion ())
212244 if err != nil {
@@ -275,6 +307,40 @@ func (s *CallAteletRestoreStep) CheckPrerequisite(ctx context.Context, input *Re
275307 if state .Actor .GetStatus () != ateapipb .Actor_STATUS_RESUMING {
276308 return status .Errorf (codes .FailedPrecondition , "CallAteletRestoreStep prerequisite not met for Actor: %s (got: %v, want %s)" , input .ActorName , state .Actor .GetStatus (), ateapipb .Actor_STATUS_RESUMING )
277309 }
310+ if state .Worker == nil {
311+ return status .Errorf (codes .FailedPrecondition , "Assigned worker is nil" )
312+ }
313+ // Verify if the worker is still assigned to the same Actor.
314+ assigned := state .Worker .GetAssignment ().GetActor ()
315+ if assigned .GetAtespace () != input .Atespace || assigned .GetName () != input .ActorName {
316+ slog .ErrorContext (ctx , "crashing actor because its assigned worker no longer belongs to it" ,
317+ slog .String ("worker" , state .Worker .GetWorkerPod ()),
318+ slog .Any ("assignment" , state .Worker .GetAssignment ()))
319+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
320+ return fmt .Errorf ("while crashing actor: %w" , cerr )
321+ }
322+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
323+ }
324+ eligible , err := isWorkerEligibleForActor (state .Worker , state .ActorTemplate .Spec .SandboxClass , state .ActorTemplate .Spec .WorkerSelector , state .Actor .GetWorkerSelector ())
325+ if err != nil {
326+ return fmt .Errorf ("while calling isWorkerEligbleForActor :%w" , err )
327+ }
328+ if ! eligible {
329+ slog .ErrorContext (ctx , "crashing actor because previously assigned worker is not eligible anymore" )
330+ release := proto .Clone (state .Worker ).(* ateapipb.Worker )
331+ release .Assignment = nil
332+ // If that worker's pool is no longer eligible (e.g. the actor's
333+ // worker_selector was updated after the failed attempt), release it back
334+ // to the free pool instead of leaving it claimed forever — nothing else
335+ // reclaims a healthy worker whose actor moved on to a different pool.
336+ if err := s .store .UpdateWorker (ctx , release , release .Version ); err != nil {
337+ return fmt .Errorf ("while releasing stale worker assignment: %w" , err )
338+ }
339+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
340+ return fmt .Errorf ("while crashing actor: %w" , cerr )
341+ }
342+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
343+ }
278344 return nil
279345}
280346func (s * CallAteletRestoreStep ) Execute (ctx context.Context , input * ResumeInput , state * ResumeState ) error {
0 commit comments