use std::collections::HashSet;
use std::time::Duration;
use arcbox_connect::sandbox_v1;
use arcbox_connect::v1::{SandboxCleanupTicket, WatchSandboxCleanupRequest};
use buffa::Message;
use tokio::io::AsyncWrite;
use tokio::sync::mpsc;
use super::{SandboxService, convert};
use crate::error::SandboxError;
use crate::rpc::{ErrorResponse, MessageType, write_message};
use crate::sandbox_cleanup_watch::unseen_rescan_tickets;
const EVENT_QUEUE_CAPACITY: usize = 64;
impl SandboxService {
pub(crate) async fn handle_cleanup_events<S>(
&self,
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncWrite + Unpin,
{
WatchSandboxCleanupRequest::decode_from_slice(payload)
.map_err(|error| anyhow::anyhow!("decode cleanup watch: {error}"))?;
let mut lifecycle = self.manager.subscribe_events();
let mut snapshot = match self.pending_cleanup_tickets().await {
Ok(tickets) => tickets,
Err(error) => {
let response = ErrorResponse::new(error.status_code(), error.to_string());
write_message(stream, MessageType::Error, trace_id, &response.encode()).await?;
return Ok(());
}
};
snapshot.sort_by(|left, right| {
left.startup
.cmp(&right.startup)
.then_with(|| left.id.cmp(&right.id))
});
let mut sent = HashSet::<(String, String)>::new();
for ticket in snapshot {
write_cleanup_ticket(stream, trace_id, &mut sent, ticket).await?;
}
let mut rescan = tokio::time::interval(Duration::from_secs(1));
rescan.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
rescan.tick().await;
loop {
tokio::select! {
event = lifecycle.recv() => match event {
Ok(event) if event.is_terminal() => {
if let Some(ticket) = self
.pending_cleanup_ticket(&event.sandbox_id)
.await
.map_err(|error| anyhow::anyhow!(error.to_string()))?
{
write_cleanup_ticket(stream, trace_id, &mut sent, ticket).await?;
}
}
Ok(_) => {}
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!(skipped, "sandbox cleanup receiver lagged; forcing replay");
return Ok(());
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => return Ok(()),
},
_ = rescan.tick() => {
let tickets = self
.pending_cleanup_tickets()
.await
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
for ticket in unseen_rescan_tickets(&mut sent, tickets) {
write_cleanup_ticket(stream, trace_id, &mut sent, ticket).await?;
}
}
}
}
}
pub async fn handle_events<S>(
&self,
stream: &mut S,
trace_id: &str,
payload: &[u8],
) -> anyhow::Result<()>
where
S: AsyncWrite + Unpin,
{
let mut rx = match self.subscribe_events(payload) {
Ok(r) => r,
Err(e) => {
let err = ErrorResponse::new(e.status_code(), e.to_string());
write_message(stream, MessageType::Error, trace_id, &err.encode()).await?;
return Ok(());
}
};
while let Some(encoded) = rx.recv().await {
write_message(stream, MessageType::SandboxEvent, trace_id, &encoded).await?;
}
Ok(())
}
pub fn subscribe_events(
&self,
payload: &[u8],
) -> Result<mpsc::Receiver<Vec<u8>>, SandboxError> {
let req = sandbox_v1::SandboxEventsRequest::decode_from_slice(payload)
.map_err(|e| SandboxError::Decode(e.to_string()))?;
let filter_id = req.sandbox_id.clone();
let filter_kind = req.kind.as_known().unwrap_or_default();
let mut bcast_rx = self.manager.subscribe_events();
let (tx, out_rx) = mpsc::channel::<Vec<u8>>(EVENT_QUEUE_CAPACITY);
tokio::spawn(async move {
loop {
match bcast_rx.recv().await {
Ok(event) => {
if !filter_id.is_empty() && event.sandbox_id != filter_id {
continue;
}
if filter_kind != sandbox_v1::SandboxEventKind::Unspecified
&& convert::event_kind(&event.action) != filter_kind
{
continue;
}
let msg = convert::vm_event_to_proto(event);
if tx.send(msg.encode_to_vec()).await.is_err() {
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(skipped = n, "sandbox events receiver lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
}
}
});
Ok(out_rx)
}
}
async fn write_cleanup_ticket<S>(
stream: &mut S,
trace_id: &str,
sent: &mut HashSet<(String, String)>,
ticket: SandboxCleanupTicket,
) -> anyhow::Result<()>
where
S: AsyncWrite + Unpin,
{
if sent.insert((ticket.id.clone(), ticket.token.clone())) {
write_message(
stream,
MessageType::SandboxCleanupEvent,
trace_id,
&ticket.encode_to_vec(),
)
.await?;
}
Ok(())
}