use std::sync::{Arc, Mutex, MutexGuard};
use lgwks_std::wire::{Archive, Deserialize, Serialize};
use crate::proposal::{
Decoder, Intervention, LedgerLimits, Outcome, Plan, PlanBudget, PlanLimits, Provenance,
Refusal, RepairLedger, Source, Surface, payload_digest,
};
use super::run_store::{remember_at, step_definition};
use super::{FlowError, Scope};
#[derive(Clone)]
pub struct Gate {
inner: Arc<Mutex<Admission>>,
}
struct Admission {
decoder: Decoder,
surface: Surface,
budget: PlanBudget,
ledger: RepairLedger,
}
impl Gate {
#[must_use]
pub fn new(
tenant: &str,
surface: Surface,
decoder: Decoder,
admissions: PlanBudget,
ledger: LedgerLimits,
) -> Self {
Self {
inner: Arc::new(Mutex::new(Admission {
decoder,
surface,
budget: admissions,
ledger: RepairLedger::new(ledger, Provenance::of(Source::Model, tenant, b"")),
})),
}
}
#[must_use]
pub fn limits(&self) -> PlanLimits {
self.lock().decoder.limits()
}
#[must_use]
pub fn tenant(&self) -> String {
self.lock().surface.tenant().to_owned()
}
#[must_use]
pub fn remaining(&self) -> u32 {
self.lock().budget.remaining()
}
#[must_use]
pub fn spent(&self) -> u64 {
self.lock().ledger.spent()
}
#[must_use]
pub fn repetitions(&self, fingerprint: &str) -> u32 {
self.lock().ledger.repetitions(fingerprint)
}
#[must_use]
pub fn has_progress(&self, fingerprint: &str) -> bool {
self.lock().ledger.has_progress(fingerprint)
}
#[must_use]
pub fn record_evidence(&self, fingerprint: &str) -> bool {
self.lock().ledger.record_evidence(fingerprint)
}
fn lock(&self) -> MutexGuard<'_, Admission> {
crate::journal::owner::lock(&self.inner)
}
}
enum Decision {
Admitted(Plan),
Refused {
refusal: Refusal,
provenance: Provenance,
},
Intervention(Intervention),
}
pub async fn admit(
scope: &Scope,
step: &str,
gate: &Gate,
payload: &[u8],
source: Source,
) -> Result<Plan, FlowError> {
let here = scope.enter(step)?;
match decide(gate, payload, source) {
Decision::Admitted(plan) => Ok(plan),
Decision::Refused {
refusal,
provenance,
} => {
record_refusal(&here, payload, source, &refusal).await?;
Err(FlowError::Refused {
at: Arc::clone(here.shared_path()),
refusal: Box::new(refusal),
provenance,
})
}
Decision::Intervention(intervention) => Err(FlowError::Intervention {
at: Arc::clone(here.shared_path()),
intervention: Box::new(intervention),
}),
}
}
fn decide(gate: &Gate, payload: &[u8], source: Source) -> Decision {
let mut admission = gate.lock();
if let Err(refusal) = admission.budget.charge() {
return Decision::Refused {
refusal,
provenance: Provenance::of(source, admission.surface.tenant(), payload),
};
}
match admission
.decoder
.decode(&admission.surface, payload, source)
{
Outcome::Admitted { plan, .. } => Decision::Admitted(plan),
Outcome::Refused {
refusal,
provenance,
} => {
match admission.ledger.record_failure(refusal.label()) {
Outcome::Intervention(intervention) => Decision::Intervention(intervention),
_ => Decision::Refused {
refusal: *refusal,
provenance,
},
}
}
Outcome::Intervention(intervention) => Decision::Intervention(intervention),
}
}
const REFUSAL_SUB_STEP: &str = "refusal";
async fn record_refusal(
scope: &Scope,
payload: &[u8],
source: Source,
refusal: &Refusal,
) -> Result<(), FlowError> {
if super::run_store::installed().is_none() || scope.run().is_none() {
return Ok(());
}
let record = refusal_record(scope.path(), refusal, payload, source);
let run = scope.run();
let child = scope.enter(REFUSAL_SUB_STEP)?;
let definition = step_definition(scope, REFUSAL_SUB_STEP);
remember_at(&child, run, &definition, move || async move { Ok(record) })
.await
.map(|_| ())
}
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv)]
pub struct RefusalRecord {
at: String,
arm: String,
digest: String,
source: String,
}
impl RefusalRecord {
#[must_use]
pub fn at(&self) -> &str {
&self.at
}
#[must_use]
pub fn arm(&self) -> &str {
&self.arm
}
#[must_use]
pub fn digest(&self) -> &str {
&self.digest
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
}
#[must_use]
pub fn refusal_record(
at: &str,
refusal: &Refusal,
payload: &[u8],
source: Source,
) -> RefusalRecord {
RefusalRecord {
at: at.to_owned(),
arm: refusal.label().to_owned(),
digest: payload_digest(payload).to_hex(),
source: source.label().to_owned(),
}
}
pub fn read_refusal(bytes: &[u8]) -> Result<RefusalRecord, FlowError> {
<RefusalRecord as super::run_store::Durable>::from_record(bytes)
.map_err(|cause| FlowError::failed(format!("the recorded value is not a refusal: {cause}")))
}
#[must_use]
pub fn refusal_of(error: &FlowError) -> Option<&Refusal> {
match *error {
FlowError::Refused { ref refusal, .. } => Some(refusal.as_ref()),
_ => None,
}
}
#[must_use]
pub fn provenance_of(error: &FlowError) -> Option<&Provenance> {
match *error {
FlowError::Refused { ref provenance, .. } => Some(provenance),
_ => None,
}
}
#[must_use]
pub fn intervention_of(error: &FlowError) -> Option<&Intervention> {
match *error {
FlowError::Intervention {
ref intervention, ..
} => Some(intervention.as_ref()),
_ => None,
}
}