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::api::oplog::OplogIndex;
16use crate::bindings::golem::durability::durability::{
17    CustomDurableInvocation, DurableFunctionType as RawDurableFunctionType,
18    LiveCustomDurableInvocation, OplogEntryVersion as RawOplogEntryVersion,
19    PersistedDurableFunctionInvocation, begin_custom_durable_invocation, observe_function_call,
20};
21use crate::schema::{FromSchema, IntoSchema, IntoTypedSchemaValue};
22use std::fmt::Display;
23use std::future::Future;
24use std::marker::PhantomData;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoSchema, FromSchema)]
27pub enum DurableFunctionType {
28    ReadLocal,
29    WriteLocal,
30    ReadRemote,
31    WriteRemote,
32    WriteRemoteBatched(Option<OplogIndex>),
33    WriteRemoteTransaction(Option<OplogIndex>),
34}
35
36impl From<DurableFunctionType> for RawDurableFunctionType {
37    fn from(value: DurableFunctionType) -> Self {
38        match value {
39            DurableFunctionType::ReadLocal => Self::ReadLocal,
40            DurableFunctionType::WriteLocal => Self::WriteLocal,
41            DurableFunctionType::ReadRemote => Self::ReadRemote,
42            DurableFunctionType::WriteRemote => Self::WriteRemote,
43            DurableFunctionType::WriteRemoteBatched(index) => Self::WriteRemoteBatched(index),
44            DurableFunctionType::WriteRemoteTransaction(index) => {
45                Self::WriteRemoteTransaction(index)
46            }
47        }
48    }
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoSchema, FromSchema)]
52pub enum OplogEntryVersion {
53    V1,
54    V2,
55}
56
57impl From<RawOplogEntryVersion> for OplogEntryVersion {
58    fn from(value: RawOplogEntryVersion) -> Self {
59        match value {
60            RawOplogEntryVersion::V1 => Self::V1,
61            RawOplogEntryVersion::V2 => Self::V2,
62        }
63    }
64}
65
66#[must_use = "a durability invocation must be run to completion"]
67pub struct Durability<SOk, SErr> {
68    interface: &'static str,
69    function: &'static str,
70    invocation: CustomDurableInvocation,
71    forced_commit: bool,
72    _sok: PhantomData<SOk>,
73    _serr: PhantomData<SErr>,
74}
75
76impl<SOk, SErr> Durability<SOk, SErr> {
77    pub fn new<SIn>(
78        interface: &'static str,
79        function: &'static str,
80        function_type: DurableFunctionType,
81        input: &SIn,
82    ) -> Self
83    where
84        SIn: Clone + IntoSchema,
85    {
86        observe_function_call(interface, function);
87        let function_type = RawDurableFunctionType::from(function_type);
88        let function_name = if interface.is_empty() {
89            function.to_string()
90        } else {
91            format!("{interface}::{function}")
92        };
93        let request = input
94            .clone()
95            .into_typed_schema_value()
96            .unwrap_or_else(|err| panic!("Failed serializing durable function input: {err}"));
97        let request = crate::encode_typed_schema_value(&request)
98            .unwrap_or_else(|err| panic!("Failed encoding durable function input: {err}"));
99        let invocation = begin_custom_durable_invocation(&function_name, request, function_type);
100
101        Self {
102            interface,
103            function,
104            invocation,
105            forced_commit: false,
106            _sok: PhantomData,
107            _serr: PhantomData,
108        }
109    }
110
111    pub fn enable_forced_commit(&mut self) {
112        self.forced_commit = true;
113    }
114
115    pub fn with_forced_commit(mut self) -> Self {
116        self.forced_commit = true;
117        self
118    }
119
120    pub fn run<Ok, Err, F>(self, body: F) -> Result<Ok, Err>
121    where
122        F: FnOnce() -> Result<Ok, Err>,
123        Ok: Clone + From<SOk>,
124        Err: From<SErr>,
125        SErr: FromSchema + for<'a> From<&'a Err>,
126        SOk: FromSchema + From<Ok>,
127        Result<SOk, SErr>: IntoSchema,
128    {
129        match self.invocation {
130            CustomDurableInvocation::Live(invocation) => {
131                let result = body();
132                let serializable_result = result
133                    .as_ref()
134                    .map(|result| result.clone().into())
135                    .map_err(|err| err.into());
136                Self::finish(invocation, serializable_result, self.forced_commit);
137                result
138            }
139            CustomDurableInvocation::Replayed(ref invocation) => self
140                .replay_serializable(invocation)
141                .map(Into::into)
142                .map_err(Into::into),
143        }
144    }
145
146    pub async fn run_async<Ok, Err, F, Fut>(self, body: F) -> Result<Ok, Err>
147    where
148        F: FnOnce() -> Fut,
149        Fut: Future<Output = Result<Ok, Err>>,
150        Ok: Clone + From<SOk>,
151        Err: From<SErr>,
152        SErr: FromSchema + for<'a> From<&'a Err>,
153        SOk: FromSchema + From<Ok>,
154        Result<SOk, SErr>: IntoSchema,
155    {
156        match self.invocation {
157            CustomDurableInvocation::Live(invocation) => {
158                let result = body().await;
159                let serializable_result = result
160                    .as_ref()
161                    .map(|result| result.clone().into())
162                    .map_err(|err| err.into());
163                Self::finish(invocation, serializable_result, self.forced_commit);
164                result
165            }
166            CustomDurableInvocation::Replayed(ref invocation) => self
167                .replay_serializable(invocation)
168                .map(Into::into)
169                .map_err(Into::into),
170        }
171    }
172
173    pub fn run_infallible<Ok, F>(self, body: F) -> Ok
174    where
175        F: FnOnce() -> Ok,
176        Ok: Clone + From<SOk>,
177        SOk: FromSchema + From<Ok>,
178        SErr: FromSchema + Display,
179        Result<SOk, SErr>: IntoSchema,
180    {
181        match self.invocation {
182            CustomDurableInvocation::Live(invocation) => {
183                let result = body();
184                let serializable_result = Ok(result.clone().into());
185                Self::finish(invocation, serializable_result, self.forced_commit);
186                result
187            }
188            CustomDurableInvocation::Replayed(ref invocation) => {
189                match self.replay_serializable(invocation) {
190                    Ok(result) => result.into(),
191                    Err(err) => panic!(
192                        "Function {} previously failed with {}",
193                        self.function_name(),
194                        err
195                    ),
196                }
197            }
198        }
199    }
200
201    pub async fn run_infallible_async<Ok, F, Fut>(self, body: F) -> Ok
202    where
203        F: FnOnce() -> Fut,
204        Fut: Future<Output = Ok>,
205        Ok: Clone + From<SOk>,
206        SOk: FromSchema + From<Ok>,
207        SErr: FromSchema + Display,
208        Result<SOk, SErr>: IntoSchema,
209    {
210        match self.invocation {
211            CustomDurableInvocation::Live(invocation) => {
212                let result = body().await;
213                let serializable_result = Ok(result.clone().into());
214                Self::finish(invocation, serializable_result, self.forced_commit);
215                result
216            }
217            CustomDurableInvocation::Replayed(ref invocation) => {
218                match self.replay_serializable(invocation) {
219                    Ok(result) => result.into(),
220                    Err(err) => panic!(
221                        "Function {} previously failed with {}",
222                        self.function_name(),
223                        err
224                    ),
225                }
226            }
227        }
228    }
229
230    fn finish(
231        invocation: LiveCustomDurableInvocation,
232        result: Result<SOk, SErr>,
233        forced_commit: bool,
234    ) where
235        Result<SOk, SErr>: IntoSchema,
236    {
237        let response = result
238            .into_typed_schema_value()
239            .unwrap_or_else(|err| panic!("Failed serializing durable function result: {err}"));
240        let response = crate::encode_typed_schema_value(&response)
241            .unwrap_or_else(|err| panic!("Failed encoding durable function result: {err}"));
242        LiveCustomDurableInvocation::finish(invocation, response, forced_commit);
243    }
244
245    fn replay_serializable(
246        &self,
247        invocation: &PersistedDurableFunctionInvocation,
248    ) -> Result<SOk, SErr>
249    where
250        SOk: FromSchema,
251        SErr: FromSchema,
252    {
253        Self::validate_oplog_entry(invocation, &self.function_name());
254        let response = crate::decode_typed_schema_value(&invocation.response)
255            .unwrap_or_else(|err| panic!("Failed decoding durable function response: {err}"));
256        FromSchema::from_value(response.value())
257            .unwrap_or_else(|err| panic!("Unexpected durable function payload: {err}"))
258    }
259
260    fn function_name(&self) -> String {
261        if self.interface.is_empty() {
262            // For backward compatibility - some of the recorded function names were not following the pattern
263            self.function.to_string()
264        } else {
265            format!("{}::{}", self.interface, self.function)
266        }
267    }
268
269    fn validate_oplog_entry(
270        oplog_entry: &PersistedDurableFunctionInvocation,
271        expected_function_name: &str,
272    ) {
273        if oplog_entry.function_name != expected_function_name {
274            panic!(
275                "Unexpected imported function call entry in oplog: expected {}, got {}",
276                expected_function_name, oplog_entry.function_name
277            );
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use crate::durability::DurableFunctionType;
285    use crate::{FromSchema, IntoSchema};
286    use std::io::Error;
287
288    // This is not an actual runnable test - with no host implementation - but verifies through
289    // an example that the Durability API is usable.
290    #[allow(dead_code)]
291    fn durability_interface_test() {
292        #[derive(Debug, IntoSchema, FromSchema)]
293        enum CustomError {
294            Error1,
295            Error2,
296        }
297
298        impl From<&std::io::Error> for CustomError {
299            fn from(_value: &Error) -> Self {
300                Self::Error1
301            }
302        }
303
304        impl From<CustomError> for std::io::Error {
305            fn from(value: CustomError) -> Self {
306                Error::other(format!("{value:?}"))
307            }
308        }
309
310        fn durable_fn() -> Result<u64, std::io::Error> {
311            let input = "input".to_string();
312            super::Durability::<u64, CustomError>::new(
313                "custom",
314                "random-number-generator",
315                DurableFunctionType::ReadLocal,
316                &input,
317            )
318            .run(|| Ok(1234))
319        }
320    }
321}