use crate::base::{BudgetCause, Error, ErrorKind, ExprCause, Result};
use crate::expr::{Expr, ExprEngine, FromValue};
use bumpalo::Bump;
use datalogic_rs::Logic;
use datavalue::DataValue;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConditionErrorPolicy {
#[default]
Skip,
Stop,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DispatchKind<'a> {
InlineBuiltin {
workflow_id: &'a str,
task_id: &'a str,
function: &'a str,
},
HostTask {
workflow_id: &'a str,
task_id: &'a str,
function: &'a str,
},
LoopStep {
workflow_id: &'a str,
iteration: u64,
},
ForEachElement {
workflow_id: &'a str,
task_id: &'a str,
index: usize,
},
}
pub struct OccupancyGuard<'a> {
on_drop: Option<Box<dyn FnOnce() + Send + 'a>>,
}
impl<'a> OccupancyGuard<'a> {
pub const fn noop() -> Self {
Self { on_drop: None }
}
pub fn new(on_drop: impl FnOnce() + Send + 'a) -> Self {
Self {
on_drop: Some(Box::new(on_drop)),
}
}
}
impl Default for OccupancyGuard<'_> {
fn default() -> Self {
Self::noop()
}
}
impl fmt::Debug for OccupancyGuard<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OccupancyGuard")
.field("active", &self.on_drop.is_some())
.finish()
}
}
impl Drop for OccupancyGuard<'_> {
fn drop(&mut self) {
if let Some(f) = self.on_drop.take() {
f();
}
}
}
pub trait RunAccountant: Send + Sync {
fn before_task_dispatch(&self, kind: DispatchKind<'_>) -> Result<OccupancyGuard<'_>> {
let _ = kind;
Ok(OccupancyGuard::noop())
}
fn on_expr_evaluated(&self, ops: u64) {
let _ = ops;
}
fn charge_custom(&self, kind: &str, units: u64) -> Result<()> {
let _ = (kind, units);
Ok(())
}
}
pub(crate) struct TaskBudget<'a> {
max_tasks: Option<u64>,
tasks_used: AtomicU64,
max_expr_ops: Option<u64>,
expr_ops_used: AtomicU64,
accountant: Option<&'a dyn RunAccountant>,
}
impl fmt::Debug for TaskBudget<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TaskBudget")
.field("max_tasks", &self.max_tasks)
.field("tasks_used", &self.tasks_used.load(Ordering::Relaxed))
.field("max_expr_ops", &self.max_expr_ops)
.field("expr_ops_used", &self.expr_ops_used.load(Ordering::Relaxed))
.field("has_accountant", &self.accountant.is_some())
.finish()
}
}
impl<'a> TaskBudget<'a> {
pub(crate) const fn new(
max_tasks: Option<u64>,
max_expr_ops: Option<u64>,
accountant: Option<&'a dyn RunAccountant>,
) -> Self {
Self {
max_tasks,
tasks_used: AtomicU64::new(0),
max_expr_ops,
expr_ops_used: AtomicU64::new(0),
accountant,
}
}
pub(crate) const fn unlimited() -> Self {
Self::new(None, None, None)
}
pub(crate) const fn accountant(&self) -> Option<&'a dyn RunAccountant> {
self.accountant
}
pub(crate) const fn is_run_limited(&self) -> bool {
self.max_tasks.is_some() || self.max_expr_ops.is_some() || self.accountant.is_some()
}
pub(crate) fn charge_dispatch(&self, kind: DispatchKind<'_>) -> Result<OccupancyGuard<'_>> {
if let Some(limit) = self.max_tasks {
let admitted =
self.tasks_used
.try_update(Ordering::Relaxed, Ordering::Relaxed, |used| {
(used < limit).then_some(used + 1)
});
if admitted.is_err() {
let used = limit.saturating_add(1);
return Err(Error::new(
ErrorKind::Budget,
format!("run task budget exceeded ({used} > {limit} max tasks)"),
)
.with_expr_cause(ExprCause::budget_exceeded(used, limit, None)));
}
}
match self.accountant {
Some(acc) => acc.before_task_dispatch(kind),
None => Ok(OccupancyGuard::noop()),
}
}
pub(crate) fn eval_view<'e, T: FromValue>(
&self,
exprs: &ExprEngine,
expr: &'e Expr<T>,
root: &DataValue<'_>,
arena: &Bump,
) -> Result<T::Out<'e>> {
if let Some(c) = expr.constant() {
return Ok(T::lend(c));
}
let ops_limit = self.remaining_ops_limit(exprs);
let (mut res, ops) = exprs.eval_view_metered(expr, root, arena, ops_limit);
self.reconcile_ops(ops, &mut res);
res
}
pub(crate) fn eval_in<'v>(
&self,
exprs: &ExprEngine,
logic: &'v Logic,
reads_secret: bool,
root: &'v DataValue<'v>,
arena: &'v Bump,
) -> Result<(&'v DataValue<'v>, Option<u64>)> {
let ops_limit = self.remaining_ops_limit(exprs);
let (mut res, ops) = exprs.eval_in_metered(logic, reads_secret, root, arena, ops_limit);
self.reconcile_ops(ops, &mut res);
res.map(|v| (v, ops))
}
fn remaining_ops_limit(&self, exprs: &ExprEngine) -> Option<u64> {
let limit = self.max_expr_ops?;
let used = self.expr_ops_used.load(Ordering::Relaxed);
let remaining = limit.saturating_sub(used);
#[cfg(feature = "budget")]
{
let engine_limit = exprs.logic().resolve_ops_budget(None);
if engine_limit < remaining {
return Some(engine_limit);
}
}
let _ = exprs;
Some(remaining)
}
fn reconcile_ops<T>(&self, ops: Option<u64>, res: &mut Result<T>) {
let Some(ops) = ops else {
return;
};
if let Some(acc) = self.accountant {
acc.on_expr_evaluated(ops);
}
let Some(limit) = self.max_expr_ops else {
return;
};
let prev = self.expr_ops_used.fetch_add(ops, Ordering::Relaxed);
let total = prev.saturating_add(ops);
if total <= limit {
return;
}
match res {
Err(err) if *err.kind() == ErrorKind::Budget => {
if let Some(mut cause) = err.expr_cause().cloned()
&& cause.budget.is_none_or(|b| b.limit <= limit)
{
cause.budget = Some(BudgetCause { used: total, limit });
cause.ops = Some(ops);
let updated = mem_take_error(err).with_expr_cause(cause);
*err = updated;
}
}
_ => {
*res = Err(Error::new(
ErrorKind::Budget,
format!("run expression ops budget exceeded ({total} > {limit} max ops)"),
)
.with_expr_cause(ExprCause::budget_exceeded(
total,
limit,
Some(ops),
)));
}
}
}
}
fn mem_take_error(err: &mut Error) -> Error {
std::mem::replace(err, Error::new(ErrorKind::Eval, ""))
}