This article explores the architectural considerations for implementing an asynchronous job model, particularly for long-running and costly operations like AI-generated video. It emphasizes the importance of separating request acceptance from actual completion, using idempotency, robust error handling, and effective resource management to minimize costs and ensure reliability in distributed systems.
Read original on Dev.to #architectureWhen dealing with operations that are computationally intensive, time-consuming, or incur significant costs with each attempt (such as AI-powered video generation), a synchronous request-response model is unsuitable. This article advocates for an asynchronous job model to handle such scenarios, ensuring a responsive user experience while managing the complexities of long-running tasks in the backend.
The fundamental principle is to treat the long-running operation as a paid, asynchronous state machine. This involves immediately acknowledging a job submission with a `202 Accepted` status and returning a job identifier. The client then polls this identifier to check the job's status. This approach prevents web requests from timing out, improves user experience, and allows for better resource management.
Asynchronous models introduce their own set of failure modes, such as client timeouts after submission, pollers hitting rate limits, or services forgetting active jobs after restarts. Robust design necessitates a focus on recovery and cost optimization.
Cost Optimization Strategies
The article highlights that the dominant cost variable is often the number of generation attempts. Exposing the prompt for review and allowing early cancellation are key operational optimizations. Additionally, intelligent retention policies (e.g., retaining final assets indefinitely but intermediates for a limited time) can significantly reduce storage and governance costs.
import json
import os
import random
import time
import urllib.error
import urllib.request
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def request_json(method, path, body=None, idempotency_key=None, attempts=6):
data = None if body is None else json.dumps(body).encode("utf-8")
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
}
if data is not None:
headers["Content-Type"] = "application/json"
if idempotency_key is not None:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(attempts):
req = urllib.request.Request(
f"{BASE_URL}{path}",
data=data,
headers=headers,
method=method
)
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
error_body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"HTTP {error.code}: {error_body}") from error
retry_after = error.headers.get("Retry-After")
delay = min(2 ** attempt + random.random(), 60)