use std::future::Future;
use std::sync::Arc;
use lgwks_std::wire::rkyv::de::pooling::Pool;
use lgwks_std::wire::rkyv::rancor::Strategy;
use lgwks_std::wire::{Archive, Deserialize, Serialize, WireError};
use crate::effect::RunId;
use crate::rt::task_local;
use crate::task::{DefinitionIdentity, Drift};
use super::{FlowError, Scope, StepKey};
pub trait Durable: Sized {
fn to_record(&self) -> Result<Vec<u8>, WireError>;
fn from_record(bytes: &[u8]) -> Result<Self, WireError>;
}
impl<T> Durable for T
where
T: Archive,
for<'a> T: Serialize<
Strategy<
lgwks_std::wire::rkyv::ser::Serializer<
lgwks_std::wire::AlignedVec,
lgwks_std::wire::rkyv::ser::allocator::ArenaHandle<'a>,
lgwks_std::wire::rkyv::ser::sharing::Share,
>,
WireError,
>,
>,
for<'a> <T as Archive>::Archived: Deserialize<T, Strategy<Pool, WireError>>
+ lgwks_std::wire::rkyv::bytecheck::CheckBytes<
Strategy<
lgwks_std::wire::rkyv::validation::Validator<
lgwks_std::wire::rkyv::validation::archive::ArchiveValidator<'a>,
lgwks_std::wire::rkyv::validation::shared::SharedValidator,
>,
WireError,
>,
>,
{
fn to_record(&self) -> Result<Vec<u8>, WireError> {
lgwks_std::wire::to_bytes::<WireError>(self).map(|bytes| bytes.as_ref().to_vec())
}
fn from_record(bytes: &[u8]) -> Result<Self, WireError> {
lgwks_std::wire::from_bytes::<T, WireError>(bytes)
}
}
task_local! {
static RECORDS: Option<Records>;
static DEFINITION: Option<Arc<DefinitionIdentity>>;
static AUTHORITY: Option<Authority>;
}
#[derive(Clone)]
pub(crate) struct Authority(pub(crate) Arc<dyn AuthorityCheck>);
pub(crate) trait AuthorityCheck: Send + Sync {
fn uncovered(&self, required: &[crate::cap::Cap]) -> Vec<crate::cap::Shortage>;
}
impl Authority {
pub(crate) fn shortfall(
&self,
required: &[crate::cap::Cap],
) -> Option<Vec<crate::cap::Shortage>> {
let short = self.0.uncovered(required);
(!short.is_empty()).then_some(short)
}
}
pub(crate) fn authority() -> Option<Authority> {
let installed = AUTHORITY.try_with(Clone::clone);
if let Err(ref access) = installed {
lgwks_std::trace::debug!(error = ?access, "authority: polled outside every scope, so no authority is installed");
}
installed.ok().flatten()
}
pub(crate) async fn with_authority<R>(
authority: Option<Authority>,
body: impl Future<Output = R>,
) -> R {
AUTHORITY.scope(authority, body).await
}
pub trait RunRecords: Send + Sync {
fn lookup(
&self,
tenant: &str,
run: RunId,
key: StepKey,
) -> Result<Option<StoredValue>, FlowError>;
fn append(
&self,
tenant: &str,
run: RunId,
key: StepKey,
path: &str,
definition: &DefinitionIdentity,
bytes: Vec<u8>,
) -> Result<Appended, FlowError>;
fn append_async<'a>(
&'a self,
record: StagedRecord<'a>,
) -> crate::BoxFuture<'a, Result<Appended, FlowError>> {
Box::pin(async move {
let (tenant, run, key, path, definition) = (
record.tenant(),
record.run(),
record.key(),
record.path(),
record.definition(),
);
self.append(tenant, run, key, path, definition, record.into_bytes())
})
}
fn compatibility(
&self,
_run: RunId,
_definition: &DefinitionIdentity,
) -> Result<bool, FlowError> {
Ok(true)
}
fn drift(&self, _run: RunId, _definition: &DefinitionIdentity) -> Result<Drift, FlowError> {
Err(FlowError::failed(
"this store holds no definition identity for the run, so it cannot name a \
drift axis; refusing to invent one",
))
}
fn stage<'a>(
&self,
tenant: &'a str,
run: RunId,
key: StepKey,
path: &'a str,
definition: &'a DefinitionIdentity,
bytes: Vec<u8>,
) -> StagedRecord<'a> {
StagedRecord::of(tenant, run, key, path, definition, bytes)
}
}
#[derive(Debug, Clone)]
pub struct StagedRecord<'a> {
tenant: &'a str,
run: RunId,
key: StepKey,
path: &'a str,
definition: &'a DefinitionIdentity,
bytes: Vec<u8>,
}
impl<'a> StagedRecord<'a> {
fn of(
tenant: &'a str,
run: RunId,
key: StepKey,
path: &'a str,
definition: &'a DefinitionIdentity,
bytes: Vec<u8>,
) -> Self {
Self {
tenant,
run,
key,
path,
definition,
bytes,
}
}
#[must_use]
pub const fn tenant(&self) -> &'a str {
self.tenant
}
#[must_use]
pub const fn run(&self) -> RunId {
self.run
}
#[must_use]
pub const fn key(&self) -> StepKey {
self.key
}
#[must_use]
pub const fn path(&self) -> &'a str {
self.path
}
#[must_use]
pub const fn definition(&self) -> &'a DefinitionIdentity {
self.definition
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredValue {
path: String,
bytes: Vec<u8>,
}
impl StoredValue {
pub(crate) fn new(path: String, bytes: Vec<u8>) -> Self {
Self { path, bytes }
}
pub(crate) fn bytes(&self) -> &[u8] {
&self.bytes
}
pub(crate) fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Appended {
Recorded,
AlreadyRecorded,
Conflicting,
}
#[derive(Clone)]
pub(crate) struct Records(pub(crate) Arc<dyn RunRecords>);
impl Records {
pub(crate) fn lookup(
&self,
tenant: &str,
run: RunId,
key: StepKey,
) -> Result<Option<StoredValue>, FlowError> {
self.0.lookup(tenant, run, key)
}
pub(crate) fn agrees(
&self,
run: RunId,
definition: &DefinitionIdentity,
) -> Result<(), FlowError> {
match self.0.compatibility(run, definition) {
Ok(true) => Ok(()),
Ok(false) => Err(FlowError::incompatible("", self.0.drift(run, definition)?)),
Err(error) => Err(error),
}
}
pub(crate) fn record<'a>(
&'a self,
tenant: &'a str,
run: RunId,
key: StepKey,
path: &'a str,
definition: &'a DefinitionIdentity,
bytes: Vec<u8>,
) -> crate::BoxFuture<'a, Result<(), FlowError>> {
let conflict = std::sync::Arc::from(path);
Box::pin(async move {
match self
.0
.append_async(self.0.stage(tenant, run, key, path, definition, bytes))
.await
{
Ok(Appended::Recorded) | Ok(Appended::AlreadyRecorded) => Ok(()),
Ok(Appended::Conflicting) => Err(FlowError::Failed {
at: conflict,
reason: format!(
"a different value is already recorded for step {path:?} under this run; \
refusing to overwrite a committed record"
),
}),
Err(error) => Err(error.located_at(&conflict)),
}
})
}
pub(crate) async fn claim<'a>(
&'a self,
tenant: &'a str,
run: RunId,
key: StepKey,
path: &'a str,
definition: &'a DefinitionIdentity,
bytes: Vec<u8>,
) -> Result<Appended, FlowError> {
self.0
.append_async(self.0.stage(tenant, run, key, path, definition, bytes))
.await
}
}
impl std::fmt::Debug for Records {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("RunRecords")
}
}
pub(crate) async fn within<R>(
records: Option<Records>,
definition: &DefinitionIdentity,
body: impl Future<Output = R>,
) -> R {
RECORDS
.scope(
records,
DEFINITION.scope(Some(Arc::new(definition.clone())), body),
)
.await
}
pub(crate) fn installed_definition() -> Option<Arc<DefinitionIdentity>> {
let installed = DEFINITION.try_with(Clone::clone);
if let Err(ref access) = installed {
lgwks_std::trace::debug!(error = ?access, "installed_definition: polled outside every scope, so no identity is installed");
}
installed.ok().flatten()
}
pub(crate) fn installed() -> Option<Records> {
let installed = RECORDS.try_with(Clone::clone);
if let Err(ref access) = installed {
lgwks_std::trace::debug!(error = ?access, "installed: polled outside every scope, so no store is installed");
}
installed.ok().flatten()
}
pub(crate) fn step_definition(scope: &Scope, step: &str) -> Arc<DefinitionIdentity> {
match installed_definition() {
Some(installed) => installed,
None => Arc::new(definition_of(scope, step)),
}
}
pub async fn remember<T, Fut>(
scope: &Scope,
step: &str,
body: impl FnOnce() -> Fut,
) -> Result<T, FlowError>
where
T: lgwks_std::wire::Archive + Durable,
Fut: Future<Output = Result<T, FlowError>>,
{
let child = scope.enter(step)?;
let definition = step_definition(scope, step);
step_in(&child, &definition, body).await
}
pub(crate) fn definition_of(scope: &Scope, step: &str) -> DefinitionIdentity {
let mut hasher = lgwks_std::hash::Hasher::new();
hasher.write_framed(b"lgwks.bot.definition.v1");
hasher.write_framed(scope.tenant().as_str().as_bytes());
hasher.write_framed(step.as_bytes());
if let Some(run) = scope.run() {
hasher.write_framed(run.id().to_hex().as_bytes());
} else {
hasher.write_framed(b"no-run");
}
DefinitionIdentity::new(scope.path(), u64::MAX, hasher.finalize(), 1)
}
pub(crate) async fn step_in<T, Fut>(
scope: &Scope,
definition: &DefinitionIdentity,
body: impl FnOnce() -> Fut,
) -> Result<T, FlowError>
where
T: lgwks_std::wire::Archive + Durable,
Fut: Future<Output = Result<T, FlowError>>,
{
remember_at(scope, scope.run(), definition, body).await
}
pub(crate) async fn remember_at<T, Fut>(
scope: &Scope,
run: Option<RunId>,
definition: &DefinitionIdentity,
body: impl FnOnce() -> Fut,
) -> Result<T, FlowError>
where
T: lgwks_std::wire::Archive + Durable,
Fut: Future<Output = Result<T, FlowError>>,
{
let Some(records) = installed() else {
return body().await;
};
let Some(run) = run else {
return body().await;
};
records
.agrees(run, definition)
.map_err(|error| error.located_at(scope.shared_path()))?;
let key = scope.key();
let tenant = scope.tenant().as_str();
if let Some(stored) = records.lookup(tenant, run, key)? {
return decode(&stored);
}
let value = body()
.await
.map_err(|error| error.located_at(scope.shared_path()))?;
let bytes = encode(&value).map_err(|error| error.located_at(scope.shared_path()))?;
records
.record(tenant, run, key, scope.path(), definition, bytes)
.await?;
Ok(value)
}
fn decode<T>(stored: &StoredValue) -> Result<T, FlowError>
where
T: Durable,
{
T::from_record(&stored.bytes).map_err(|cause| FlowError::Failed {
at: std::sync::Arc::from(stored.path.as_str()),
reason: format!("the value recorded for this step is not the type it returns: {cause}"),
})
}
fn encode<T>(value: &T) -> Result<Vec<u8>, FlowError>
where
T: Durable,
{
value.to_record().map_err(|cause| FlowError::Failed {
at: std::sync::Arc::from(""),
reason: format!("this step's value could not be archived: {cause}"),
})
}