blob: 37536fa18dce6198a9e84cc4bde4188d347f6d6b [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.
*/
//! Prepare and persist without Work; return to the original owner for admission.
use super::*;
use maka_plugin_core::operation::{OperationOutcome, Receipt};
use std::{panic::AssertUnwindSafe, sync::OnceLock};
pub(super) struct Session {
owner: Arc<dyn NativeMutation>,
reference: Arc<OnceLock<RecoveryRef>>,
caller: Option<Box<CallerConfirmation>>,
pub(super) progress: subscriptions::Flow,
cancelled: bool,
}
enum Phase {
Preparing,
Closing,
Failed,
}
pub(super) struct Preparing<V: ?Sized> {
pub(super) action: Prepared<V>,
resource: Resource,
session: Option<Session>,
// A confirmed caller token remains owned through aborted preparation close.
recorded: Option<RecordedIntent>,
phase: Phase,
outcome: Option<ActionOutcome>,
}
impl<V: ?Sized> Preparing<V> {
pub(super) fn failed(&self) -> bool {
matches!(self.phase, Phase::Failed)
}
}
impl Session {
fn cancel(&mut self) {
self.cancelled = true;
self.progress.stop.cancel();
if std::panic::catch_unwind(AssertUnwindSafe(|| self.owner.cancel())).is_err() {
self.progress.failure.get_or_insert_with(|| {
"native cancellation panicked; read original receipt".into()
});
}
}
fn failure(&self, reason: String) -> ActionOutcome {
match self.reference.get() {
Some(reference) => unavailable(reference.clone(), self.caller.clone(), reason),
None => ActionOutcome::NotAccepted { reason },
}
}
}
impl<V: ?Sized + 'static> Application<V> {
pub(crate) fn cancel_mutation(&mut self, consumer: &Mount, key: &str) {
let queued = std::mem::take(&mut self.actions.queue);
for action in queued {
if !action.subscription && action.consumer == *consumer && action.key == key {
self.reject_action(action, "action cancelled before admission".into());
} else {
self.actions.queue.push_back(action);
}
}
for preparing in self.actions.preparing.values_mut() {
if preparing.action.consumer == *consumer
&& preparing.action.key == key
&& let Some(session) = &mut preparing.session
{
session.cancel();
}
}
for running in self.actions.running.values_mut() {
if running.consumer == *consumer
&& running.key == key
&& let Some(session) = &mut running.mutation
{
session.cancel();
}
}
}
pub(super) fn start_mutation(&mut self, action: Prepared<V>) -> Result<Option<ActionTask>> {
let (Some(source), Some(store)) = (action.source.clone(), self.store.clone()) else {
self.reject_action(action, "mutation requires a persisted UI source".into());
return Ok(None);
};
let Export::Service { implementation, .. } = self.runtime.inspect(&action.capability)?
else {
unreachable!()
};
let implementation = implementation.clone();
let resource = self
.runtime
.own_resource(action.capability.provider(), "mutation preparation")?;
let id = action.id;
let consumer = action.capability.consumer().instance().clone();
let recovery = action.recovery.clone();
let request = ServiceRequest {
source: ServiceSource {
deployment: Some(source.reference().clone()),
consumer: action.capability.consumer().clone(),
mount: action.consumer.clone(),
request: id,
},
method: action.method.clone(),
input: action.input.clone(),
resources: action.resources.as_ref().expect("captured").clone(),
};
let owner = std::panic::catch_unwind(AssertUnwindSafe(|| implementation.mutation(request)));
let owner = match owner {
Ok(Ok(owner)) => owner,
Ok(Err(reason)) => {
self.runtime.complete_resource(&resource, Ok(()))?;
self.reject_action(action, reason);
return Ok(None);
}
Err(_) => {
let reason =
"mutation owner construction panicked; cleanup unconfirmed".to_string();
self.actions.outcomes.push_back(Outcome {
continuations: Vec::new(),
consumer: action.consumer.clone(),
key: action.key.clone(),
outcome: ActionOutcome::NotAccepted {
reason: reason.clone(),
},
request: None,
});
self.actions.preparing.insert(
id,
Preparing {
action,
resource: resource.clone(),
session: None,
recorded: None,
phase: Phase::Failed,
outcome: None,
},
);
let completed = self.runtime.complete_resource(&resource, Err(reason));
self.sync_subscription_demand();
completed?;
return Ok(None);
}
};
let reference = Arc::new(OnceLock::new());
self.actions.preparing.insert(
id,
Preparing {
action,
resource,
session: Some(Session {
owner: owner.clone(),
reference: reference.clone(),
caller: None,
progress: subscriptions::Flow::new(),
cancelled: false,
}),
recorded: None,
phase: Phase::Preparing,
outcome: None,
},
);
Ok(Some(ActionTask {
id,
future: Box::pin(async move {
let prepared = async {
let original = owner.prepare().await?;
original.validate().map_err(|e| e.to_string())?;
maka_plugin_core::json::exact_numbers(
&serde_json::to_value(&original).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
if original.operation != id {
return Err(
"native mutation changed the original operation identity".into()
);
}
// This immutable locator is retained even if SQL commit or
// task delivery becomes unknown. It grants no execution.
reference
.set(original.clone())
.map_err(|_| "original operation already prepared")?;
store
.record_application_intent_from(&source, consumer, original, recovery)
.await
.map_err(|e| e.to_string())
}
.await;
TaskCompletion::Prepared(prepared)
}),
}))
}
/// Complete on the original foreground owner and execute any returned task
/// even after its presentation has disappeared. No error implies cleanup.
pub fn finish_task(
&mut self,
id: Uuid,
completion: TaskCompletion,
) -> Result<Option<ActionTask>> {
let result = (|| match completion {
TaskCompletion::Subscription(completion) => self.finish_subscription(id, completion),
TaskCompletion::Query(completion) => {
if self
.actions
.running
.get(&id)
.is_some_and(|r| r.stream.is_some())
{
return Err(Error::Invalid(
"subscription requires its original stream completion".into(),
));
}
self.finish_query(id, completion)?;
Ok(None)
}
TaskCompletion::Prepared(recorded) => self.finish_preparation(id, recorded),
TaskCompletion::MutationProgress { value, next } => {
self.finish_mutation_progress(id, value, next)
}
TaskCompletion::Mutation(completion) => {
self.finish_mutation(id, completion)?;
Ok(None)
}
TaskCompletion::Closed(cleanup) => {
self.finish_preparation_close(id, cleanup)?;
Ok(None)
}
TaskCompletion::Interrupted(reason) => {
self.interrupt_task(id, reason)?;
Ok(None)
}
})();
self.sync_subscription_demand();
result
}
fn finish_preparation(
&mut self,
id: Uuid,
recorded: std::result::Result<RecordedIntent, String>,
) -> Result<Option<ActionTask>> {
let preparing = self
.actions
.preparing
.get(&id)
.ok_or_else(|| Error::Invalid("unknown preparation".into()))?;
if !matches!(preparing.phase, Phase::Preparing) {
return Err(Error::Invalid("preparation already completed".into()));
}
let session = preparing.session.as_ref().expect("constructed owner");
let gate = recorded.and_then(|recorded| {
if session.reference.get() != Some(&recorded.intent().reference) {
return Err("caller token disagrees with original preparation".into());
}
Ok(recorded)
});
let recorded = match gate {
Ok(recorded) => recorded,
Err(reason) => return self.close_preparation(id, reason, None),
};
if session.cancelled
|| self.is_retiring(&preparing.action.consumer)
|| preparing.action.invalidated
|| preparing
.action
.captures
.values()
.any(|target| !self.target_valid(target))
{
return self.close_preparation(
id,
"mutation cancelled or consumer retired before admission".into(),
Some(recorded),
);
}
let execution = match self.runtime.admit(&preparing.action.capability) {
Ok(execution) => execution,
Err(error) => return self.close_preparation(id, error.to_string(), Some(recorded)),
};
let preparing = self.actions.preparing.remove(&id).expect("preparing");
let mut session = preparing.session.expect("constructed owner");
session.caller = Some(Box::new(recorded.intent().confirmation()));
let owner = session.owner.clone();
self.actions.running.insert(
id,
Running {
continuations: Vec::new(),
consumer: preparing.action.consumer,
key: preparing.action.key,
method: preparing.action.method,
completed: false,
execution,
resources: preparing.action.resources.expect("captured"),
mutation: Some(session),
stream: None,
},
);
// Accepted Work protects the provider before its preparation fence ends.
self.runtime
.complete_resource(&preparing.resource, Ok(()))?;
let (send, receive) = tokio::sync::mpsc::channel(1);
Ok(Some(ActionTask {
id,
future: mutation_progress(owner.dispatch(recorded, send), receive),
}))
}
fn close_preparation(
&mut self,
id: Uuid,
reason: String,
recorded: Option<RecordedIntent>,
) -> Result<Option<ActionTask>> {
let preparing = self.actions.preparing.get_mut(&id).expect("preparing");
let session = preparing.session.as_mut().expect("constructed owner");
session.caller = recorded
.as_ref()
.map(|recorded| Box::new(recorded.intent().confirmation()));
preparing.outcome = Some(session.failure(reason));
preparing.recorded = recorded;
preparing.phase = Phase::Closing;
let owner = session.owner.clone();
Ok(Some(ActionTask {
id,
future: Box::pin(async move { TaskCompletion::Closed(owner.close().await) }),
}))
}
fn finish_preparation_close(
&mut self,
id: Uuid,
cleanup: std::result::Result<(), String>,
) -> Result<()> {
let preparing = self
.actions
.preparing
.get_mut(&id)
.ok_or_else(|| Error::Invalid("unknown preparation close".into()))?;
if !matches!(preparing.phase, Phase::Closing) {
return Err(Error::Invalid("preparation is not closing".into()));
}
let failure = cleanup.as_ref().err().cloned();
preparing.phase = Phase::Failed;
if let Some(outcome) = preparing.outcome.take() {
self.actions.outcomes.push_back(Outcome {
continuations: Vec::new(),
consumer: preparing.action.consumer.clone(),
key: preparing.action.key.clone(),
outcome,
request: None,
});
}
self.runtime
.complete_resource(&preparing.resource, cleanup)?;
if let Some(reason) = failure {
return Err(Error::Core(maka_plugin_core::Error::CleanupFailed(vec![
reason,
])));
}
self.actions.preparing.remove(&id);
Ok(())
}
pub(super) fn confirm_preparation_cleanup(
&mut self,
id: Uuid,
cleanup: std::result::Result<(), String>,
) -> Result<()> {
let preparing = self.actions.preparing.get_mut(&id).expect("preparation");
if !preparing.failed() {
return Err(Error::Invalid("preparation has not completed".into()));
}
// Only an embedding's real recovery receipt may confirm the original
// task/session cleanup. This neither repeats preparation nor dispatches.
preparing.phase = Phase::Closing;
self.finish_preparation_close(id, cleanup)
}
fn finish_mutation_progress(
&mut self,
id: Uuid,
value: Value,
next: TaskFuture,
) -> Result<Option<ActionTask>> {
let running = self
.actions
.running
.get(&id)
.ok_or_else(|| Error::Invalid("unknown mutation progress".into()))?;
let session = running
.mutation
.as_ref()
.filter(|_| !running.completed)
.ok_or_else(|| Error::Invalid("progress requires an active mutation".into()))?;
let Export::Service { methods, .. } = running.execution.value() else {
unreachable!()
};
let valid = methods[&running.method]
.progress
.as_ref()
.ok_or_else(|| Error::Invalid("mutation has no progress declaration".into()))
.and_then(|progress| {
progress
.validate(&value)
.map_err(|e| Error::Invalid(e.to_string()))
});
let consumer = running.consumer.clone();
let key = running.key.clone();
let wanted = self.update_wanted(&consumer, &key, id);
if session.progress.pending_feedback && !session.progress.stop.is_cancelled() {
return Err(Error::Invalid(
"mutation progress feedback has not committed".into(),
));
}
if let Err(error) = valid {
self.fail_update(id, error.to_string());
}
let flow = &mut self
.actions
.running
.get_mut(&id)
.expect("running")
.mutation
.as_mut()
.expect("mutation")
.progress;
if wanted && !flow.stop.is_cancelled() {
flow.pending_feedback = true;
self.actions.outcomes.push_back(Outcome {
continuations: Vec::new(),
consumer,
key,
outcome: ActionOutcome::Updated { value },
request: Some(id),
});
} else {
// Closing only the presentation drains the original mutation. It is
// neither cancellation of accepted effects nor physical cleanup.
flow.stop.cancel();
}
let stop = flow.stop.clone();
let ack = flow.ack.clone();
Ok(Some(ActionTask {
id,
future: Box::pin(async move {
tokio::select! { biased;
() = stop.cancelled() => {},
permit = ack.acquire_owned() => { if let Ok(permit) = permit { permit.forget(); } },
}
next.await
}),
}))
}
fn finish_mutation(&mut self, id: Uuid, completion: MutationCompletion) -> Result<()> {
let running = self
.actions
.running
.get(&id)
.ok_or_else(|| Error::Invalid("unknown mutation".into()))?;
if running.completed {
return Err(Error::Invalid("mutation already completed".into()));
}
let session = running
.mutation
.as_ref()
.ok_or_else(|| Error::Invalid("query has no operation receipt".into()))?;
let original = session.reference.get().expect("prepared reference");
let output = if let Some(reason) = &session.progress.failure {
session.failure(format!(
"mutation progress failed: {reason}; read original receipt"
))
} else if completion.reply.reference != *original {
session.failure(
"native completion changed the original reference; read original receipt".into(),
)
} else {
let candidate = ActionOutcome::Operation {
caller: session.caller.clone(),
reply: Box::new(completion.reply),
};
let valid: Result<()> = (|| {
let ActionOutcome::Operation { reply, .. } = &candidate else {
unreachable!()
};
if let Receipt::Recorded {
outcome: OperationOutcome::Returned { value },
..
} = &reply.receipt
{
let Export::Service { methods, .. } = running.execution.value() else {
unreachable!()
};
methods[&running.method]
.output
.validate(value)
.map_err(|e| Error::Invalid(e.to_string()))?;
}
Ok(())
})();
match valid {
Ok(()) => self.action_feedback(&running.consumer, candidate),
Err(error) => session.failure(format!(
"operation result delivery failed: {error}; read original receipt"
)),
}
};
let cleanup = completion.cleanup;
let failure = cleanup.as_ref().err().cloned();
self.runtime.settle(&running.execution, cleanup)?;
self.actions.outcomes.push_back(Outcome {
continuations: Vec::new(),
consumer: running.consumer.clone(),
key: running.key.clone(),
outcome: output,
request: None,
});
self.actions
.running
.get_mut(&id)
.expect("running")
.completed = true;
if let Some(reason) = failure {
return Err(Error::Core(maka_plugin_core::Error::CleanupFailed(vec![
reason,
])));
}
self.actions.running.remove(&id);
Ok(())
}
fn interrupt_task(&mut self, id: Uuid, reason: String) -> Result<()> {
let reason = format!("executor interrupted; cleanup unconfirmed: {reason}");
if let Some(preparing) = self.actions.preparing.get_mut(&id) {
if matches!(preparing.phase, Phase::Failed) {
return Err(Error::Invalid("preparation already failed".into()));
}
preparing.phase = Phase::Failed;
let outcome = preparing.outcome.take().unwrap_or_else(|| {
preparing
.session
.as_ref()
.expect("constructed owner")
.failure(reason.clone())
});
self.actions.outcomes.push_back(Outcome {
continuations: Vec::new(),
consumer: preparing.action.consumer.clone(),
key: preparing.action.key.clone(),
outcome,
request: None,
});
self.runtime
.complete_resource(&preparing.resource, Err(reason.clone()))?;
return Err(Error::Core(maka_plugin_core::Error::CleanupFailed(vec![
reason,
])));
}
let running = self
.actions
.running
.get(&id)
.ok_or_else(|| Error::Invalid("unknown interrupted task".into()))?;
if let Some(session) = &running.mutation {
let reference = session.reference.get().expect("prepared reference").clone();
self.finish_mutation(
id,
MutationCompletion {
reply: OperationReply {
reference,
receipt: Receipt::Unavailable {
diagnostic: reason.clone(),
},
},
cleanup: Err(reason),
},
)
} else {
self.finish_query(
id,
ServiceCompletion {
result: Err(reason.clone()),
cleanup: Err(reason),
},
)
}
}
}
// Keep the same completion future and resource owner across each bounded item.
fn mutation_progress(
mut completion: MutationFuture,
mut receive: tokio::sync::mpsc::Receiver<Value>,
) -> TaskFuture {
Box::pin(async move {
tokio::select! { biased;
value = receive.recv() => match value {
Some(value) => TaskCompletion::MutationProgress {
value, next: mutation_progress(completion, receive),
},
None => TaskCompletion::Mutation(completion.await),
},
completed = &mut completion => TaskCompletion::Mutation(completed),
}
})
}