use crate::bindings::golem::quota::types;
use crate::schema::wit::GuestQuotaTokenHandle;
use crate::schema::wit::direct::{
FromWire, IntoWire, WireError, WirePreflight, WireReader, WireSchema, WireSchemaBuilder,
WireWriter,
};
use std::time::Duration;
#[cfg(feature = "export_golem_agentic")]
use crate::schema::{
FromSchema, FromSchemaError, IntoSchema, QuotaTokenSpec, SchemaBuilder, SchemaType,
SchemaValue, TypeId,
};
#[derive(Debug, Clone, PartialEq)]
pub struct FailedReservation {
pub estimated_wait: Option<Duration>,
}
impl From<types::FailedReservation> for FailedReservation {
fn from(raw: types::FailedReservation) -> Self {
Self {
estimated_wait: raw.estimated_wait_nanos.map(Duration::from_nanos),
}
}
}
impl std::fmt::Display for FailedReservation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.estimated_wait {
Some(d) => write!(f, "quota reservation failed (retry after {d:?})"),
None => write!(f, "quota reservation failed"),
}
}
}
impl std::error::Error for FailedReservation {}
#[must_use]
pub struct Reservation {
raw: types::Reservation,
}
impl Reservation {
pub fn commit(self, used: u64) {
types::Reservation::commit(self.raw, used);
}
}
pub struct QuotaToken {
handle: GuestQuotaTokenHandle,
}
impl QuotaToken {
pub fn new(resource_name: &str, expected_use: u64) -> Self {
Self {
handle: GuestQuotaTokenHandle::new(types::new_token(resource_name, expected_use)),
}
}
pub fn reserve(&self, amount: u64) -> Result<Reservation, FailedReservation> {
self.handle
.with_handle(|raw| types::reserve(raw, amount))
.unwrap_or_else(|| panic!("{TOKEN_CONSUMED}"))
.map(|raw| Reservation { raw })
.map_err(FailedReservation::from)
}
pub fn split(&mut self, child_expected_use: u64) -> QuotaToken {
let raw = self
.handle
.with_handle(|raw| types::split(raw, child_expected_use))
.unwrap_or_else(|| panic!("{TOKEN_CONSUMED}"));
QuotaToken {
handle: GuestQuotaTokenHandle::new(raw),
}
}
pub fn merge(&mut self, other: QuotaToken) {
if self.handle.cell_id() == other.handle.cell_id() {
panic!("cannot merge a quota token with itself");
}
if !self.handle.is_present() {
panic!("{TOKEN_CONSUMED}");
}
let other_raw = other
.handle
.take()
.unwrap_or_else(|| panic!("{TOKEN_CONSUMED}"));
self.handle
.with_handle(|raw| types::merge(raw, other_raw))
.unwrap_or_else(|| panic!("{TOKEN_CONSUMED}"));
}
}
const TOKEN_CONSUMED: &str = "quota token has already been transferred and can no longer be used; split the token first if \
you need to both keep and send a capability";
impl WireSchema for QuotaToken {
fn wire_type_id() -> String {
"golem.core.QuotaToken".to_string()
}
fn append_schema(builder: &mut WireSchemaBuilder) -> i32 {
GuestQuotaTokenHandle::append_schema(builder)
}
}
impl FromWire for QuotaToken {
fn read_wire(reader: &mut WireReader, index: i32) -> Result<Self, WireError> {
Ok(Self {
handle: GuestQuotaTokenHandle::read_wire(reader, index)?,
})
}
}
impl IntoWire for QuotaToken {
fn preflight(&self, resources: &mut WirePreflight) -> Result<(), WireError> {
self.handle.preflight(resources)
}
fn write_wire(&self, writer: &mut WireWriter) -> Result<i32, WireError> {
self.handle.write_wire(writer)
}
}
#[cfg(feature = "export_golem_agentic")]
impl IntoSchema for QuotaToken {
fn type_id() -> TypeId {
TypeId::new("golem.core.QuotaToken")
}
fn register_in(_builder: &mut SchemaBuilder) -> SchemaType {
SchemaType::quota_token(QuotaTokenSpec::default())
}
fn to_value(&self) -> SchemaValue {
SchemaValue::QuotaToken(self.handle.clone())
}
}
#[cfg(feature = "export_golem_agentic")]
impl FromSchema for QuotaToken {
fn from_value(value: &SchemaValue) -> Result<Self, FromSchemaError> {
match value {
SchemaValue::QuotaToken(handle) => Ok(QuotaToken {
handle: handle.clone(),
}),
other => Err(FromSchemaError::shape_mismatch(
"quota-token",
format!("{other:?}"),
"QuotaToken",
)),
}
}
}
pub fn with_reservation<T, F>(token: &QuotaToken, amount: u64, f: F) -> Result<T, FailedReservation>
where
F: FnOnce(&Reservation) -> (u64, T),
{
let reservation = token.reserve(amount)?;
let (used, value) = f(&reservation);
reservation.commit(used);
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::wit::{direct, wire};
use test_r::test;
#[test]
fn direct_quota_preflight_is_atomic_and_transfer_is_affine() {
let first = GuestQuotaTokenHandle::new(unsafe { types::QuotaToken::from_handle(17) });
let second = GuestQuotaTokenHandle::new(unsafe { types::QuotaToken::from_handle(29) });
let tokens = vec![
QuotaToken {
handle: first.clone(),
},
QuotaToken {
handle: second.clone(),
},
QuotaToken {
handle: second.clone(),
},
];
assert!(matches!(
direct::encode(&tokens),
Err(WireError::AliasedResource(_))
));
assert!(first.is_present());
assert!(second.is_present());
let tokens = (
QuotaToken {
handle: first.clone(),
},
QuotaToken {
handle: second.clone(),
},
);
let tree = direct::encode(&tokens).unwrap();
assert!(!first.is_present());
assert!(!second.is_present());
assert!(matches!(
direct::encode(&tokens),
Err(WireError::ConsumedResource(_))
));
let decoded = direct::decode::<(QuotaToken, QuotaToken)>(tree).unwrap();
let forwarded = direct::encode(&decoded).unwrap();
let handles = forwarded
.value_nodes
.into_iter()
.filter_map(|node| match node {
wire::SchemaValueNode::QuotaTokenHandle(handle) => Some(handle.take_handle()),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(handles, vec![17, 29]);
}
}