Skip to main content

Resonate Python quickstart tutorial • Part 1

In the first part of this tutorial you will build a mock summarization service using the Resonate Python SDK and the FastAPI library. This part of the tutorial showcases how Resonate provides Durable Execution for application-level failures using the SDK's local promise storage.

Durable Execution for application-level failures means that you get Durable Promise resolution timeouts, transparent function execution retries, and retry rate limits without adding an additional supervisor service.

You will create an HTTP service with a single route handler. The handler will use resonate.run() to run the downloadAndSummarize() function. Resonate will supervise the executions of these functions, ensuring they execute to completion.

downloadAndSummarize application flow

If any of the functions throw an error or reject a promise, Resonate will automatically retry the function execution.

Prerequisites

This tutorial assumes that you have Python 3 and a package managere installed. This tutorial recommends using Rye as the package manager.

Set up the project

Create a project folder.

mkdir resonate-quickstart && cd resonate-quickstart

Initialize a new Rye project:

rye init summary --script

Sync the project:

rye sync

Install the dependencies:

rye add resonate-sdk fastapi uvicorn pydantic

Rye will automatically generate an init.py file and main.py file in the src/summarize directory. Delete the main.py file and remove all the lines from the init.py file.

Next, create an app.py file in the src/summarize directory and then copy and paste the minimal distributed async/await application below:

docs/get-started/python-quickstart/part-1/code/src/summarize/app.py

import random
import time

def downloadAndSummarize(ctx, url):
print("Downloading and summarizing content from", url)
# Download the content from the provided URL
content = yield ctx.lfc(download, url)
# Summarize the downloaded content
summary = yield ctx.lfc(summarize, content).with_options(promise_id="asdfsda")
# Return the summary
return summary

def download(_, url):
print(f"Downloading data from {url}")
time.sleep(2.5)
# Simulate a failure to download data 50% of the time
if random.randint(0, 100) > 50:
print("Download failed")
raise Exception("Failed to download data")
print("Download successful")
return "This is the text of the page that was downloaded"

def summarize(_, content):
print("Summarizing content...")
time.sleep(2.5)
if random.randint(0, 100) > 50:
print("Summarization failed")
raise Exception("Failed to summarize content")
print("Summarization successful")
return "This is the summary of the page that was downloaded"

The code that makes up app.py is a simple mock application that simulates downloading a page from the internet and then summarizing the content of that page. It represents the "business logic" of the service. You have your downloadAndSummarize() main function that orchestrates the steps of your application (download() then summarize()).

Both the download() and summarize() functions include a 2.5-second timeout to simulate the time required for downloading and summarizing content from a URL. These functions are invoked using ctx.lfc, which wraps the function call in a promise, adds it to the call graph, and introduces durability.

In this example, download() and summarize() are mock functions designed to “fail” 50% of the time. When they return a an error, Resonate automatically retries them until they succeed.

Next, create a file named gateway.ts and paste in the following FastAPI code that uses Resonate:

docs/get-started/python-quickstart/part-1/code/src/summarize/gateway.py

from resonate.scheduler import Scheduler
from resonate.storage import LocalPromiseStore
from resonate.retry_policy import Constant
from fastapi import FastAPI
from pydantic import BaseModel
from summarize.app import downloadAndSummarize
import uvicorn


app = FastAPI()

class SummarizeRequest(BaseModel):
url: str

class SummarizeResponse(BaseModel):
summary: str
error: str = None

# Define a route handler for the /summarize endpoint
@app.post('/summarize')
def summarize_route_handler(request: SummarizeRequest) -> SummarizeResponse:
try:
# Extract the URL from the request
url = request.url
# Summarize the text
promise = resonate.run(f"summarize-{url}", downloadAndSummarize, url=url)
return {'summary': promise.result()}
except Exception as e:
return {'error': str(e)}

# Create a Resonate Scheduler
resonate = Scheduler(durable_promise_storage=LocalPromiseStore())
# Register the downloadAndSummarize function with the Resonate scheduler
resonate.register(downloadAndSummarize)

# Define a main function to start the FastAPI app
def main():
uvicorn.run(app, host="127.0.0.1", port=5000)
print("Serving HTTP on port 5000...")

# Run the main function when the script is executed
if __name__ == '__main__':
main()

The gateway.ts file contains a FastAPI server that listens on port 5000 and includes a single route handler that leverages Resonate to run the downloadAndSummarize() function. To set it up, you instantiate a Resonate Scheduler, register the top-level function and start a FastAPI server.

When calling resonate.run(), you must provide the function name, a unique identifier, and the function’s arguments. The identifier serves as the Durable Promise ID for the function invocation. If you use the same identifier for multiple calls to resonate.run(), you will receive the result from the initial execution without re-running the function. To get a new result on each call, you need to supply a different promise ID.

Lastly, make sure to update your pyproject.toml file to includes the following scripts:

[project.scripts]
"app-node" = "summarize.gateway:main"

Run the application

To start your summarization service run rye run app-node.

rye run app-node

Then, from another terminal send a POST request to the /summarize endpoint.

curl -X POST http://localhost:5000/summarize -H "Content-Type: application/json" -d '{"url": "http://example.com"}'

Watch the log output of your service. There is a good chance that you will see either "download failed" or "summarization failed" or both one or more times.

However, even if you see those failures logged, eventually you should get the text response, "This is a summary of the text", back to where you made POST request.

After the request for example.com succeeds, try running it again. Notice how on subsequent requests with the same URL, Resonate does not start the executions again but returns the same root-level promise results immediately.

This is because the url makes up part of the root-level promise ID. The Resonate TypeScript SDK stores that promise locally and ensures that if the same ID is used in subsequent calls, the result of the execution associated with that promise is returned.

However, if you restart the service or provide a different url, the service will execute the functions again.

Up next

In the next part of the tutorial, you will connect to a Resonate Server which stores the promise remotely, so that even if your HTTP server crashes, the function executions will be able to eventually complete.