use std::fmt;
use std::num::NonZeroU128;
use std::sync::Arc;
use lgwks_std::hash::Hasher;
use lgwks_std::wire::{Archive, Deserialize, Serialize, WireError, from_bytes, to_bytes};
use crate::effect::{Id128, InputIdentity, RunId};
use crate::script::{Durable, FlowError};
use super::{Disposition, Report, StoreError};
pub const MAX_REQUEST_KEY_BYTES: usize = 128;
pub(crate) const BINDING_STEP: &str = "@request";
pub(crate) const TERMINAL_STEP: &str = "@terminal";
const RUN_DOMAIN: &[u8] = b"lgwks.bot.request-run.v1";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RequestKey(Arc<str>);
impl RequestKey {
pub fn new(key: &str) -> Result<Self, FlowError> {
use crate::script::scope::{NameFault, name_fault};
let reason = match name_fault(key, MAX_REQUEST_KEY_BYTES) {
None => return Ok(Self(Arc::from(key))),
Some(NameFault::Empty) => "the request key is empty",
Some(NameFault::TooLong) => "the request key is longer than MAX_REQUEST_KEY_BYTES",
Some(NameFault::BadChar) => {
"the request key may hold only ASCII letters, digits, '-', '_', '.' and ':'"
}
};
Err(FlowError::InvalidRequestKey { reason })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RequestKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InputDigest([u8; 32]);
impl InputDigest {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn to_hex(&self) -> String {
lgwks_std::hex::encode(self.0)
}
#[must_use]
pub fn of<I: InputIdentity>(input: &I) -> Self {
let mut hasher = Hasher::new();
hasher.write_framed(I::SCHEMA_ID);
input.write_identity(&mut hasher);
Self(*hasher.finalize().as_bytes())
}
}
impl fmt::Display for InputDigest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_hex())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Submission<O> {
Executed(Report<O>),
Reattached(Report<O>),
InFlight(InFlight),
}
impl<O> Submission<O> {
#[must_use]
pub fn report(&self) -> Option<&Report<O>> {
match *self {
Self::Executed(ref report) | Self::Reattached(ref report) => Some(report),
Self::InFlight(_) => None,
}
}
#[must_use]
pub fn run_id(&self) -> Option<crate::effect::RunId> {
match *self {
Self::Executed(ref report) | Self::Reattached(ref report) => report.run_id(),
Self::InFlight(ref in_flight) => Some(in_flight.run()),
}
}
}
#[derive(Debug, Clone)]
pub struct InFlight {
run: crate::effect::RunId,
task: String,
records: usize,
}
impl InFlight {
pub(crate) fn new(run: crate::effect::RunId, task: String, records: usize) -> Self {
Self { run, task, records }
}
#[must_use]
pub const fn run(&self) -> crate::effect::RunId {
self.run
}
#[must_use]
pub fn task(&self) -> &str {
&self.task
}
#[must_use]
pub const fn records(&self) -> usize {
self.records
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestConflict {
key: RequestKey,
existing: InputDigest,
requested: InputDigest,
}
impl RequestConflict {
pub(crate) fn new(key: RequestKey, existing: InputDigest, requested: InputDigest) -> Self {
Self {
key,
existing,
requested,
}
}
#[must_use]
pub fn key(&self) -> &RequestKey {
&self.key
}
#[must_use]
pub const fn existing(&self) -> InputDigest {
self.existing
}
#[must_use]
pub const fn requested(&self) -> InputDigest {
self.requested
}
}
impl fmt::Display for RequestConflict {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"request key {:?} is already bound to input digest {} and cannot be rebound to {}",
self.key.as_str(),
self.existing.to_hex(),
self.requested.to_hex()
)
}
}
impl std::error::Error for RequestConflict {}
#[derive(Debug)]
#[non_exhaustive]
pub enum RequestError {
NoStore,
Conflict(RequestConflict),
Store(StoreError),
Record(FlowError),
IdCollision,
}
impl fmt::Display for RequestError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::NoStore => formatter.write_str(
"this host has no run store, so a durable request has nowhere to record its \
receipt; install one with HostBuilder::run_store",
),
Self::Conflict(ref conflict) => conflict.fmt(formatter),
Self::Store(ref cause) => write!(formatter, "the run store refused: {cause}"),
Self::Record(ref cause) => write!(formatter, "a request record was refused: {cause}"),
Self::IdCollision => formatter.write_str(
"the request key hashed to the all-zero run identity; use a different key",
),
}
}
}
impl std::error::Error for RequestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Self::Conflict(ref conflict) => Some(conflict),
Self::Store(ref cause) => Some(cause),
Self::Record(ref cause) => Some(cause),
Self::NoStore | Self::IdCollision => None,
}
}
}
impl From<StoreError> for RequestError {
fn from(cause: StoreError) -> Self {
Self::Store(cause)
}
}
impl From<FlowError> for RequestError {
fn from(cause: FlowError) -> Self {
Self::Record(cause)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[rkyv(crate = lgwks_std::wire::rkyv, compare(PartialEq), derive(Debug))]
pub(crate) enum TerminalRecord {
Succeeded {
value: Vec<u8>,
},
Stopped {
disposition: u8,
at: String,
reason: String,
},
}
const DISPOSITION_SUCCEEDED: u8 = 0;
const DISPOSITION_FAILED: u8 = 1;
const DISPOSITION_CANCELLED: u8 = 2;
const DISPOSITION_DEADLINE: u8 = 3;
const DISPOSITION_REFUSED: u8 = 4;
const DISPOSITION_BLOCKED: u8 = 5;
fn disposition_code(disposition: Disposition) -> u8 {
match disposition {
Disposition::Succeeded => DISPOSITION_SUCCEEDED,
Disposition::Failed => DISPOSITION_FAILED,
Disposition::Cancelled => DISPOSITION_CANCELLED,
Disposition::DeadlineExceeded => DISPOSITION_DEADLINE,
Disposition::Refused => DISPOSITION_REFUSED,
Disposition::Blocked => DISPOSITION_BLOCKED,
}
}
fn disposition_from_code(code: u8) -> Option<Disposition> {
match code {
DISPOSITION_SUCCEEDED => Some(Disposition::Succeeded),
DISPOSITION_FAILED => Some(Disposition::Failed),
DISPOSITION_CANCELLED => Some(Disposition::Cancelled),
DISPOSITION_DEADLINE => Some(Disposition::DeadlineExceeded),
DISPOSITION_REFUSED => Some(Disposition::Refused),
DISPOSITION_BLOCKED => Some(Disposition::Blocked),
_ => None,
}
}
impl TerminalRecord {
pub(crate) fn succeeded<O: Durable>(output: &O) -> Result<Self, FlowError> {
let value = output.to_record().map_err(|cause| {
FlowError::failed(format!(
"the request's output could not be archived: {cause}"
))
})?;
Ok(Self::Succeeded { value })
}
pub(crate) fn stopped(disposition: Disposition, at: &str, reason: String) -> Self {
Self::Stopped {
disposition: disposition_code(disposition),
at: at.to_owned(),
reason,
}
}
pub(crate) fn encode(&self) -> Result<Vec<u8>, FlowError> {
to_bytes::<WireError>(self)
.map(|bytes| bytes.as_ref().to_vec())
.map_err(|cause| {
FlowError::failed(format!(
"the request terminal record could not be archived: {cause}"
))
})
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self, FlowError> {
from_bytes::<Self, WireError>(bytes).map_err(|cause| {
FlowError::failed(format!(
"the request terminal record could not be decoded: {cause}"
))
})
}
pub(crate) fn parts<O: Durable>(
self,
) -> Result<(Disposition, Option<O>, Option<FlowError>), FlowError> {
match self {
Self::Succeeded { value } => {
let output = O::from_record(&value).map_err(|cause| {
FlowError::failed(format!(
"the value recorded for this request is not the type it returns: {cause}"
))
})?;
Ok((Disposition::Succeeded, Some(output), None))
}
Self::Stopped {
disposition,
at,
reason,
} => {
let disposition = disposition_from_code(disposition).ok_or_else(|| {
FlowError::failed(format!("unknown disposition code {disposition}"))
})?;
let error = FlowError::Failed {
at: Arc::from(at.as_str()),
reason,
};
Ok((disposition, None, Some(error)))
}
}
}
}
pub(crate) fn derive_run(tenant: &str, key: &RequestKey) -> Result<RunId, RequestError> {
let mut hasher = Hasher::new();
hasher
.write_framed(RUN_DOMAIN)
.write_framed(tenant.as_bytes())
.write_framed(key.as_str().as_bytes());
let digest = hasher.finalize();
let mut raw = [0u8; 16];
match digest.as_bytes().get(..raw.len()) {
Some(head) => raw.copy_from_slice(head),
None => {
let refusal = Err(RequestError::IdCollision);
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "derive_run: returning an error to the caller");
return refusal;
}
}
match NonZeroU128::new(u128::from_be_bytes(raw)) {
Some(value) => Ok(RunId::new(Id128::from_nonzero(value))),
None => Err(RequestError::IdCollision),
}
}
pub(crate) fn digest_of_record(bytes: &[u8]) -> Result<InputDigest, RequestError> {
let raw: [u8; 32] = bytes.try_into().map_err(|error| {
RequestError::Record(FlowError::failed(format!(
"the request receipt is not a 32-byte input digest: {error}"
)))
})?;
Ok(InputDigest::from_bytes(raw))
}