Skip to main content

golem_rust/
quota.rs

1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Ergonomic wrappers for the `golem:quota/host` WIT interface.
16//!
17//! # Typical usage
18//!
19//! ```rust,ignore
20//! use golem_rust::quota::{QuotaToken, with_reservation};
21//!
22//! let token = QuotaToken::new("openai-tokens", 1000);
23//! let result = with_reservation(&token, 500, |_reservation| {
24//!     // ... call external API ...
25//!     let actual_used = 312u64;
26//!     (actual_used, my_api_result)
27//! });
28//! ```
29
30use crate::bindings::golem::api::host::EnvironmentId;
31use crate::bindings::golem::quota::types;
32use crate::value_and_type::type_builder::TypeNodeBuilder;
33use crate::value_and_type::wasi::Datetime;
34use crate::value_and_type::{FromValueAndType, IntoValue};
35use golem_wasm::{NodeBuilder, WitValueExtractor};
36use std::time::Duration;
37
38/// Error returned when a reservation cannot be granted because the resource's
39/// enforcement policy is `reject`.
40///
41/// Contains an optional estimated wait time — only available for rate-limited
42/// resources where a future refill is predictable.
43#[derive(Debug, Clone, PartialEq)]
44pub struct FailedReservation {
45    /// How long the caller would likely need to wait for capacity, if known.
46    pub estimated_wait: Option<Duration>,
47}
48
49impl From<types::FailedReservation> for FailedReservation {
50    fn from(raw: types::FailedReservation) -> Self {
51        Self {
52            estimated_wait: raw.estimated_wait_nanos.map(Duration::from_nanos),
53        }
54    }
55}
56
57impl std::fmt::Display for FailedReservation {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self.estimated_wait {
60            Some(d) => write!(f, "quota reservation failed (retry after {d:?})"),
61            None => write!(f, "quota reservation failed"),
62        }
63    }
64}
65
66impl std::error::Error for FailedReservation {}
67
68/// A short-lived capability representing a pending resource consumption.
69///
70/// Dropping a `Reservation` without calling [`commit`][Reservation::commit] is
71/// equivalent to committing with the full reserved amount
72#[must_use]
73pub struct Reservation {
74    raw: types::Reservation,
75}
76
77impl Reservation {
78    /// Commit actual usage.
79    ///
80    /// - If `used` < reserved — unused capacity is returned to the pool.
81    /// - If `used` > reserved — the excess is deducted from the token's
82    ///   remaining allocation as "debt".
83    pub fn commit(self, used: u64) {
84        types::Reservation::commit(self.raw, used);
85    }
86}
87
88/// An unforgeable capability granting the right to consume a named resource.
89///
90/// Dropping the token releases the underlying lease back to the executor pool.
91///
92/// # Example
93///
94/// ```rust,ignore
95/// let token = QuotaToken::new("llm-tokens", 1000);
96/// match token.reserve(500) {
97///     Ok(reservation) => {
98///         let used = call_llm_api();
99///         reservation.commit(used);
100///     }
101///     Err(e) => eprintln!("quota unavailable: {e}"),
102/// }
103/// ```
104pub struct QuotaToken {
105    raw: types::QuotaToken,
106}
107
108impl QuotaToken {
109    /// Request a quota capability for the given resource.
110    ///
111    /// - `resource_name`: the resource name as declared in the manifest.
112    /// - `expected_use`: expected units per reservation; used to derive the
113    ///   credit rate and max-credit for fair scheduling.
114    pub fn new(resource_name: &str, expected_use: u64) -> Self {
115        Self {
116            raw: types::QuotaToken::new(resource_name, expected_use),
117        }
118    }
119
120    /// Reserve `amount` units from the local allocation.
121    ///
122    /// Blocks internally until capacity is available or the resource's
123    /// enforcement action fires.  Returns a [`Reservation`] handle that
124    /// must be committed (or dropped) to release unused capacity.
125    ///
126    /// Returns `Err(FailedReservation)` when the enforcement policy is `reject`.
127    /// For `throttle` / `terminate` policies the call suspends or terminates
128    /// the agent before returning.
129    pub fn reserve(&self, amount: u64) -> Result<Reservation, FailedReservation> {
130        self.raw
131            .reserve(amount)
132            .map(|raw| Reservation { raw })
133            .map_err(FailedReservation::from)
134    }
135
136    /// Split off a child token with `child_expected_use` units.
137    ///
138    /// - The parent's expected-use is reduced by `child_expected_use`.
139    /// - Credits are divided proportionally between parent and child.
140    ///
141    /// # Panics
142    ///
143    /// Traps if `child_expected_use` exceeds the parent's current expected-use.
144    pub fn split(&mut self, child_expected_use: u64) -> QuotaToken {
145        QuotaToken {
146            raw: self.raw.split(child_expected_use),
147        }
148    }
149
150    /// Merge `other` into this token, combining expected-use and credits.
151    ///
152    /// Both tokens must refer to the same resource (same resource-name and
153    /// environment).  `other` is consumed.
154    ///
155    /// # Panics
156    ///
157    /// Traps if the tokens refer to different resources.
158    pub fn merge(&mut self, other: QuotaToken) {
159        self.raw.merge(other.raw);
160    }
161
162    fn to_record(&self) -> types::QuotaTokenRecord {
163        self.raw.to_record()
164    }
165
166    fn from_record(record: &types::QuotaTokenRecord) -> QuotaToken {
167        QuotaToken {
168            raw: types::QuotaToken::from_record(record),
169        }
170    }
171}
172
173/// Reserve `amount` units, run `f`, then commit the actual usage returned by `f`.
174///
175/// `f` receives a shared reference to the [`Reservation`] (for inspection) and
176/// must return `(used, value)`.
177///
178/// Returns `Err(FailedReservation)` if the reservation could not be granted.
179///
180/// # Example
181///
182/// ```rust,ignore
183/// let result = with_reservation(&token, 500, |_res| {
184///     let data = call_external_api();
185///     (data.tokens_used, data)
186/// });
187/// ```
188pub fn with_reservation<T, F>(token: &QuotaToken, amount: u64, f: F) -> Result<T, FailedReservation>
189where
190    F: FnOnce(&Reservation) -> (u64, T),
191{
192    let reservation = token.reserve(amount)?;
193    let (used, value) = f(&reservation);
194    reservation.commit(used);
195    Ok(value)
196}
197
198impl IntoValue for QuotaToken {
199    fn add_to_builder<T: NodeBuilder>(self, builder: T) -> T::Result {
200        let record = self.to_record();
201        let builder = builder.record();
202        let builder = record.environment_id.add_to_builder(builder.item());
203        let builder = record.resource_name.add_to_builder(builder.item());
204        let builder = record.expected_use.add_to_builder(builder.item());
205        let builder = record.last_credit.add_to_builder(builder.item());
206        let builder = record.last_credit_at.add_to_builder(builder.item());
207        builder.finish()
208    }
209
210    fn add_to_type_builder<T: TypeNodeBuilder>(builder: T) -> T::Result {
211        let builder = builder.record(
212            Some("QuotaTokenRecord".to_string()),
213            Some("golem:quota".to_string()),
214        );
215        let builder = <EnvironmentId>::add_to_type_builder(builder.field("environment-id"));
216        let builder = <String>::add_to_type_builder(builder.field("resource-name"));
217        let builder = <u64>::add_to_type_builder(builder.field("expected-use"));
218        let builder = <i64>::add_to_type_builder(builder.field("last-credit"));
219        let builder = <Datetime>::add_to_type_builder(builder.field("last-credit-at"));
220        builder.finish()
221    }
222}
223
224impl FromValueAndType for QuotaToken {
225    fn from_extractor<'a, 'b>(
226        extractor: &'a impl WitValueExtractor<'a, 'b>,
227    ) -> Result<Self, String> {
228        let environment_id = <EnvironmentId>::from_extractor(
229            &extractor
230                .field(0)
231                .ok_or_else(|| "Missing environment-id".to_string())?,
232        )?;
233        let resource_name = <String>::from_extractor(
234            &extractor
235                .field(1)
236                .ok_or_else(|| "Missing resource-name".to_string())?,
237        )?;
238        let expected_use = <u64>::from_extractor(
239            &extractor
240                .field(2)
241                .ok_or_else(|| "Missing expected-use".to_string())?,
242        )?;
243        let last_credit = <i64>::from_extractor(
244            &extractor
245                .field(3)
246                .ok_or_else(|| "Missing last-credit".to_string())?,
247        )?;
248        let last_credit_at = <Datetime>::from_extractor(
249            &extractor
250                .field(4)
251                .ok_or_else(|| "Missing last-credit-at".to_string())?,
252        )?;
253        let record = types::QuotaTokenRecord {
254            environment_id,
255            resource_name,
256            expected_use,
257            last_credit,
258            last_credit_at,
259        };
260        Ok(QuotaToken::from_record(&record))
261    }
262}