Worker status polling (#385)

Changes deployments so that worker status is polled instead of waiting
in a single request to reduce risk of proxy timeouts
This commit is contained in:
Sterling Dreyer 2025-05-09 10:43:39 -07:00 committed by GitHub
parent 479d137e62
commit c128717e53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 67 additions and 39 deletions

View file

@ -4,6 +4,7 @@ import os
import re import re
import secrets import secrets
import tarfile import tarfile
import time
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -13,6 +14,10 @@ from arcadepy import Arcade, NotFoundError
from httpx import Client from httpx import Client
from packaging.requirements import Requirement from packaging.requirements import Requirement
from pydantic import BaseModel, field_serializer, field_validator, model_validator from pydantic import BaseModel, field_serializer, field_validator, model_validator
from rich.console import Console
from rich.table import Table
console = Console()
# Base class for versioned packages # Base class for versioned packages
@ -111,25 +116,49 @@ class Request(BaseModel):
pypi: Pypi | None = None pypi: Pypi | None = None
custom_repositories: list[PackageRepository] | None = None custom_repositories: list[PackageRepository] | None = None
local_packages: list[LocalPackage] | None = None local_packages: list[LocalPackage] | None = None
wait: bool = False
@field_serializer("secret") @field_serializer("secret")
def serialize_secret(self, secret: Secret) -> str: def serialize_secret(self, secret: Secret) -> str:
return secret.value return secret.value
def poll_worker_status(self, cloud_client: Client, worker_name: str) -> Any:
while True:
try:
worker_resp = cloud_client.get(
f"{cloud_client.base_url}/api/v1/workers/{worker_name}?wait_for_completion=true",
timeout=10,
)
worker_resp.raise_for_status()
except httpx.TimeoutException:
time.sleep(1)
continue
except httpx.ConnectError as e:
raise ValueError(f"Failed to connect to cloud: {e}")
except Exception as e:
raise ValueError(f"Failed to start worker: {e}")
status = worker_resp.json()["data"]["status"]
if status == "Running":
return worker_resp.json()["data"]
if status == "Failed":
raise ValueError(f"Worker failed to start: {worker_resp.json()['data']['error']}")
def execute(self, cloud_client: Client, engine_client: Arcade) -> Any: def execute(self, cloud_client: Client, engine_client: Arcade) -> Any:
# Attempt to deploy worker to the cloud # Attempt to deploy worker to the cloud
try: try:
cloud_response = cloud_client.put( cloud_response = cloud_client.put(
str(cloud_client.base_url) + "/api/v1/workers", str(cloud_client.base_url) + "/api/v1/workers",
json=self.model_dump(mode="json"), json=self.model_dump(mode="json"),
timeout=120, timeout=360,
) )
cloud_response.raise_for_status() cloud_response.raise_for_status()
except httpx.ConnectError as e: except httpx.ConnectError as e:
raise ValueError(f"Failed to connect to cloud: {e}") raise ValueError(f"Failed to connect to cloud: {e}")
except Exception: except Exception as e:
msg = cloud_response.json().get("msg", f"{cloud_response.status_code}: Unknown error") # change this so it handles errors that aren't just from cloud
raise ValueError(f"Failed to start worker: {msg}") raise ValueError(f"Failed to start worker: {e}")
parse_deployment_response(cloud_response.json())
worker_data = self.poll_worker_status(cloud_client, self.name)
try: try:
# Check if worker already exists # Check if worker already exists
@ -138,7 +167,7 @@ class Request(BaseModel):
id=self.name, id=self.name,
enabled=self.enabled, enabled=self.enabled,
http={ http={
"uri": cloud_response.json()["data"]["worker_endpoint"], "uri": worker_data["endpoint"],
"secret": self.secret.value, "secret": self.secret.value,
"timeout": self.timeout, "timeout": self.timeout,
"retry": self.retries, "retry": self.retries,
@ -150,7 +179,7 @@ class Request(BaseModel):
id=self.name, id=self.name,
enabled=self.enabled, enabled=self.enabled,
http={ http={
"uri": cloud_response.json()["data"]["worker_endpoint"], "uri": worker_data["endpoint"],
"secret": self.secret.value, "secret": self.secret.value,
"timeout": self.timeout, "timeout": self.timeout,
"retry": self.retries, "retry": self.retries,
@ -342,3 +371,33 @@ def get_env_secret(secret: str) -> Secret:
raise ValueError(f"Multiple environment variables found in secret: {secret}") raise ValueError(f"Multiple environment variables found in secret: {secret}")
# If no matches are found, return the secret as is # If no matches are found, return the secret as is
return Secret(value=secret, pattern=None) return Secret(value=secret, pattern=None)
def parse_deployment_response(response: dict) -> None:
# Check what changes were made to the worker and display
changes = response["data"]["changes"]
additions = changes.get("additions", [])
removals = changes.get("removals", [])
updates = changes.get("updates", [])
no_changes = changes.get("no_changes", [])
print_deployment_table(additions, removals, updates, no_changes)
def print_deployment_table(
additions: list, removals: list, updates: list, no_changes: list
) -> None:
table = Table(title="Changed Packages")
table.add_column("Adding", justify="right", style="green")
table.add_column("Removing", justify="right", style="red")
table.add_column("Updating", justify="right", style="yellow")
table.add_column("No Changes", justify="right", style="dim")
max_rows = max(len(additions), len(removals), len(updates), len(no_changes))
# Add each row of worker package changes to the table
for i in range(max_rows):
addition = additions[i] if i < len(additions) else ""
removal = removals[i] if i < len(removals) else ""
update = updates[i] if i < len(updates) else ""
no_change = no_changes[i] if i < len(no_changes) else ""
table.add_row(addition, removal, update, no_change)
console.print(table)

View file

@ -48,7 +48,6 @@ from arcade.cli.utils import (
validate_and_get_config, validate_and_get_config,
version_callback, version_callback,
) )
from arcade.cli.worker import parse_deployment_response
cli = typer.Typer( cli = typer.Typer(
cls=OrderCommands, cls=OrderCommands,
@ -631,8 +630,7 @@ def deploy(
console.log(f"Deploying '{worker.config.id}...'", style="dim") console.log(f"Deploying '{worker.config.id}...'", style="dim")
try: try:
# Attempt to deploy worker # Attempt to deploy worker
response = worker.request().execute(cloud_client, engine_client) worker.request().execute(cloud_client, engine_client)
parse_deployment_response(response)
console.log(f"✅ Worker '{worker.config.id}' deployed successfully.", style="dim") console.log(f"✅ Worker '{worker.config.id}' deployed successfully.", style="dim")
except Exception as e: except Exception as e:
console.log( console.log(

View file

@ -170,36 +170,6 @@ def compare_endpoints(worker_id: str, engine_endpoint: str, deployments: list[di
return engine_endpoint return engine_endpoint
def parse_deployment_response(response: dict) -> None:
# Check what changes were made to the worker and display
changes = response["data"]["changes"]
additions = changes.get("additions", [])
removals = changes.get("removals", [])
updates = changes.get("updates", [])
no_changes = changes.get("no_changes", [])
print_deployment_table(additions, removals, updates, no_changes)
def print_deployment_table(
additions: list, removals: list, updates: list, no_changes: list
) -> None:
table = Table(title="Changed Packages")
table.add_column("Added", justify="right", style="green")
table.add_column("Removed", justify="right", style="red")
table.add_column("Updated", justify="right", style="yellow")
table.add_column("No Changes", justify="right", style="dim")
max_rows = max(len(additions), len(removals), len(updates), len(no_changes))
# Add each row of worker package changes to the table
for i in range(max_rows):
addition = additions[i] if i < len(additions) else ""
removal = removals[i] if i < len(removals) else ""
update = updates[i] if i < len(updates) else ""
no_change = no_changes[i] if i < len(no_changes) else ""
table.add_row(addition, removal, update, no_change)
console.print(table)
@app.command("enable", help="Enable a worker") @app.command("enable", help="Enable a worker")
def enable_worker( def enable_worker(
worker_id: str, worker_id: str,

View file

@ -82,6 +82,7 @@ def test_deployment_dict(test_dir):
"enabled": true, "enabled": true,
"timeout": 10, "timeout": 10,
"retries": 3, "retries": 3,
"wait": false,
"pypi": { "pypi": {
"packages": [ "packages": [
{ {