@@ -48,11 +48,13 @@ 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
5455type LoadActorForResumeStep struct {
5556 store store.Interface
57+ workerCache workercache.Cache
5658 actorTemplateLister listersv1alpha1.ActorTemplateLister
5759}
5860
@@ -80,6 +82,36 @@ func (s *LoadActorForResumeStep) Execute(ctx context.Context, input *ResumeInput
8082 }
8183 state .ActorTemplate = actorTemplate
8284
85+ // If the Actor is in Resuming state, it means a previous attempt crashed after AssignWorkerStep.
86+ // We don't need to repeat the AssignWorkerStep, load the Worker now.
87+ if actor .Status == ateapipb .Actor_STATUS_RESUMING {
88+ allPopulated := actor .AteomPodUid != "" && actor .WorkerPoolName != "" && actor .AteomPodName != ""
89+ if ! allPopulated {
90+ slog .ErrorContext (ctx , "expected all of AteomPodUid, WorkerPoolName and AteomPodName to be populated, found" ,
91+ slog .String ("AteomPodUid" , actor .AteomPodUid ),
92+ slog .String ("WorkerPoolName" , actor .WorkerPoolName ),
93+ slog .String ("AteomPodName" , actor .AteomPodName ))
94+
95+ // Crash the actor if its worker assignment is corrupted. We should never be in this state.
96+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
97+ return cerr
98+ }
99+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
100+ }
101+
102+ wk , err := s .store .GetWorker (ctx , actor .AteomPodNamespace , actor .WorkerPoolName , actor .AteomPodName )
103+ if err != nil {
104+ // Crash the actor if it was assigned to a deleted pod.
105+ if errors .Is (err , ErrWorkerPodNotFound ) {
106+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
107+ return cerr
108+ }
109+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
110+ }
111+ return fmt .Errorf ("failed to get already assigned worker for actor %w" , err )
112+ }
113+ state .Worker = wk
114+ }
83115 return nil
84116}
85117
@@ -116,19 +148,16 @@ type AssignWorkerStep struct {
116148func (s * AssignWorkerStep ) Name () string { return "AssignWorker" }
117149
118150func (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
151+ // RESUMING means a previous attempt already assigned a worker (loaded by
152+ // LoadActorForResumeStep); RUNNING is past this step entirely.
153+ return state .Actor .GetStatus () == ateapipb .Actor_STATUS_RESUMING || state .Actor .GetStatus () == ateapipb .Actor_STATUS_RUNNING , nil
123154}
124155func (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.
127156 switch state .Actor .GetStatus () {
128- case ateapipb .Actor_STATUS_SUSPENDED , ateapipb .Actor_STATUS_PAUSED , ateapipb . Actor_STATUS_RESUMING :
157+ case ateapipb .Actor_STATUS_SUSPENDED , ateapipb .Actor_STATUS_PAUSED :
129158 return nil
130159 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 )
160+ 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 )
132161 }
133162}
134163
@@ -138,13 +167,9 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
138167 return fmt .Errorf ("while listing workers: %w" , err )
139168 }
140169
141- var assignedWorker * ateapipb.Worker
142-
143170 // 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.
171+ // This can happen if ateapi crashed after updating worker with actor assignment,
172+ // but has not yet updated the actor.
148173 for _ , worker := range workers {
149174 if worker .Assignment == nil {
150175 continue
@@ -157,20 +182,28 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
157182 return fmt .Errorf ("while checking worker eligibility: %w" , err )
158183 }
159184 if eligible {
160- assignedWorker = worker
185+ state . Worker = worker
161186 break
162187 }
163188 // Workers() returns pointers directly from the cache so we need to clone before
164189 // 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- }
190+ releaseWorker := proto .Clone (worker ).(* ateapipb.Worker )
191+ releaseWorker .Assignment = nil
192+ // The claimed worker is no longer eligible (e.g. the actor's
193+ // worker_selector changed after the failed attempt); release it back
194+ // to the free pool — nothing else reclaims a healthy worker whose
195+ // actor moved on to a different pool. Best effort in the background.
196+ go func (release * ateapipb.Worker ) {
197+ bgCtx , cancel := context .WithTimeout (ctx , 10 * time .Second )
198+ defer cancel ()
199+ if err := s .store .UpdateWorker (bgCtx , release , release .Version ); err != nil {
200+ slog .ErrorContext (bgCtx , "Failed to release stale worker assignment" ,
201+ slog .String ("worker" , release .GetWorkerNamespace ()+ "/" + release .GetWorkerPod ()),
202+ slog .Any ("err" , err ))
203+ }
204+ }(releaseWorker )
170205 }
171-
172- // If not, find a free one using randomized shuffling
173- if assignedWorker == nil {
206+ if state .Worker == nil {
174207 pickedWorker , err := s .findFreeWorker (workers , state .ActorTemplate .Spec .SandboxClass , state .ActorTemplate .Spec .WorkerSelector , state .Actor .GetWorkerSelector (), state .Actor .GetLatestSnapshotInfo ().GetLocal ().GetNodeVmsWithLocalSnapshots ())
175208 if err != nil {
176209 return err
@@ -179,14 +212,14 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
179212 return status .Errorf (codes .FailedPrecondition , "no free workers available" )
180213 }
181214
182- assignedWorker = pickedWorker
215+ state . Worker = pickedWorker
183216 slog .InfoContext (ctx , "Picked worker" , slog .Any ("worker" , pickedWorker .String ()))
184217 }
185218
186219 // Workers() returns pointers directly from the cache so we need to clone before
187220 // mutating so that the cache is not corrupted if UpdateWorker fails.
188- assignedWorker = proto .Clone (assignedWorker ).(* ateapipb.Worker )
189- assignedWorker .Assignment = & ateapipb.Assignment {
221+ state . Worker = proto .Clone (state . Worker ).(* ateapipb.Worker )
222+ state . Worker .Assignment = & ateapipb.Assignment {
190223 ActorTemplate : & ateapipb.KubeNamespacedObjectRef {
191224 Namespace : state .Actor .GetActorTemplateNamespace (),
192225 Name : state .Actor .GetActorTemplateName (),
@@ -197,16 +230,16 @@ func (s *AssignWorkerStep) Execute(ctx context.Context, input *ResumeInput, stat
197230 },
198231 }
199232
200- if err := s .store .UpdateWorker (ctx , assignedWorker , assignedWorker .Version ); err != nil {
233+ if err := s .store .UpdateWorker (ctx , state . Worker , state . Worker .Version ); err != nil {
201234 return err
202235 }
203236
204237 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 ()
238+ state .Actor .AteomPodNamespace = state . Worker .GetWorkerNamespace ()
239+ state .Actor .AteomPodName = state . Worker .GetWorkerPod ()
240+ state .Actor .AteomPodIp = state . Worker .GetIp ()
241+ state .Actor .AteomPodUid = state . Worker .GetWorkerPodUid ()
242+ state .Actor .WorkerPoolName = state . Worker .GetWorkerPool ()
210243
211244 updatedActor , err := s .store .UpdateActor (ctx , state .Actor , state .Actor .GetMetadata ().GetVersion ())
212245 if err != nil {
@@ -275,6 +308,40 @@ func (s *CallAteletRestoreStep) CheckPrerequisite(ctx context.Context, input *Re
275308 if state .Actor .GetStatus () != ateapipb .Actor_STATUS_RESUMING {
276309 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 )
277310 }
311+ if state .Worker == nil {
312+ return status .Errorf (codes .FailedPrecondition , "Assigned worker is nil" )
313+ }
314+ // Verify if the worker is still assigned to the same Actor.
315+ assigned := state .Worker .GetAssignment ().GetActor ()
316+ if assigned .GetAtespace () != input .Atespace || assigned .GetName () != input .ActorName {
317+ slog .ErrorContext (ctx , "crashing actor because its assigned worker no longer belongs to it" ,
318+ slog .String ("worker" , state .Worker .GetWorkerPod ()),
319+ slog .Any ("assignment" , state .Worker .GetAssignment ()))
320+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
321+ return fmt .Errorf ("while crashing actor: %w" , cerr )
322+ }
323+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
324+ }
325+ eligible , err := isWorkerEligibleForActor (state .Worker , state .ActorTemplate .Spec .SandboxClass , state .ActorTemplate .Spec .WorkerSelector , state .Actor .GetWorkerSelector ())
326+ if err != nil {
327+ return fmt .Errorf ("while calling isWorkerEligbleForActor :%w" , err )
328+ }
329+ if ! eligible {
330+ slog .ErrorContext (ctx , "crashing actor because previously assigned worker is not eligible anymore" )
331+ release := proto .Clone (state .Worker ).(* ateapipb.Worker )
332+ release .Assignment = nil
333+ // If that worker's pool is no longer eligible (e.g. the actor's
334+ // worker_selector was updated after the failed attempt), release it back
335+ // to the free pool instead of leaving it claimed forever — nothing else
336+ // reclaims a healthy worker whose actor moved on to a different pool.
337+ if err := s .store .UpdateWorker (ctx , release , release .Version ); err != nil {
338+ return fmt .Errorf ("while releasing stale worker assignment: %w" , err )
339+ }
340+ if cerr := crashActor (ctx , s .store , input .Atespace , input .ActorName ); cerr != nil {
341+ return fmt .Errorf ("while crashing actor: %w" , cerr )
342+ }
343+ return status .Errorf (codes .Aborted , "actor %s crashed" , input .ActorName )
344+ }
278345 return nil
279346}
280347func (s * CallAteletRestoreStep ) Execute (ctx context.Context , input * ResumeInput , state * ResumeState ) error {
0 commit comments