blob: 9c9e892df75674bd6551ce1f37b299530b12f199 [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.
*/
mod dispatch;
use crate::{
Error,
authorization::Origin,
command::{Mutation, MutationResult, Query, QueryResult},
controller::Controller,
delivery::{Clock, Dispatcher},
view::View,
};
use futures_util::{StreamExt, stream::FuturesUnordered};
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use tokio::{
sync::{Notify, mpsc, oneshot, watch},
time::Instant,
};
use tokio_util::sync::CancellationToken;
#[derive(Clone)]
pub struct Handle {
commands: mpsc::Sender<Command>,
view: watch::Receiver<Arc<View>>,
wake: Arc<Notify>,
}
enum Command {
Mutate(Box<Edit>),
Creation {
operation_id: uuid::Uuid,
reply: oneshot::Sender<Result<Option<String>, Error>>,
},
}
struct Edit {
mutation: Mutation,
expected_revision: Option<u64>,
origin: Origin,
reply: oneshot::Sender<Result<MutationResult, Error>>,
}
impl Handle {
/// Serialized with writes. None only means no committed receipt was observed.
pub async fn creation(&self, operation_id: uuid::Uuid) -> Result<Option<String>, Error> {
let (reply, receive) = oneshot::channel();
self.commands
.try_send(Command::Creation {
operation_id,
reply,
})
.map_err(|error| match error {
mpsc::error::TrySendError::Full(_) => Error::Busy,
mpsc::error::TrySendError::Closed(_) => Error::Closed,
})?;
tokio::time::timeout(Duration::from_secs(10), receive)
.await
.map_err(|_| Error::Unavailable("creation query deadline exceeded".into()))?
.map_err(|_| Error::Closed)?
}
pub fn snapshot(&self) -> Arc<View> {
self.view.borrow().clone()
}
pub fn subscribe(&self) -> watch::Receiver<Arc<View>> {
self.view.clone()
}
pub fn query(&self, query: Query) -> Result<QueryResult, Error> {
if self.commands.is_closed() {
return Err(Error::Closed);
}
self.view.borrow().query(query)
}
/// Called by Host's existing resume/wake notifications.
pub fn wake(&self) {
self.wake.notify_one();
}
pub async fn mutate(
&self,
mutation: Mutation,
origin: Origin,
) -> Result<MutationResult, Error> {
self.enqueue(mutation, origin, None).await
}
pub async fn mutate_if_current(
&self,
mutation: Mutation,
origin: Origin,
expected_revision: u64,
) -> Result<MutationResult, Error> {
self.enqueue(mutation, origin, Some(expected_revision))
.await
}
async fn enqueue(
&self,
mutation: Mutation,
origin: Origin,
expected_revision: Option<u64>,
) -> Result<MutationResult, Error> {
let (reply, receive) = oneshot::channel();
self.commands
.try_send(Command::Mutate(Box::new(Edit {
mutation,
expected_revision,
origin,
reply,
})))
.map_err(|error| match error {
mpsc::error::TrySendError::Full(_) => Error::Busy,
mpsc::error::TrySendError::Closed(_) => Error::Closed,
})?;
// A timeout does not roll back a mutation already accepted by the owner.
tokio::time::timeout(Duration::from_secs(10), receive)
.await
.map_err(|_| Error::OutcomeUnknown)?
.map_err(|_| Error::OutcomeUnknown)?
}
}
impl maka_capabilities::background::BackgroundWork for Handle {
fn wake(&self) {
Handle::wake(self);
}
fn is_pending(&self) -> bool {
let view = self.view.borrow();
!self.commands.is_closed() && (!view.ready || view.pending_work)
}
}
struct Owner {
controller: Controller,
dispatcher: Arc<dyn Dispatcher>,
clock: Arc<dyn Clock>,
updates: watch::Sender<Arc<View>>,
jobs: FuturesUnordered<dispatch::Job>,
active: BTreeMap<String, CancellationToken>,
retries: BTreeMap<String, dispatch::Retry>,
}
/// The returned future belongs to the Fiber and must start only after publication.
/// Dropping it stops new triggers; Host-owned accepted executions are not cancelled.
pub fn start(
controller: Controller,
dispatcher: Arc<dyn Dispatcher>,
clock: Arc<dyn Clock>,
stop: CancellationToken,
) -> (
Handle,
impl Future<Output = Result<(), String>> + Send + 'static,
) {
let (updates, view) = watch::channel(Arc::new(View::default()));
let (commands, mut requests) = mpsc::channel::<Command>(32);
let wake = Arc::new(Notify::new());
let handle = Handle {
commands,
view,
wake: wake.clone(),
};
let mut owner = Owner {
controller,
dispatcher,
clock,
updates,
jobs: FuturesUnordered::new(),
active: BTreeMap::new(),
retries: BTreeMap::new(),
};
let future = async move {
let mut last_tick = owner.clock.now();
let mut reload = true;
let mut recover = true;
loop {
if stop.is_cancelled() {
return Ok(());
}
if reload {
match owner.controller.reload().await {
Ok(()) => {
// A lost settlement acknowledgement can leave a retry in
// memory after its Fire has already committed as complete.
owner.reconcile_attempts();
reload = false;
}
Err(error) => {
owner.failed(error);
tokio::select! {
_ = stop.cancelled() => return Ok(()),
_ = tokio::time::sleep(Duration::from_secs(1)) => continue,
}
}
}
}
let now = owner.clock.now();
// A resumed machine or large forward clock adjustment follows the
// configured misfire policy, not a backlog of timer callbacks.
recover |= now.saturating_sub(last_tick) > 60_000;
last_tick = now;
if recover {
if let Err(error) = owner.controller.recover(now).await {
owner.failed(error);
reload = true;
} else {
recover = false;
}
}
if !reload && let Err(error) = owner.launch(now).await {
owner.failed(error);
reload = true;
}
if !reload {
owner.publish();
}
let delay = if reload {
Duration::from_secs(1)
} else {
owner.delay(now)
};
tokio::select! {
_ = stop.cancelled() => return Ok(()),
Some(completed) = owner.jobs.next(), if !owner.jobs.is_empty() => {
owner.active.remove(&completed.task_id);
if let Err(error) = owner.finish(completed).await {
owner.failed(error);
reload = true;
}
}
command = requests.recv() => {
let Some(command) = command else { return Ok(()); };
let command = match command {
Command::Mutate(command) => *command,
Command::Creation { operation_id, reply } => {
if !reply.is_closed() {
let result = if reload {
Err(Error::Unavailable("scheduler is recovering".into()))
} else {
owner.controller.creation(operation_id).await
};
let _ = reply.send(result);
}
continue;
}
};
if command.reply.is_closed() { continue; }
if reload {
let _ = command.reply.send(Err(Error::Unavailable("scheduler is recovering".into())));
continue;
}
let result = owner.controller.mutate_at_revision(command.mutation, command.origin, command.expected_revision, owner.clock.now(), owner.dispatcher.as_ref()).await;
if matches!(result, Err(Error::Storage(_))) {
owner.failed(result.as_ref().err().unwrap());
reload = true;
} else if result.is_ok() {
owner.reconcile_attempts();
owner.publish();
}
let _ = command.reply.send(result);
}
_ = wake.notified() => { recover = true; }
_ = tokio::time::sleep(delay) => {}
}
}
};
(handle, future)
}
impl Owner {
fn reconcile_attempts(&mut self) {
// Domain state owns withdrawal. Host only observes the attempt's
// cancellation signal, never the task catalog or plugin publication.
for (id, stop) in &self.active {
let active = self.controller.catalog.plans.get(id).is_some_and(|saved| {
saved.plan.task.status == crate::task::Status::Active
&& saved.plan.pending.is_some()
});
if !active {
stop.cancel();
}
}
self.retries.retain(|id, retry| {
self.controller
.catalog
.plans
.get(id)
.and_then(|saved| saved.plan.pending.as_ref())
.is_some_and(|fire| fire.id == retry.fire_id)
});
}
fn publish(&self) {
let previous = self.updates.borrow().clone();
if !previous.ready || previous.revision != self.controller.catalog.revision.unwrap_or(0) {
self.updates
.send_replace(Arc::new(self.controller.view(&previous)));
}
}
fn failed(&self, error: impl ToString) {
// A lost mutation acknowledgement may hide a committed pause. Fence
// unadmitted notifications until durable state has been reloaded.
for stop in self.active.values() {
stop.cancel();
}
let previous = self.updates.borrow().clone();
self.updates.send_replace(Arc::new(View {
revision: previous.revision,
tasks: previous.tasks.clone(),
task_versions: previous.task_versions.clone(),
ready: false,
pending_work: previous.pending_work,
error: Some(error.to_string()),
}));
}
fn delay(&self, now: i64) -> Duration {
// Recheck wall time periodically even without an OS wake notification.
let mut delay = Duration::from_secs(30);
if self.jobs.len() >= 8 {
return delay;
}
for (id, saved) in &self.controller.catalog.plans {
if self.active.contains_key(id) {
continue;
}
if let Some(retry) = self.retries.get(id).filter(|retry| {
saved
.plan
.pending
.as_ref()
.is_some_and(|fire| fire.id == retry.fire_id)
}) {
delay = delay.min(retry.at.saturating_duration_since(Instant::now()));
} else if let Some(at) = saved.plan.task.next_fire_at
&& saved.plan.task.status == crate::task::Status::Active
{
delay = delay.min(Duration::from_millis(at.saturating_sub(now).max(0) as u64));
} else if saved.plan.pending.is_some() {
delay = Duration::ZERO;
}
}
delay
}
}