@@ -14,6 +14,7 @@ use crate::manifest::VmManifest;
1414use super :: { CachedVMActor , SchedulerActor , VmLifecycle , VmPlacement } ;
1515
1616const UNRESOLVED_VM_CACHE_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
17+ const UNCONFIRMED_RUNNING_PLACEMENT_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
1718
1819impl SchedulerActor {
1920 /// Releases excess allocation once an entry list no longer represents migration.
@@ -51,11 +52,11 @@ impl SchedulerActor {
5152 /// Expires pending placements that were never confirmed by agent status.
5253 ///
5354 /// Pending entries expire after 30 seconds. The five-second discovery loop
54- /// triggers this maintenance, so a slow-to-report create can be forgotten.
55- /// When the expired placement was the last placement for a VM, all correlated
56- /// manifest, placement, and actor-cache state is removed .
55+ /// triggers this maintenance. Expiry removes only the unconfirmed placement:
56+ /// the manifest remains scheduler intent, allowing periodic reconciliation to
57+ /// dispatch a replacement create request .
5758 pub ( super ) fn cleanup_unresolved_vm_cache (
58- manifests : & mut AHashMap < Ulid , VmManifest > ,
59+ _manifests : & mut AHashMap < Ulid , VmManifest > ,
5960 placements : & mut AHashMap < Ulid , Vec < VmPlacement > > ,
6061 data_cache : & mut AHashMap < Ulid , Vec < CachedVMActor > > ,
6162 ) {
@@ -73,8 +74,16 @@ impl SchedulerActor {
7374 . collect ( ) ;
7475
7576 for vmid in empty_vmids {
76- Self :: remove_vm_state ( vmid, manifests, placements, data_cache) ;
77+ if data_cache
78+ . get ( & vmid)
79+ . is_some_and ( |entries| entries. iter ( ) . all ( |entry| entry. actor_ref . is_none ( ) ) )
80+ {
81+ data_cache. remove ( & vmid) ;
82+ }
7783 }
84+
85+ // Keep manifests and empty placement entries: they represent desired VM
86+ // state and are consumed by `ReconcileVmPlacements`.
7887 }
7988
8089 /// Returns every VM that could be on an agent, deduplicating each source in
@@ -153,27 +162,15 @@ impl SchedulerActor {
153162 }
154163 }
155164
156- /// Removes placements assigned to a departed agent and drops VM state only
157- /// when no placement remains.
158- // TODO: Preserve VM intent and enqueue replacement placement or recreation
159- // when an agent disappears instead of dropping the last VM state.
165+ /// Removes placements assigned to a departed agent while retaining empty
166+ /// placement entries as VM intent for periodic reconciliation.
160167 pub ( super ) fn remove_agent_placements (
161168 agent_id : ActorId ,
162- manifests : & mut AHashMap < Ulid , VmManifest > ,
163169 placements : & mut AHashMap < Ulid , Vec < VmPlacement > > ,
164- data_cache : & mut AHashMap < Ulid , Vec < CachedVMActor > > ,
165170 ) {
166- let empty_vmids: Vec < _ > = placements
167- . iter_mut ( )
168- . filter_map ( |( vmid, entries) | {
169- entries. retain ( |entry| entry. agent_id != agent_id) ;
170- Self :: shrink_non_migrating_entries ( entries) ;
171- entries. is_empty ( ) . then_some ( * vmid)
172- } )
173- . collect ( ) ;
174-
175- for vmid in empty_vmids {
176- Self :: remove_vm_state ( vmid, manifests, placements, data_cache) ;
171+ for entries in placements. values_mut ( ) {
172+ entries. retain ( |entry| entry. agent_id != agent_id) ;
173+ Self :: shrink_non_migrating_entries ( entries) ;
177174 }
178175 }
179176
@@ -209,16 +206,16 @@ impl SchedulerActor {
209206 self . agent_data_cache . remove ( & actor_id) ;
210207 self . agent_vm_index . remove ( & actor_id) ;
211208 self . invalidate_pending_resources ( ) ;
212- Self :: remove_agent_placements (
213- actor_id,
214- & mut self . vm_manifests ,
215- & mut self . vm_placements ,
216- & mut self . vm_data_cache ,
217- ) ;
209+ Self :: remove_agent_placements ( actor_id, & mut self . vm_placements ) ;
218210 }
219211
220- /// Aborts VM polling and removes actor state, retaining a VM only when
221- /// another discovered actor or unresolved placement can still represent it.
212+ /// Aborts VM polling and makes an unrepresented VM placement recoverable.
213+ ///
214+ /// A VM actor is not associated with a particular migration entry. Therefore
215+ /// placements are cleared only when no discovered actor remains for that VM;
216+ /// pending destination placements are retained to avoid racing an in-flight
217+ /// migration. Empty placement entries retain the manifest's desired intent
218+ /// for periodic reconciliation.
222219 pub ( super ) fn cleanup_vm_actor ( & mut self , actor_id : ActorId ) {
223220 if let Some ( keepalive_task) = self . vm_keepalive_tasks . remove ( & actor_id) {
224221 trace ! ( ?actor_id, "Aborting VM keepalive task" ) ;
@@ -227,29 +224,32 @@ impl SchedulerActor {
227224 let vmid = self . vm_actorid_ulid_map . remove ( & actor_id) ;
228225 self . invalidate_pending_resources ( ) ;
229226 Self :: remove_vm_actor ( actor_id, & mut self . vm_data_cache ) ;
230- if let Some ( vmid) = vmid
231- && self
232- . vm_data_cache
233- . get ( & vmid)
234- . is_none_or ( |entries| entries. iter ( ) . all ( |entry| entry. actor_ref . is_none ( ) ) )
235- {
236- Self :: remove_vm_state (
237- vmid,
238- & mut self . vm_manifests ,
239- & mut self . vm_placements ,
240- & mut self . vm_data_cache ,
241- ) ;
227+
228+ let Some ( vmid) = vmid else {
229+ return ;
230+ } ;
231+ let has_discovered_actor = self
232+ . vm_data_cache
233+ . get ( & vmid)
234+ . is_some_and ( |entries| entries. iter ( ) . any ( |entry| entry. actor_ref . is_some ( ) ) ) ;
235+ if !has_discovered_actor {
236+ if let Some ( entries) = self . vm_placements . get_mut ( & vmid) {
237+ entries. retain ( |entry| entry. lifecycle == VmLifecycle :: Pending ) ;
238+ Self :: shrink_non_migrating_entries ( entries) ;
239+ }
240+ self . invalidate_pending_resources ( ) ;
242241 }
243242 }
244243
245- /// Incorporates additions from a status delta into placement observations.
244+ /// Incorporates additions and removals from a status delta into placement observations.
246245 ///
247- /// Removals deliberately do not delete desired placements: they may be
248- /// transient observations and reconciliation must be able to recreate the VM.
246+ /// A removal is authoritative for a confirmed (`Running`) placement on this
247+ /// agent. Pending placements are retained through their timeout because a
248+ /// create can race the next status delta.
249249 pub ( super ) fn reconcile_agent_delta (
250250 agent_id : ActorId ,
251251 added : & [ Ulid ] ,
252- _removed : & [ Ulid ] ,
252+ removed : & [ Ulid ] ,
253253 manifests : & AHashMap < Ulid , VmManifest > ,
254254 placements : & mut AHashMap < Ulid , Vec < VmPlacement > > ,
255255 ) {
@@ -272,17 +272,22 @@ impl SchedulerActor {
272272 }
273273 }
274274
275- // A removal is an observation about the agent, not a change to the
276- // scheduler's desired state. Keep the placement so reconciliation can
277- // schedule the VM again. The full status path performs the same
278- // distinction for snapshots.
275+ for vmid in removed {
276+ if let Some ( entries) = placements. get_mut ( vmid) {
277+ entries. retain ( |entry| {
278+ entry. agent_id != agent_id || entry. lifecycle == VmLifecycle :: Pending
279+ } ) ;
280+ Self :: shrink_non_migrating_entries ( entries) ;
281+ }
282+ }
279283 }
280284
281285 /// Reconciles scheduler placement observations with a complete agent snapshot.
282286 ///
283287 /// Known, reported VMs gain or refresh `Running` placements. Unknown VMs are
284- /// ignored because the scheduler has no retained intent for them. Absent VMs
285- /// leave existing desired placements intact so future reconciliation can act.
288+ /// ignored because the scheduler has no retained intent for them. An absent
289+ /// running placement expires after its confirmation timeout; an absent pending
290+ /// placement stays reserved through its pending timeout.
286291 pub ( super ) fn reconcile_agent_placements (
287292 agent_id : ActorId ,
288293 status : & AgentStatus ,
@@ -313,17 +318,27 @@ impl SchedulerActor {
313318 let empty_vmids: Vec < _ > = placements
314319 . iter_mut ( )
315320 . filter_map ( |( vmid, entries) | {
316- // TODO: Expire or repair `Running` placements that remain absent
317- // from repeated full snapshots; they are retained indefinitely now.
318- for entry in entries
319- . iter_mut ( )
320- . filter ( |entry| entry. agent_id == agent_id)
321- {
322- if observed. contains ( vmid) {
323- entry. lifecycle = VmLifecycle :: Running ;
324- entry. last_confirmed_at = Some ( now) ;
321+ entries. retain_mut ( |entry| {
322+ if entry. agent_id != agent_id || observed. contains ( vmid) {
323+ if entry. agent_id == agent_id {
324+ entry. lifecycle = VmLifecycle :: Running ;
325+ entry. last_confirmed_at = Some ( now) ;
326+ }
327+ return true ;
325328 }
326- }
329+
330+ match entry. lifecycle {
331+ VmLifecycle :: Pending => {
332+ now. duration_since ( entry. created_at ) < UNRESOLVED_VM_CACHE_TIMEOUT
333+ }
334+ VmLifecycle :: Running => {
335+ entry. last_confirmed_at . is_some_and ( |last_confirmed_at| {
336+ now. duration_since ( last_confirmed_at)
337+ < UNCONFIRMED_RUNNING_PLACEMENT_TIMEOUT
338+ } )
339+ }
340+ }
341+ } ) ;
327342 Self :: shrink_non_migrating_entries ( entries) ;
328343 entries. is_empty ( ) . then_some ( * vmid)
329344 } )
0 commit comments