blob: e843db159139428a680f7635e0487a801d2cdbcd [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 super::Owner;
use crate::{
Error,
delivery::Delivery,
plan::Plan,
task::{Effect, Outcome, Run},
};
use futures_util::future::BoxFuture;
use std::time::Duration;
use tokio::time::Instant;
pub(super) type Job = BoxFuture<'static, Completed>;
pub(super) struct Completed {
pub task_id: String,
fire_id: String,
delivery: Delivery,
}
pub(super) struct Retry {
pub at: Instant,
pub fire_id: String,
delay: Duration,
}
impl Owner {
pub(super) async fn launch(&mut self, now: i64) -> Result<(), Error> {
let ids = self
.controller
.catalog
.plans
.keys()
.cloned()
.collect::<Vec<_>>();
for id in ids {
if self.jobs.len() >= 8 {
break;
}
if self.active.contains_key(&id)
|| self
.retries
.get(&id)
.is_some_and(|retry| retry.at > Instant::now())
{
continue;
}
let original = &self.controller.catalog.plans[&id].plan;
let mut plan = original.clone();
let fire = plan.claim(now)?.cloned();
let Some(mut fire) = fire else {
if &plan != original {
self.save(plan).await?;
}
continue;
};
if matches!(fire.effect, Effect::Notify(_)) {
if fire.delivery_started {
// We cannot distinguish "sent, lost acknowledgement" from
// "crashed before sending"; never silently send it twice.
self.finish(Completed {
task_id: id,
fire_id: fire.id,
delivery: Delivery::Blocked(
"Notification outcome unknown; not replayed".into(),
),
})
.await?;
continue;
}
fire.delivery_started = true;
plan.pending = Some(fire.clone());
}
if &plan != original {
self.save(plan).await?;
}
let dispatcher = self.dispatcher.clone();
let stop = tokio_util::sync::CancellationToken::new();
self.active.insert(id.clone(), stop.clone());
self.jobs.push(Box::pin(async move {
let _cancel_on_drop = stop.clone().drop_guard();
let fire_id = fire.id.clone();
let notification = matches!(fire.effect, Effect::Notify(_));
let delivery = match dispatcher.dispatch(fire, stop).await {
Delivery::Retry(_) if notification => {
Delivery::Blocked("Notification outcome unknown; not replayed".into())
}
result => result,
};
Completed {
task_id: id,
fire_id,
delivery,
}
}));
}
Ok(())
}
pub(super) async fn finish(&mut self, completed: Completed) -> Result<(), Error> {
let Some(saved) = self.controller.catalog.plans.get(&completed.task_id) else {
return Ok(());
};
let mut plan = saved.plan.clone();
let Some(fire) = &plan.pending else {
return Ok(());
};
if fire.id != completed.fire_id {
return Ok(());
}
let at = self
.clock
.now()
.max(fire.scheduled_at)
.max(plan.task.updated_at);
let deferred = matches!(&completed.delivery, Delivery::Deferred(_));
let (outcome, message, session_id, run_id) = match completed.delivery {
Delivery::Accepted { session_id, run_id } => (
Outcome::Ok,
"Execution admitted".into(),
Some(session_id),
Some(run_id),
),
Delivery::Notified => (Outcome::Ok, "Notification sent".into(), None, None),
Delivery::Failed(message) => (Outcome::Failed, message, None, None),
Delivery::Blocked(message) => (Outcome::Blocked, message, None, None),
Delivery::Retry(message) | Delivery::Deferred(message) => {
if deferred {
plan.pending.as_mut().unwrap().delivery_started = false;
if plan.task.status != crate::task::Status::Active
&& plan.waiting_notification()
{
plan.cancel_waiting_notification();
self.retries.remove(&completed.task_id);
return self.save(plan).await;
}
}
let delay = self
.retries
.get(&completed.task_id)
.map_or(Duration::from_millis(250), |retry| {
(retry.delay * 2).min(Duration::from_secs(30))
});
self.retries.insert(
completed.task_id,
Retry {
at: Instant::now() + delay,
fire_id: completed.fire_id,
delay,
},
);
plan.task.last_error = Some(message.to_owned());
plan.task.updated_at = at;
return self.save(plan).await;
}
};
plan.settle(Run {
id: completed.fire_id,
at,
outcome,
message: message.to_owned(),
session_id,
run_id,
})?;
self.save(plan).await?;
self.retries.remove(&completed.task_id);
Ok(())
}
async fn save(&mut self, plan: Plan) -> Result<(), Error> {
self.controller
.repository
.save(&mut self.controller.catalog, plan)
.await
}
}