use std::collections::HashMap;
use rivet_envoy_protocol as protocol;
use crate::actor::create_actor;
use crate::connection::ws_send;
use crate::envoy::EnvoyContext;
use crate::stringify::stringify_command_wrapper;
pub const ACK_COMMANDS_INTERVAL_MS: u64 = 5 * 60 * 1000;
pub async fn handle_commands(ctx: &mut EnvoyContext, commands: Vec<protocol::CommandWrapper>) {
tracing::info!(command_count = commands.len(), "received commands");
for command_wrapper in &commands {
tracing::info!(
command = %stringify_command_wrapper(command_wrapper),
"received command"
);
}
let stopped_actors: Vec<(String, u32)> = commands
.iter()
.filter(|c| matches!(c.inner, protocol::Command::CommandStopActor(_)))
.map(|c| (c.checkpoint.actor_id.clone(), c.checkpoint.generation))
.collect();
for command_wrapper in commands {
let checkpoint = command_wrapper.checkpoint;
let dedup_key = (checkpoint.actor_id.clone(), checkpoint.generation);
if let Some(&last_idx) = ctx.processed_command_idx.get(&dedup_key) {
if checkpoint.index <= last_idx {
tracing::debug!(
actor_id = %checkpoint.actor_id,
generation = checkpoint.generation,
index = checkpoint.index,
last_idx,
"skipping replayed command"
);
continue;
}
}
ctx.processed_command_idx
.insert(dedup_key, checkpoint.index);
match command_wrapper.inner {
protocol::Command::CommandStartActor(val) => {
let actor_name = val.config.name.clone();
let (handle, active_http_request_count) = create_actor(
ctx.shared.clone(),
checkpoint.actor_id.clone(),
checkpoint.generation,
val.config,
val.hibernating_requests,
val.preloaded_kv,
);
ctx.insert_actor(
checkpoint.actor_id.clone(),
checkpoint.generation,
handle,
active_http_request_count,
actor_name,
checkpoint.index,
);
}
protocol::Command::CommandStopActor(val) => {
let entry = ctx.get_actor_entry_mut(&checkpoint.actor_id, checkpoint.generation);
if let Some(entry) = entry {
entry.received_stop = true;
entry.last_command_idx = checkpoint.index;
let _ = entry.handle.send(crate::actor::ToActor::Stop {
command_idx: checkpoint.index,
reason: val.reason,
});
} else {
tracing::warn!(
actor_id = %checkpoint.actor_id,
generation = checkpoint.generation,
"received stop actor command for unknown actor"
);
}
}
}
}
if !stopped_actors.is_empty() {
send_stop_command_acks(ctx, &stopped_actors).await;
}
}
async fn send_stop_command_acks(ctx: &EnvoyContext, actors: &[(String, u32)]) {
let mut highest: HashMap<(String, u32), i64> = HashMap::new();
for key in actors {
if let Some(&index) = ctx.processed_command_idx.get(key) {
highest.insert(key.clone(), index);
}
}
if highest.is_empty() {
return;
}
send_ack_checkpoints(ctx, checkpoints_from(highest)).await;
}
pub async fn send_command_ack(ctx: &mut EnvoyContext) {
let mut highest: HashMap<(String, u32), i64> = HashMap::new();
for (actor_id, generations) in &ctx.actors {
for (generation, entry) in generations {
if entry.last_command_idx >= 0 {
highest.insert((actor_id.clone(), *generation), entry.last_command_idx);
}
}
}
for ((actor_id, generation), &index) in &ctx.processed_command_idx {
highest
.entry((actor_id.clone(), *generation))
.and_modify(|existing| *existing = (*existing).max(index))
.or_insert(index);
}
if highest.is_empty() {
return;
}
let last_command_checkpoints = checkpoints_from(highest);
let send_failed = send_ack_checkpoints(ctx, last_command_checkpoints.clone()).await;
if send_failed {
return;
}
for cp in &last_command_checkpoints {
ctx.processed_command_idx
.remove(&(cp.actor_id.clone(), cp.generation));
}
}
fn checkpoints_from(highest: HashMap<(String, u32), i64>) -> Vec<protocol::ActorCheckpoint> {
highest
.into_iter()
.map(
|((actor_id, generation), index)| protocol::ActorCheckpoint {
actor_id,
generation,
index,
},
)
.collect()
}
async fn send_ack_checkpoints(
ctx: &EnvoyContext,
last_command_checkpoints: Vec<protocol::ActorCheckpoint>,
) -> bool {
ws_send(
&ctx.shared,
protocol::ToRivet::ToRivetAckCommands(protocol::ToRivetAckCommands {
last_command_checkpoints,
}),
)
.await
}