blob: b06377aa952a279791ee1c8368e6b5181cb8a9c8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{
Error,
coordinator::Coordinator,
decision::Decision,
schedule::{Source, Update},
view::Snapshot,
};
use maka_agent_facts::language::Language;
use std::{
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::sync::{mpsc, oneshot, watch};
#[derive(Clone)]
pub struct Handle {
commands: mpsc::Sender<Command>,
view: watch::Receiver<Arc<View>>,
}
pub struct View {
pub swarm: Option<crate::swarm::Snapshot>,
pub change_key: String,
pub snapshot: Snapshot,
pub error: Option<String>,
pub initialized: bool,
pub pending_work: bool,
}
struct Command {
update: Box<Update>,
reply: oneshot::Sender<Result<u64, Error>>,
}
impl Handle {
pub fn snapshot(&self) -> Arc<View> {
self.view.borrow().clone()
}
pub fn subscribe(&self) -> watch::Receiver<Arc<View>> {
self.view.clone()
}
pub async fn update(&self, decision: Decision, source: Source) -> Result<u64, Error> {
let snapshot = self.snapshot();
if source.invocation.session_id != snapshot.snapshot.root_session_id {
return Err(Error::Invalid(
"tool source belongs to another graph root".into(),
));
}
let update = decision.update(snapshot.snapshot.graph_id.clone(), source)?;
let (reply, receive) = oneshot::channel();
self.commands
.send(Command {
update: Box::new(update),
reply,
})
.await
.map_err(|_| Error::Closed)?;
receive.await.map_err(|_| {
Error::OutcomeUnknown("accepted graph decision owner disappeared".into())
})?
}
}
pub fn start(
mut coordinator: Coordinator,
language: impl Fn() -> Language + Send + 'static,
) -> Result<
(
Handle,
impl Future<Output = Result<(), String>> + Send + 'static,
),
Error,
> {
let mut changed = coordinator.changes()?;
let (updates, view) = watch::channel(Arc::new(View {
swarm: coordinator.swarm_snapshot(),
change_key: coordinator.change_key(),
snapshot: coordinator.snapshot(),
error: None,
initialized: false,
pending_work: true,
}));
let (commands, mut requests) = mpsc::channel::<Command>(32);
let handle = Handle { commands, view };
let owner = async move {
let mut retry = Duration::from_millis(250);
loop {
{
// A fresh control request may preempt reads or a lost Host reply;
// accepted work remains keyed by its already-persisted intent.
enum Next {
Request(Option<Command>),
Reconciled(Result<(), Error>),
}
let next = tokio::select! {
biased;
command = requests.recv() => Next::Request(command),
result = coordinator.reconcile(language()) => Next::Reconciled(result),
};
match next {
Next::Request(None) => return Ok(()),
Next::Request(Some(command)) => {
let result = coordinator.decide(*command.update, now()?).await;
let _ = command.reply.send(result);
continue;
}
Next::Reconciled(result) => {
// Session preparation may hold Host admission while it
// waits for this read model. Publish before a supervisor
// wake tries to acquire that same admission.
updates.send_replace(Arc::new(View {
swarm: coordinator.swarm_snapshot(),
change_key: coordinator.change_key(),
snapshot: coordinator.snapshot(),
error: result.as_ref().err().map(ToString::to_string),
initialized: true,
pending_work: coordinator.has_background_work(),
}));
let result = match result {
Ok(()) => coordinator.wake_supervisor(language()).await,
Err(error) => Err(error),
};
let error = result.err().map(|error| error.to_string());
let retrying = error.is_some()
|| !coordinator.failures.is_empty()
|| coordinator.catching_up();
updates.send_replace(Arc::new(View {
swarm: coordinator.swarm_snapshot(),
change_key: coordinator.change_key(),
snapshot: coordinator.snapshot(),
error,
initialized: true,
pending_work: coordinator.has_background_work(),
}));
if retrying {
retry = (retry * 2).min(Duration::from_secs(30));
} else {
retry = Duration::from_millis(250);
}
}
}
}
tokio::select! {
biased;
command = requests.recv() => match command {
None => return Ok(()),
Some(command) => {
let result = coordinator.decide(*command.update, now()?).await;
let _ = command.reply.send(result);
}
},
result = changed.changed() => {
result.map_err(|_| "Host execution observer closed".to_string())?;
// Coalesce token commits without placing one task per event.
tokio::time::sleep(Duration::from_millis(100)).await;
changed.borrow_and_update();
},
_ = tokio::time::sleep(retry), if coordinator.catching_up()
|| updates.borrow().error.is_some() || !coordinator.failures.is_empty() => {}
}
}
};
Ok((handle, owner))
}
fn now() -> Result<u64, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| error.to_string())?
.as_millis()
.try_into()
.map_err(|_| "system clock overflow".into())
}