Durable Promises
What is a Durable Promise?
Durable promises are like regular promises but they persist in storage. They have a unique identity that lives beyond the execution of the underlying function execution. By doing persisting promises, if something goes wrong (like a power outage or network hiccup), the application can look back at the saved results, quickly skip to where it left off, and keep going as if nothing ever happened, making your application resilient to unexpected failures.
This also enables functions in different processes to call and await on each other. See Resonate Call Graphs for learn more about how Resonate facilitates the execution of distributed functions in a single business process.
Storage
Where are Durable Promises stored?
There are two places where a Durable Promise can be stored, locally or remotely.
Local storage
“Local storage, sometimes called "Local mode", stores Durable Promises in local memory. There are multiple ways to visualize this dynamic.
The following is a component diagram of an Application Node running in local mode:”
The Application Node's local memory stores all Durable Promises.
You can visualize this dynamic in another way through a sequence diagram:
In the diagram above, promise 1 corresponding to the invocation of function 1 is stored in the local memory where the process is hosted.
Local mode still provides function-level recovery. In other words, if a called function fails (throws an error or rejects a promise) in local mode, it is retried within the bounds of the timeout budget.
The following psuedocode example uses local storage.
The main function (f1()
) synchronously awaits on two other functions (f2()
and f3()
).
fn f1(ctx: Context) {
await ctx.run(f2);
await ctx.run(f3);
return;
}
fn f2() {
// ...
return;
}
fn f3() {
// ...
return;
}
await resonate.run("f1", `your-proimise-id`);
This sequence graph could be visualized like the following:
The next level of detail could include the promises, which might look like this:
Notice that this sequence graphs above do not include the Resonate Server (supervisor).
If all the functions are meant to execute locally, within the same process, an Application Node may chose to rely soley on a local promise store.
However, if you want f1()
, f2()
, or f3()
to be resumable even if the process crashes, or the Application Node functions need to await on functions in other Application Nodes, then use Remote mode.
When to use
Local storage is ideal for starting out with Resonate. It enables you to integrate Distributed Async Await into a single Application Node without the overhead of running the Resonate Server.
It also serves use cases where there is a requirement of very low latency on single Application Nodes that do not need to make any Remote Function Calls and are not concerned with the Application Node restarting the execution from scratch should it crash, but you still want individual functions to be transparently retried if they fail.
Part 1 of the quickstart tutorials showcases this functionality:
Remote storage
Remote storage, sometimes called "Remote mode", stores all Durable Promises in the Resonate Server (the supervisor service). Again, there are multiple ways to visualize this dynamic.
The following is a component diagram of an Application Node running in remote mode:
By default, the Resonate Server stores all Durable Promises in a database. Currently, to view all the promises stored in the Resonate Server, you can use the Resonate CLI.
durable=false
Depending on the requirements of the application, a function invocation (LFI) can choose to pass durable=false
, bypassing the creation and storage of a Durable Promise.
This is useful when the result of the function is not needed for recovery or when the function is idempotent and can be retried without side effects and extremely low latency is required.
You can visualize this dynamic in another way through a sequence diagram:
In the diagram above, promise 1 corresponding to the invocation of function 1 is stored in the Resonate Server.
Consider the same psuedocode example as above:
fn f1(ctx: Context) {
await ctx.run(f2);
await ctx.run(f3);
return;
}
fn f2() {
// ...
return;
}
fn f3() {
// ...
return;
}
await resonate.run("f1", `your-proimise-id`);
With Remote storage, the sequence would look like the following:
This sequence shows what happens "under the hood" when a Resonate Server (supervisor) is used.
Using Remote storage enables two things:
- It enables the "distribution" aspect of Distributed Async Await.
- It enables platform-level recovery.
In regards to platform-level recovery, for example when your (Application Node) crashes, Remote storage can ensure that when your process comes back up, any "in-progress" executions continue from where they left off. It also enables the Call Graph to recover on a completely different Application Node.
Part 2 of the quickstart tutorials showcases this functionality:
Now let's look at the distribution aspect that is enabled by Remote storage.
The remote use case involves a Durable Promise that is created by one process and completed by another distinct process. The primary purpose is to facilitate coordination between different processes or services, serving as the foundation for features like:
-
Task framework: Durable promises allow you to distribute tasks across multiple machines for parallel execution and collect the operations’ results.
-
Notifications: When a durable promise is created or completed, it can trigger notifications to other processes or services that are interested in the result, enabling efficient communication and coordination.
-
Human in the Loop: Durable promises can seamlessly integrate human input into your automated workflows, allowing for manual intervention or approval steps when needed.