Skip to main content

Distributed Async Await

Distributed Async Await is a powerful programming model provided by Resonate through its SDK and server infrastructure. This model is built on top of an Asynchronous Remote Procedure Call (RPC) system, enabling seamless distributed execution of functions and promises across different nodes in a network. At its core, the model focuses on resilience, coordination, and durability, ensuring that processes can persist and resume even after unexpected failures.

Durable Execution - beyond reliability​

A key feature of Distributed Async Await is Durable Execution—the ability of computations to resume after process crashes. This abstraction ensures that the progress of a function can be resumed, providing a resilient environment for cloud-based applications.

However, it’s essential not to think of Distributed Async Await merely in terms of reliability, failure tolerance, or failure transparency. While these are valuable attributes, they are not the essence of the model. Instead, Distributed Async Await offers a comprehensive approach to cloud programming, designed to provide a delightful developer experience and empower developers to understand and reason about their system’s behavior.

Synchronous vs Asynchronous Execution​

A key distinction in Distributed Async Await is the comparison between synchronous and asynchronous invocations. In a synchronous function call, the caller waits for the function to complete before moving on. With asynchronous invocations, the function is suspended and the execution moves forward, allowing other tasks to be processed while waiting for the promise to resolve.

Sync vs Async Await vs Async Promise Await

Programming Mode - functions and promises​

At the heart of the Distributed Async Await model are functions and promises.

Function Promise Relationship

Functions are the fundamental units of computation.

Function state machine

Promises represent future values, acting as the primary coordination mechanism between different function executions.

Promise state machine

In this model, functions execute in sequences, with each step marked by an await statement. The execution continues until it reaches an await expression, where it pauses until the promise resolves. Once the promise is resolved, the function resumes execution with the promise’s value or throws an error if the promise is rejected.

This relationship between functions and promises forms the backbone of Resonate’s programming model. Invocation and Await are two crucial relationships that tie downstream and upstream executions together, ensuring that one execution can only continue once its dependent executions have completed.

Invocation and Await Relationship

Durable Promises and asynchronous invocations​

Distributed Async Await extends traditional function invocations with the concept of Durable Promises—promises that persist their identity and state in storage. This ensures that function executions can resume, even after disruptions. The following types of function calls, LFIs, RFIs, and Detached, illustrate how this model works in practice.

Local Function Invocation​

A Local Function Invocation, also known as an LFI, is when a function executes on the same machine or node where it was invoked. The promise associated with the LFI stores in local memory or in the Resonate Server, and the function will not return until the promise resolves. LFIs are ideal for workflows that rely on resources exclusive to the local environment.

An LFI can start a new Call Graph, creating a top-level promise, or it can extend a Call Graph. resonate.run() is an LFI that creates a top-level promise, creating a new Call Graph.

Resonate SDKs provide both an LFI and an LFC method for invoking functions. The LFI method provides a promise which can be awaited on later for getting the result of the invoked function. The following code example demonstrates the use of the LFI method.

def foo(ctx : Context):

p : Promise = yield ctx.lfi(bar)
# Do other work
v : Value = yield p

The LFC method is syntactic sugar for LFI and is used to invoke a function and immediately await the result. The following example demonstrates the use of the LFC method.

def foo(ctx : Context):

v : Value = yield ctx.lfc(bar)

Remote Function Invocation​

A Remote Function Call, also known as an RFI, is executed on a different machine, requiring the Application Node to be connected to a Resonate Server. In this case, the promise is stored on the server, which also manages the routing of the function execution to the appropriate node. Like LFIs, RFIs ensure the parent function doesn’t return until the promise resolves, but the actual execution occurs remotely.

An RFI extends the Call Graph to a different Application Node.

Resonate SDKs provide both an RFI and an RFC method for invoking functions. The RFI method provides a promise which can be awaited on later for getting the result of the invoked function. The following code example demonstrates the use of the RFI method.

def foo(ctx : Context):

p : Promise = yield ctx.rfi(bar)
# Do other work
v : Value = yield p

The RFC method is syntactic sugar for RFI and is used to invoke a function and immediately await the result. The following example demonstrates the use of the RFC method.

def foo(ctx : Context):

v : Value = yield ctx.rfc(bar)

Detached Function Call​

A Detached Function Call is an LFI that creates a top-level promise, similar to resonate.run(), creating a new Call Graph. However, the distinction is that with a Detached Function Call, the parent function can return without waiting for the promise associated with the detached function to resolve. This behavior resembles a background job.

This is useful for tasks that do not need immediate results, allowing the Application Node to continue processing without waiting. The following code example demonstrates the use of the Detached Function Call.

def foo(ctx : Context):

yield ctx.detached(bar)
# Do other work

How Distributed Async Await works​

Understanding how Distributed Async Await functions mechanically is simpler than grasping its broader purpose. Here’s an outline of how the process works:

Distributed Async Await interleaves function executions with save points. These save points allow the function to resume from where it left off in the event of a failure or crash. This ensures that no computation is lost, and the system can pick up execution without starting over.

The save points are the Durable Promises.

If a function execution terminates prematurely, the runtime restarts the execution using the save points (Durable Promises) to rebuild the state. By leveraging the persistence of Durable Promises, the system avoids repeating any already completed tasks, reducing redundancy and ensuring efficient recovery.

This behavior ensures that even in the event of system failures, long-running or critical processes can continue without losing progress.