Skip to content

Commit 6af1092

Browse files
committed
fix todos
1 parent fef23a9 commit 6af1092

19 files changed

Lines changed: 552 additions & 180 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ codegen-units = 1
4242
# for any crates using kameo actors on_panic, this would literally break the binary.
4343
# For specific crates/binaries this might be something we do, but I (caleb) doubt it.
4444

45-
strip = "symbols" # TODO: this we should talk about doing this, because it makes debugging harder, so if we have a panic in production, the backtrace will likely not be as useful.
45+
# Keep symbol names in production binaries so panic backtraces remain actionable.
46+
strip = "none"
4647

4748
incremental = false # this is just to get reproducible builds. incremental compiles aren't consistent and can sometimes be broken when combined with optimizations
4849

odorobo/src/actors/agent_actor.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,16 @@ impl Actor for AgentActor {
121121
})
122122
}
123123

124-
// async fn on_panic(state: Self::Args, weak_actor_ref: WeakActorRef<Self>, _panic: &PanicError) {
125-
// panic!("Agent panicked: {:?}", _panic);
126-
// }
127-
//
128124
async fn on_panic(
129125
&mut self,
130126
_actor_ref: WeakActorRef<Self>,
131127
err: PanicError,
132-
) -> Result<std::ops::ControlFlow<ActorStopReason>> {
133-
error!("Agent panicked: {:?}", err);
134-
135-
// todo: if we panic, we should completely regen the self struct from scratch. The assumption should be that memory corruption could have possibly happened becauew
136-
137-
Ok(ControlFlow::Continue(()))
128+
) -> Result<ControlFlow<ActorStopReason>> {
129+
error!(
130+
?err,
131+
"Agent actor panicked; stopping because its state cannot be safely rebuilt here"
132+
);
133+
Ok(ControlFlow::Break(ActorStopReason::Panicked(err)))
138134
}
139135

140136
async fn on_link_died(

odorobo/src/actors/http_actor.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::messages::vm::{
22
AgentListVMs, AgentListVMsReply, CreateVM, CreateVMReply, DeleteVM, DeleteVMReply,
3-
GetConsoleHistory, GetConsoleHistoryReply, ShutdownVM, ShutdownVMReply,
3+
GetConsoleHistory, GetConsoleHistoryReply, GetVMInfo, GetVMInfoReply, ShutdownVM,
4+
ShutdownVMReply,
45
};
56
use kameo::prelude::*;
67
use stable_eyre::{
@@ -102,6 +103,22 @@ impl Message<ShutdownVM> for HTTPActor {
102103
}
103104
}
104105

106+
impl Message<GetVMInfo> for HTTPActor {
107+
type Reply = Result<GetVMInfoReply, Report>;
108+
109+
async fn handle(
110+
&mut self,
111+
msg: GetVMInfo,
112+
_ctx: &mut Context<Self, Self::Reply>,
113+
) -> Self::Reply {
114+
self.scheduler
115+
.ask(msg)
116+
.await
117+
.map_err(|err| eyre!(err.to_string()))
118+
.wrap_err("failed to get VM info via scheduler")
119+
}
120+
}
121+
105122
impl Message<AgentListVMs> for HTTPActor {
106123
type Reply = Result<AgentListVMsReply, Report>;
107124

odorobo/src/actors/scheduler_actor/cache.rs

Lines changed: 77 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::manifest::VmManifest;
1414
use super::{CachedVMActor, SchedulerActor, VmLifecycle, VmPlacement};
1515

1616
const UNRESOLVED_VM_CACHE_TIMEOUT: Duration = Duration::from_secs(30);
17+
const UNCONFIRMED_RUNNING_PLACEMENT_TIMEOUT: Duration = Duration::from_secs(30);
1718

1819
impl 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
})

odorobo/src/actors/scheduler_actor/discovery.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,46 @@ impl Message<ReconcileVmPlacements> for SchedulerActor {
375375
&mut self.vm_data_cache,
376376
);
377377
self.invalidate_pending_resources();
378-
// TODO: Reconcile desired placements absent from agent status by choosing
379-
// a healthy agent and issuing `CreateVM`; this currently only expires
380-
// unconfirmed pending reservations.
378+
379+
let unplaced_vms: Vec<_> = self
380+
.vm_manifests
381+
.iter()
382+
.filter_map(|(vmid, manifest)| {
383+
self.vm_placements
384+
.get(vmid)
385+
.is_none_or(Vec::is_empty)
386+
.then_some((*vmid, manifest.clone()))
387+
})
388+
.collect();
389+
390+
for (vmid, config) in unplaced_vms {
391+
let request = crate::messages::vm::CreateVM { vmid, config };
392+
match self.schedule_agent(&request) {
393+
Ok(agent) => {
394+
self.vm_placements
395+
.entry(vmid)
396+
.or_default()
397+
.push(super::VmPlacement {
398+
agent_id: agent.id(),
399+
lifecycle: super::VmLifecycle::Pending,
400+
created_at: std::time::Instant::now(),
401+
last_confirmed_at: None,
402+
});
403+
self.vm_data_cache
404+
.entry(vmid)
405+
.or_default()
406+
.push(CachedVMActor { actor_ref: None });
407+
self.invalidate_pending_resources();
408+
409+
if let Err(error) = agent.tell(&request).send() {
410+
warn!(?error, %vmid, "failed to recreate unplaced VM");
411+
self.vm_placements.insert(vmid, Vec::new());
412+
self.vm_data_cache.remove(&vmid);
413+
self.invalidate_pending_resources();
414+
}
415+
}
416+
Err(error) => trace!(?error, %vmid, "no eligible agent to recreate VM"),
417+
}
418+
}
381419
}
382420
}

0 commit comments

Comments
 (0)