use super::config::{OUTCOME_POLL_MS, RunState};
use super::run::settle_control;
use crate::session::accept::AcceptPath;
use crate::session::dispatch::{self, ClientLink};
use anyhow::{Result, anyhow};
use onlyne_proto::{AckArgs, ClientOp, Delivery, QueryRolesArgs, RoleInfo};
use onlyne_session::SessionOutcome;
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
use tokio::time::sleep;
pub(super) async fn outcome_loop(state: RunState) -> Result<()> {
let Some(feed) = state.dispatch.outcome_feed() else {
return std::future::pending::<Result<()>>().await;
};
loop {
while let Some(outcome) = feed.try_recv() {
settle_session_outcome(&state, outcome).await?;
}
sleep(Duration::from_millis(OUTCOME_POLL_MS)).await;
}
}
pub(super) async fn settle_session_outcome(
state: &RunState,
outcome: SessionOutcome,
) -> Result<()> {
let SessionOutcome {
task_id,
outcome,
head,
head_kind,
note,
refusals,
handoffs,
} = outcome;
let terminal = dispatch::task_outcome_of(outcome).ok_or_else(|| {
anyhow!("self-driven backend reported a non-terminal outcome for task {task_id}")
})?;
if let Some(reason) = refusals.as_deref() {
onlyne_session::record_fault(&state.store, &task_id, "permission", "acp", reason)?;
}
if outcome == onlyne_session::TaskState::Failed
&& let Some(reason) = note.as_deref()
{
onlyne_session::record_fault(&state.store, &task_id, "acp", "acp", reason)?;
}
dispatch::on_out(
&state.dispatch,
&task_id,
terminal,
head,
head_kind.as_deref(),
&handoffs,
dispatch::SettleAuthority::ClientOwned,
)
.await
}
pub(super) async fn accept_delivery(state: &RunState, delivery: &Delivery) {
if delivery.envelope.kind == onlyne_proto::MsgKind::Control {
settle_control(state, delivery).await;
return;
}
if delivery.envelope.kind == onlyne_proto::MsgKind::Task
&& let Some(task_id) = delivery.envelope.task_id()
&& state.dispatch.task_completed_here(task_id)
{
tracing::warn!(
msg_id = %delivery.msg_id,
task = %task_id,
"redelivery of a finished task settled without running it"
);
state.dispatch.push_settled(AckArgs {
msg_id: delivery.msg_id.clone(),
op_id: None,
accepted: true,
reason: Some("task already completed by this role".to_string()),
});
return;
}
if !state.dispatch.has_capacity() {
tracing::debug!(msg_id = %delivery.msg_id, "delivery waits for a free session");
return;
}
if delivery.envelope.kind == onlyne_proto::MsgKind::Completion {
state.dispatch.push_settled(AckArgs {
msg_id: delivery.msg_id.clone(),
op_id: None,
accepted: true,
reason: None,
});
return;
}
if delivery.envelope.kind == onlyne_proto::MsgKind::Note {
let injected = state.dispatch.inject_note(&delivery.envelope).await;
state.dispatch.push_settled(AckArgs {
msg_id: delivery.msg_id.clone(),
op_id: None,
accepted: injected,
reason: (!injected).then(|| "note has no live session to wake".to_string()),
});
return;
}
let accept_new = state.accept_new.load(Ordering::SeqCst);
let path = AcceptPath::new(state.dispatch.clone(), state.dispatch.role_prose());
match path.accept_new(delivery, accept_new) {
Ok(Some(session)) => {
if let Some(task_id) = delivery.envelope.task_id() {
state.dispatch.attach_msg_id(task_id, &delivery.msg_id);
}
if let Err(error) = state.dispatch.hand_staged(&session.task_id).await {
tracing::warn!(error = %error, task = %session.task_id, "staged hand-off refused");
}
}
Ok(None) => tracing::debug!(
msg_id = %delivery.msg_id,
"the link is not taking work; the delivery stays in flight"
),
Err(error) => {
tracing::warn!(error = %error, msg_id = %delivery.msg_id, "delivery refused");
state.dispatch.push_settled(AckArgs {
msg_id: delivery.msg_id.clone(),
op_id: None,
accepted: false,
reason: Some(error.to_string()),
});
}
}
}
pub(super) async fn scan_stalls(state: &RunState) {
if state.stall_report_secs == 0 {
return;
}
let due = state
.dispatch
.stall_due(Instant::now(), state.stall_report_secs);
for task_id in due {
let Some(report) = state.dispatch.stall_report(&task_id) else {
continue;
};
match dispatch::send_frame(&state.dispatch, ClientOp::Report(report)).await {
Ok(()) => state.dispatch.mark_stalled(&task_id),
Err(error) => {
tracing::warn!(error = %error, task = %task_id, "stall fault was not sent")
}
}
}
}
pub(super) async fn scan_reclaimed_resources(state: &RunState) {
for session_id in state.dispatch.reclaim_exited_resources() {
if let Err(error) = dispatch::sync_session(&state.dispatch, &session_id).await {
tracing::warn!(
session = %session_id,
error = %error,
"a reclaimed session's exit was not published"
);
}
}
}
pub(super) async fn scan_reconnect_grace(state: &RunState) {
if state.reconnect_grace_secs == 0 {
return;
}
let retired = state
.dispatch
.retire_dropped_ghosts(Instant::now(), state.reconnect_grace_secs);
if retired.is_empty() {
return;
}
for retired in retired {
tracing::info!(
session = %retired.session_id,
arm = retired.arm.word(),
quiet_secs = retired.quiet_secs,
away_secs = retired.away_secs,
silence_window_secs =
dispatch::HEARTBEAT_INTERVAL.as_secs() * dispatch::HEARTBEAT_SILENCE_MARGIN as u64,
grace_secs = state.reconnect_grace_secs,
"session retired past its window"
);
if let Err(error) = dispatch::sync_session(&state.dispatch, &retired.session_id).await {
tracing::warn!(
session = %retired.session_id,
error = %error,
"a retired session's exit was not published"
);
}
}
}
pub(super) async fn scan_control_settles(state: &RunState) {
let now = Instant::now();
for note in state.dispatch.control_settles_due(now) {
if !state.dispatch.settle_unanswered_control(¬e) {
continue;
}
tracing::info!(
task = %note.task_id,
outcome = ?note.word.outcome(),
waited_secs = now.saturating_duration_since(note.noted_at).as_secs(),
"a task was settled on an operator's word no plugin answered"
);
if let Err(error) = dispatch::sync_session(&state.dispatch, ¬e.task_id).await {
tracing::warn!(
task = %note.task_id,
error = %error,
"a settled task's exit was not published"
);
}
}
}
pub(super) async fn refresh_role_slice(link: &ClientLink, state: &RunState) -> Result<()> {
let role = state.dispatch.role();
let reply = link
.request(ClientOp::QueryRoles(QueryRolesArgs { role: Some(role) }))
.await?;
if !reply.ok {
tracing::warn!(error = ?reply.error, "role slice refresh query refused");
return Ok(());
}
let rows: Vec<RoleInfo> = reply
.data
.as_ref()
.and_then(|value| value.get("roles"))
.cloned()
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
if let Some(info) = rows.first() {
apply_role_info(state, info);
}
Ok(())
}
pub(super) fn apply_role_info(state: &RunState, info: &RoleInfo) -> Vec<&'static str> {
let current = state.dispatch.role_slice();
let next = crate::session::slice::RoleSlice::from_role_info(info, ¤t);
let Some((applied, fields)) = crate::session::slice::apply_if_changed(¤t, next) else {
return Vec::new();
};
state.dispatch.reconfigure(applied);
fields
}
pub fn accept_path(state: &RunState) -> Result<AcceptPath> {
Ok(AcceptPath::new(
state.dispatch.clone(),
state.dispatch.role_prose(),
))
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod scan_tests;