Resonate Python SDK
How does the Resonate Python SDK work.
The Resonate Python SDK is designed to offer developers an idiomatic Python experience, while providing the necessary abstractions to build Distributed Async Await applications. Central to the SDK is the concept of the Application Node, which functions through an Event Loop. This loop consists of three critical layers: the Application, the Scheduler, and the Processor, across two system spaces: Application and Kernel.
System spaces​
The system is split into two primary spaces:
- Application space: Where developers write their code and interact with the SDK.
- Kernel space: Consistenting of the Scheduler, and the Processor, this is where orchestration happens, including coroutine management, promise resolution, and function execution. Developers generally don’t need to worry about this space, as the SDK handles the advancement of coroutines and resolves Promises automatically.
Architecture overview​
In the Resonate Python SDK, the Event Loop drives the execution flow of an Application Node, dividing responsibilities among three layers:
- Application Layer (multi-threaded): This is where developers write their business logic. For instance, if an Application Node is a web server receiving multiple requests, each request is processed concurrently in a separate thread.
- Scheduler (single-threaded): Orchestrates the execution of functions and coroutines, ensuring that the multi-threaded Application Layer can execute efficiently without blocking.
- Processor (multi-threaded): Executes functions received from the Scheduler in parallel, allowing multiple operations to proceed simultaneously.
Execution flow​
Developers write normal Python functions or coroutines and then use the Resonate SDK to yield one of three things:
- A Local Function Invocation (LFI)
- A Remote Function Invocation (RFI)
- The result of a promise
Whenever you yield an LFI or RFI, the SDK returns a promise. The Developer can then choose where to await on the result of that promise.
Interaction between Components:
- Application Layer: Developers enqueue coroutines via the
resonate.run()
orctx.lfi()
methods. Both of these are considered LFIs. When adding a coroutine, a Promise[T] is immediately returned, which can be awaited later for the result. For RFIs, the invocation requests are sent to the Resonate Server. From there the Server sends the incovation to the appropriate Application Node, which then triggers the Event Loop on that Application Node. - Scheduler: The Scheduler orchestrates the concurrent execution of coroutines. It doesn’t run functions directly but forwards them to the Processor for execution.
- Processor: Functions forwarded by the Scheduler are executed in parallel, thanks to the multi-threaded nature of the Processor. Results are then placed into a completion queue (CQ), which the Scheduler monitors.
- Scheduler (again): When the Scheduler retrieves results from the CQ, it resolves the corresponding Promises, allowing the waiting coroutines to proceed with execution.
- Application Layer: Once the Promises are resolved, components of the Application Layer that are awaiting those results can continue.
User perspective​
From a user’s perspective, working with the SDK is simple:
- You write coroutines that yield an LFI, rFI, or Promise.
- When you yield a local or remote function, the SDK returns a Promise.
- You then await the result of that Promise.
This approach allows for efficient handling of asynchronous operations, enabling developers to focus on writing business logic without worrying about the underlying orchestration.
Scheduler and Processor behavior​
The Scheduler is responsible for managing two types of tasks:
- Runnables: Functions and coroutines that are ready to make progress
- Awaitables: Coroutines that are waiting for the resolution of a Promise.
The Scheduler processes runnables until it has none left, at which point it enters a sleep state. It wakes up when new information is received—either from the a new Application layer thread or from something being added to the Completion Queue.
The Processor, which is multi-threaded and configurable in the SDK, executes functions on separate threads. This ensures that long-running functions don’t block the advancement of coroutines. When a function completes, its result is placed into the Completion Queue, waking up the Scheduler to resolve the associated Promise.
Invocation deduplication​
Resonate’s Scheduler also supports deduplication of executions. Each invocation correlates to a Promise ID, and if the same ID is invoked multiple times, the SDK ensures that subsequent invocations receive the same Promise, preventing redundant execution.
This feature is particularly useful in scenarios where a user wants to make a synchronous request that takes hours to complete. The first endpoint triggers the execution and immediately responds with a new endpoint for retrieving the result. Subsequent calls with the same ID return the same Promise, providing the result once the execution completes.
Practical considerations​
When writing functions that take a long time to execute, it’s essential to yield the function into the coroutine rather than calling it directly. This ensures that the function doesn’t block other coroutines.
For example, do not use time.sleep()
in a coroutine, as it blocks the entire Scheduler thread.
You can use time.sleep()
in functions, but be aware that it will block the entire Processor thread on which it executes.
Resonate recommends using yield ctx.rfi(ctx.sleep)
instead of time.sleep()
to avoid blocking either the Scheduler or Processor threads.