fix: Wait for healthy actor in arcade dev (#142)

Context: Currently, `arcade dev` starts the actor process and then waits
a hardcoded amount of time (2sec) for the actor to start up. This isn't
enough time on some slower machines, which leads to the engine trying to
start but failing.

Fix: Wait until the actor is healthy according to its own
`/actor/health` endpoint.
This commit is contained in:
Nate Barbettini 2024-11-04 09:39:09 -08:00 committed by GitHub
parent 8dbfed5c52
commit 39d342bd67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 8 deletions

View file

@ -1,9 +1,11 @@
import http.client
import io import io
import ipaddress import ipaddress
import logging import logging
import os import os
import shutil import shutil
import signal import signal
import socket
import subprocess import subprocess
import sys import sys
import threading import threading
@ -28,8 +30,8 @@ if os.environ.get("HOMEBREW_REPOSITORY") is not None:
def start_servers( def start_servers(
host: str, actor_host: str,
port: int, actor_port: int,
engine_config: str | None, engine_config: str | None,
engine_env: str | None = None, engine_env: str | None = None,
debug: bool = False, debug: bool = False,
@ -45,8 +47,8 @@ def start_servers(
debug: Whether to run in debug mode. debug: Whether to run in debug mode.
""" """
# Validate host and port # Validate host and port
host = _validate_host(host) actor_host = _validate_host(actor_host)
port = _validate_port(port) actor_port = _validate_port(actor_port)
# Ensure engine_config is provided and validated # Ensure engine_config is provided and validated
engine_config = _get_config_file(engine_config, default_filename="engine.yaml") engine_config = _get_config_file(engine_config, default_filename="engine.yaml")
@ -55,13 +57,13 @@ def start_servers(
env_file = _get_config_file(engine_env, default_filename="arcade.env", optional=True) env_file = _get_config_file(engine_env, default_filename="arcade.env", optional=True)
# Prepare command-line arguments for the actor server and engine # Prepare command-line arguments for the actor server and engine
actor_cmd = _build_actor_command(host, port, debug) actor_cmd = _build_actor_command(actor_host, actor_port, debug)
# even if the user didn't pass an env file we may have found it in the default locations # even if the user didn't pass an env file we may have found it in the default locations
engine_cmd = _build_engine_command(engine_config, engine_env=env_file if env_file else None) engine_cmd = _build_engine_command(engine_config, engine_env=env_file if env_file else None)
# Start and manage the processes # Start and manage the processes
_manage_processes(actor_cmd, engine_cmd, debug=debug) _manage_processes(actor_cmd, actor_host, actor_port, engine_cmd, debug=debug)
def _validate_host(host: str) -> str: def _validate_host(host: str) -> str:
@ -239,6 +241,8 @@ def _build_engine_command(engine_config: str | None, engine_env: str | None = No
def _manage_processes( def _manage_processes(
actor_cmd: list[str], actor_cmd: list[str],
actor_host: str,
actor_port: int,
engine_cmd: list[str], engine_cmd: list[str],
engine_env: dict[str, str] | None = None, engine_env: dict[str, str] | None = None,
debug: bool = False, debug: bool = False,
@ -273,8 +277,7 @@ def _manage_processes(
console.print("Starting actor server...", style="bold green") console.print("Starting actor server...", style="bold green")
actor_process = _start_process("Actor", actor_cmd, debug=debug) actor_process = _start_process("Actor", actor_cmd, debug=debug)
# Wait a bit to ensure actor is up _wait_for_healthy_actor(actor_process, actor_host, actor_port)
time.sleep(2)
# Start the engine # Start the engine
console.print("Starting engine...", style="bold green") console.print("Starting engine...", style="bold green")
@ -356,6 +359,28 @@ def _start_process(
raise RuntimeError(f"Failed to start {name}") raise RuntimeError(f"Failed to start {name}")
def _wait_for_healthy_actor(
actor_process: subprocess.Popen, actor_host: str, actor_port: int
) -> None:
"""Wait until an HTTP request to `host:port/actor/health` returns 200"""
while not actor_process.poll(): # Stop waiting if the actor process has exited
time.sleep(1)
try:
conn = http.client.HTTPConnection(actor_host, actor_port, timeout=1)
conn.request("GET", "/actor/health")
res = conn.getresponse()
if res.status == 200:
break
conn.close()
except (socket.gaierror, http.client.HTTPException, ConnectionRefusedError, TimeoutError):
pass # Handle expected exceptions gracefully
console.print("Waiting for actor to start...", style="bold yellow")
time.sleep(1) # Wait just a little longer for everything to settle (discovered experimentally)
console.print("Actor is healthy", style="bold green")
def _stream_output(process: subprocess.Popen, name: str) -> None: def _stream_output(process: subprocess.Popen, name: str) -> None:
""" """
Streams the output from a subprocess to the console. Streams the output from a subprocess to the console.

View file

@ -118,6 +118,14 @@ class Config(BaseConfig):
config_data = yaml.safe_load(config_file_path.read_text()) config_data = yaml.safe_load(config_file_path.read_text())
if config_data is None:
raise ValueError(
"Invalid credentials.yaml file. Please ensure it is a valid YAML file."
)
if "cloud" not in config_data:
raise ValueError("Invalid credentials.yaml file. Expected a 'cloud' key.")
try: try:
return cls(**config_data["cloud"]) return cls(**config_data["cloud"])
except ValidationError as e: except ValidationError as e: