Skip to main content

golem_rust/
durability.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
15use crate::bindings::golem::durability::durability::{
16    CustomDurableInvocation, CustomInvocationContext,
17    DurableFunctionType as RawDurableFunctionType, LiveCustomDurableInvocation,
18    OplogEntryVersion as RawOplogEntryVersion, PersistedDurableFunctionInvocation,
19    begin_custom_durable_invocation, begin_custom_durable_invocation_async,
20    exchange_custom_invocation_context, observe_function_call,
21};
22use crate::schema::{FromSchema, IntoSchema, IntoTypedSchemaValue};
23use std::fmt::Display;
24use std::future::Future;
25use std::marker::PhantomData;
26use std::pin::Pin;
27use std::task::{Context, Poll};
28
29struct ScopedDurableFuture<F> {
30    inner: Option<Pin<Box<F>>>,
31    context: Option<CustomInvocationContext>,
32}
33
34impl<F> ScopedDurableFuture<F> {
35    fn new(inner: F) -> Self {
36        Self {
37            inner: Some(Box::pin(inner)),
38            context: None,
39        }
40    }
41}
42
43// Also saves the outgoing context on unwind, so subsequent future destruction can enter it.
44// wasm32-wasip2 uses panic=abort by default and does not run unwind cleanup.
45struct RestoreCustomContext<'a> {
46    caller: Option<CustomInvocationContext>,
47    suspended: &'a mut Option<CustomInvocationContext>,
48}
49
50impl Drop for RestoreCustomContext<'_> {
51    fn drop(&mut self) {
52        *self.suspended = Some(exchange_custom_invocation_context(self.caller.take()));
53    }
54}
55
56impl<F: Future> Future for ScopedDurableFuture<F> {
57    type Output = F::Output;
58
59    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
60        let this = self.get_mut();
61        let caller = exchange_custom_invocation_context(this.context.take());
62        let restore = RestoreCustomContext {
63            caller: Some(caller),
64            suspended: &mut this.context,
65        };
66        let result = this
67            .inner
68            .as_mut()
69            .expect("polled after completion")
70            .as_mut()
71            .poll(cx);
72        if result.is_ready() {
73            drop(this.inner.take());
74        }
75        drop(restore);
76        if result.is_ready() {
77            drop(this.context.take());
78        }
79        result
80    }
81}
82
83impl<F> Drop for ScopedDurableFuture<F> {
84    fn drop(&mut self) {
85        if let Some(context) = self.context.take() {
86            let caller = exchange_custom_invocation_context(Some(context));
87            let mut discarded = None;
88            let restore = RestoreCustomContext {
89                caller: Some(caller),
90                suspended: &mut discarded,
91            };
92            drop(self.inner.take());
93            drop(restore);
94            drop(discarded);
95        } else {
96            drop(self.inner.take());
97        }
98    }
99}
100
101struct LiveDurableBody<F: Future> {
102    future: Option<Pin<Box<F>>>,
103    result: Option<F::Output>,
104    invocation: Option<LiveCustomDurableInvocation>,
105}
106
107impl<F: Future> Drop for LiveDurableBody<F> {
108    fn drop(&mut self) {
109        // Undelivered results and descendants must be destroyed before their enclosing scope.
110        drop(self.future.take());
111        drop(self.result.take());
112        drop(self.invocation.take());
113    }
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoSchema, FromSchema)]
117pub enum DurableFunctionType {
118    ReadLocal,
119    WriteLocal,
120    ReadRemote,
121    WriteRemote,
122}
123
124impl From<DurableFunctionType> for RawDurableFunctionType {
125    fn from(value: DurableFunctionType) -> Self {
126        match value {
127            DurableFunctionType::ReadLocal => Self::ReadLocal,
128            DurableFunctionType::WriteLocal => Self::WriteLocal,
129            DurableFunctionType::ReadRemote => Self::ReadRemote,
130            DurableFunctionType::WriteRemote => Self::WriteRemote,
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoSchema, FromSchema)]
136pub enum OplogEntryVersion {
137    V1,
138    V2,
139}
140
141impl From<RawOplogEntryVersion> for OplogEntryVersion {
142    fn from(value: RawOplogEntryVersion) -> Self {
143        match value {
144            RawOplogEntryVersion::V1 => Self::V1,
145            RawOplogEntryVersion::V2 => Self::V2,
146        }
147    }
148}
149
150/// Prepares a custom durable operation without beginning it.
151///
152/// Synchronous runners begin when called. Async runners begin, including call observation,
153/// on their first poll and install their execution scope only while being polled or dropped.
154/// An unpolled async runner performs no custom invocation. Nested operations must complete or
155/// be dropped before the enclosing body returns; detached work does not inherit this scope.
156#[must_use = "a durability invocation must be run to completion"]
157pub struct Durability<SOk, SErr> {
158    interface: &'static str,
159    function: &'static str,
160    request: Option<crate::schema::wit::wire::TypedSchemaValue>,
161    function_type: RawDurableFunctionType,
162    forced_commit: bool,
163    _sok: PhantomData<SOk>,
164    _serr: PhantomData<SErr>,
165}
166
167impl<SOk, SErr> Durability<SOk, SErr> {
168    pub fn new<SIn>(
169        interface: &'static str,
170        function: &'static str,
171        function_type: DurableFunctionType,
172        input: &SIn,
173    ) -> Self
174    where
175        SIn: Clone + IntoSchema,
176    {
177        let function_type = RawDurableFunctionType::from(function_type);
178        let request = input
179            .clone()
180            .into_typed_schema_value()
181            .unwrap_or_else(|err| panic!("Failed serializing durable function input: {err}"));
182        let request = crate::encode_typed_schema_value(&request)
183            .unwrap_or_else(|err| panic!("Failed encoding durable function input: {err}"));
184        Self {
185            interface,
186            function,
187            request: Some(request),
188            function_type,
189            forced_commit: false,
190            _sok: PhantomData,
191            _serr: PhantomData,
192        }
193    }
194
195    pub fn enable_forced_commit(&mut self) {
196        self.forced_commit = true;
197    }
198
199    pub fn with_forced_commit(mut self) -> Self {
200        self.forced_commit = true;
201        self
202    }
203
204    pub fn run<Ok, Err, F>(mut self, body: F) -> Result<Ok, Err>
205    where
206        F: FnOnce() -> Result<Ok, Err>,
207        Ok: Clone + From<SOk>,
208        Err: From<SErr>,
209        SErr: FromSchema + for<'a> From<&'a Err>,
210        SOk: FromSchema + From<Ok>,
211        Result<SOk, SErr>: IntoSchema,
212    {
213        match self.begin() {
214            CustomDurableInvocation::Live(invocation) => {
215                let result = body();
216                let serializable_result = result
217                    .as_ref()
218                    .map(|result| result.clone().into())
219                    .map_err(|err| err.into());
220                Self::finish(invocation, serializable_result, self.forced_commit);
221                result
222            }
223            CustomDurableInvocation::Replayed(ref invocation) => self
224                .replay_serializable(invocation)
225                .map(Into::into)
226                .map_err(Into::into),
227        }
228    }
229
230    pub async fn run_async<Ok, Err, F, Fut>(mut self, body: F) -> Result<Ok, Err>
231    where
232        F: FnOnce() -> Fut,
233        Fut: Future<Output = Result<Ok, Err>>,
234        Ok: Clone + From<SOk>,
235        Err: From<SErr>,
236        SErr: FromSchema + for<'a> From<&'a Err>,
237        SOk: FromSchema + From<Ok>,
238        Result<SOk, SErr>: IntoSchema,
239    {
240        match self.begin_async().await {
241            CustomDurableInvocation::Live(invocation) => {
242                ScopedDurableFuture::new(async move {
243                    invocation.enter();
244                    let mut live = LiveDurableBody {
245                        future: None,
246                        result: None,
247                        invocation: Some(invocation),
248                    };
249                    live.future = Some(Box::pin(body()));
250                    live.result = Some(live.future.as_mut().unwrap().as_mut().await);
251                    drop(live.future.take());
252                    let serializable_result = live
253                        .result
254                        .as_ref()
255                        .unwrap()
256                        .as_ref()
257                        .map(|result| result.clone().into())
258                        .map_err(|err| err.into());
259                    let response = Self::encode_response(serializable_result);
260                    LiveCustomDurableInvocation::finish_async(
261                        live.invocation.as_ref().unwrap(),
262                        response,
263                        self.forced_commit,
264                    )
265                    .await;
266                    live.result.take().unwrap()
267                })
268                .await
269            }
270            CustomDurableInvocation::Replayed(ref invocation) => self
271                .replay_serializable(invocation)
272                .map(Into::into)
273                .map_err(Into::into),
274        }
275    }
276
277    pub fn run_infallible<Ok, F>(mut self, body: F) -> Ok
278    where
279        F: FnOnce() -> Ok,
280        Ok: Clone + From<SOk>,
281        SOk: FromSchema + From<Ok>,
282        SErr: FromSchema + Display,
283        Result<SOk, SErr>: IntoSchema,
284    {
285        match self.begin() {
286            CustomDurableInvocation::Live(invocation) => {
287                let result = body();
288                let serializable_result = Ok(result.clone().into());
289                Self::finish(invocation, serializable_result, self.forced_commit);
290                result
291            }
292            CustomDurableInvocation::Replayed(ref invocation) => {
293                match self.replay_serializable(invocation) {
294                    Ok(result) => result.into(),
295                    Err(err) => panic!(
296                        "Function {} previously failed with {}",
297                        self.function_name(),
298                        err
299                    ),
300                }
301            }
302        }
303    }
304
305    pub async fn run_infallible_async<Ok, F, Fut>(mut self, body: F) -> Ok
306    where
307        F: FnOnce() -> Fut,
308        Fut: Future<Output = Ok>,
309        Ok: Clone + From<SOk>,
310        SOk: FromSchema + From<Ok>,
311        SErr: FromSchema + Display,
312        Result<SOk, SErr>: IntoSchema,
313    {
314        match self.begin_async().await {
315            CustomDurableInvocation::Live(invocation) => {
316                ScopedDurableFuture::new(async move {
317                    invocation.enter();
318                    let mut live = LiveDurableBody {
319                        future: None,
320                        result: None,
321                        invocation: Some(invocation),
322                    };
323                    live.future = Some(Box::pin(body()));
324                    live.result = Some(live.future.as_mut().unwrap().as_mut().await);
325                    drop(live.future.take());
326                    let response =
327                        Self::encode_response(Ok(live.result.as_ref().unwrap().clone().into()));
328                    LiveCustomDurableInvocation::finish_async(
329                        live.invocation.as_ref().unwrap(),
330                        response,
331                        self.forced_commit,
332                    )
333                    .await;
334                    live.result.take().unwrap()
335                })
336                .await
337            }
338            CustomDurableInvocation::Replayed(ref invocation) => {
339                match self.replay_serializable(invocation) {
340                    Ok(result) => result.into(),
341                    Err(err) => panic!(
342                        "Function {} previously failed with {}",
343                        self.function_name(),
344                        err
345                    ),
346                }
347            }
348        }
349    }
350
351    fn begin(&mut self) -> CustomDurableInvocation {
352        observe_function_call(self.interface, self.function);
353        begin_custom_durable_invocation(
354            &self.function_name(),
355            self.request.take().unwrap(),
356            self.function_type,
357        )
358    }
359
360    async fn begin_async(&mut self) -> CustomDurableInvocation {
361        observe_function_call(self.interface, self.function);
362        begin_custom_durable_invocation_async(
363            self.function_name(),
364            self.request.take().unwrap(),
365            self.function_type,
366        )
367        .await
368    }
369
370    fn encode_response(result: Result<SOk, SErr>) -> crate::schema::wit::wire::TypedSchemaValue
371    where
372        Result<SOk, SErr>: IntoSchema,
373    {
374        let response = result
375            .into_typed_schema_value()
376            .unwrap_or_else(|err| panic!("Failed serializing durable function result: {err}"));
377        crate::encode_typed_schema_value(&response)
378            .unwrap_or_else(|err| panic!("Failed encoding durable function result: {err}"))
379    }
380
381    fn finish(
382        invocation: LiveCustomDurableInvocation,
383        result: Result<SOk, SErr>,
384        forced_commit: bool,
385    ) where
386        Result<SOk, SErr>: IntoSchema,
387    {
388        let response = Self::encode_response(result);
389        LiveCustomDurableInvocation::finish(invocation, response, forced_commit);
390    }
391
392    fn replay_serializable(
393        &self,
394        invocation: &PersistedDurableFunctionInvocation,
395    ) -> Result<SOk, SErr>
396    where
397        SOk: FromSchema,
398        SErr: FromSchema,
399    {
400        Self::validate_oplog_entry(invocation, &self.function_name());
401        let response = crate::decode_typed_schema_value(&invocation.response)
402            .unwrap_or_else(|err| panic!("Failed decoding durable function response: {err}"));
403        FromSchema::from_value(response.value())
404            .unwrap_or_else(|err| panic!("Unexpected durable function payload: {err}"))
405    }
406
407    fn function_name(&self) -> String {
408        if self.interface.is_empty() {
409            // For backward compatibility - some of the recorded function names were not following the pattern
410            self.function.to_string()
411        } else {
412            format!("{}::{}", self.interface, self.function)
413        }
414    }
415
416    fn validate_oplog_entry(
417        oplog_entry: &PersistedDurableFunctionInvocation,
418        expected_function_name: &str,
419    ) {
420        if oplog_entry.function_name != expected_function_name {
421            panic!(
422                "Unexpected imported function call entry in oplog: expected {}, got {}",
423                expected_function_name, oplog_entry.function_name
424            );
425        }
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use crate::durability::DurableFunctionType;
432    use crate::{FromSchema, IntoSchema};
433    use std::io::Error;
434
435    // This is not an actual runnable test - with no host implementation - but verifies through
436    // an example that the Durability API is usable.
437    #[allow(dead_code)]
438    fn durability_interface_test() {
439        #[derive(Debug, IntoSchema, FromSchema)]
440        enum CustomError {
441            Error1,
442            Error2,
443        }
444
445        impl From<&std::io::Error> for CustomError {
446            fn from(_value: &Error) -> Self {
447                Self::Error1
448            }
449        }
450
451        impl From<CustomError> for std::io::Error {
452            fn from(value: CustomError) -> Self {
453                Error::other(format!("{value:?}"))
454            }
455        }
456
457        fn durable_fn() -> Result<u64, std::io::Error> {
458            let input = "input".to_string();
459            super::Durability::<u64, CustomError>::new(
460                "custom",
461                "random-number-generator",
462                DurableFunctionType::ReadLocal,
463                &input,
464            )
465            .run(|| Ok(1234))
466        }
467    }
468}