mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(startup): make Gateway readiness timeout configurable (#7309)
This commit is contained in:
parent
871d064f9e
commit
9e5dee318b
@ -30,6 +30,10 @@ UNBROWSE_API_KEY=your-unbrowse-api-key
|
||||
# Leave unset when using the unified nginx endpoint, e.g. http://localhost:2026.
|
||||
# GATEWAY_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
|
||||
# Local make dev/start Gateway readiness wait, in positive integer seconds (default: 30).
|
||||
# Increase for slow cold starts; does not change Docker health checks or request timeouts.
|
||||
# DEER_FLOW_GATEWAY_STARTUP_TIMEOUT=240
|
||||
|
||||
# Host interface the nginx entry port listens on, for the Docker stack and for
|
||||
# local `make dev` / `make start` (where the Gateway and frontend always stay on
|
||||
# loopback). Defaults to 127.0.0.1 (loopback only), matching the
|
||||
|
||||
13
README.md
13
README.md
@ -756,6 +756,19 @@ Local services always use their internal ports (`8001`, `3000`, and `2026`).
|
||||
The root `.env` variable `PORT` configures only the published Docker ingress;
|
||||
it does not change the Next.js port used by `make dev`.
|
||||
|
||||
Local startup waits up to 30 seconds for the Gateway port. On a slow cold start,
|
||||
set a longer wait without editing the launcher:
|
||||
|
||||
```bash
|
||||
DEER_FLOW_GATEWAY_STARTUP_TIMEOUT=240 make dev
|
||||
```
|
||||
|
||||
The value must be a positive integer in seconds. You can also set it in the
|
||||
root `.env`, whose value takes precedence over the shell. It applies to local
|
||||
`make dev`, `make start`, and their daemon variants, not Docker health checks
|
||||
or LLM request timeouts. Check `logs/gateway.log` for startup errors before
|
||||
increasing the wait; an expired deadline still stops the services.
|
||||
|
||||
#### Startup Modes
|
||||
|
||||
DeerFlow runs the agent runtime inside the Gateway API. Development mode enables hot-reload; production mode uses a pre-built frontend.
|
||||
|
||||
148
backend/tests/test_serve_gateway_startup_timeout.py
Normal file
148
backend/tests/test_serve_gateway_startup_timeout.py
Normal file
@ -0,0 +1,148 @@
|
||||
"""Run the launcher and real port-wait loop without starting application services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from support.shell import require_script_bash
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# Stub process launches and readiness/time, retaining serve.sh's admission,
|
||||
# timeout forwarding, failure diagnostics and cleanup, plus the real wait loop.
|
||||
_PRELUDE = r"""
|
||||
git() { return 1; }
|
||||
pgrep() { return 1; }
|
||||
nginx() { return 0; }
|
||||
ss() { return 1; }
|
||||
netstat() { return 1; }
|
||||
powershell.exe() { return 1; }
|
||||
lsof() {
|
||||
case "$*" in
|
||||
*-iTCP:8001*)
|
||||
if [ -f "$TEST_STARTED" ] && [ "$(cat "$TEST_CLOCK")" -ge "$TEST_READY_AT" ]; then
|
||||
echo 12345
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
sleep() { printf '%s\n' "$(( $(cat "$TEST_CLOCK") + 1 ))" > "$TEST_CLOCK"; }
|
||||
python3() { "$TEST_PYTHON" "$@"; }
|
||||
env() {
|
||||
if [ "$1" = python3 ]; then shift; python3 "$@"; else command env "$@"; fi
|
||||
}
|
||||
sh() {
|
||||
case "$*" in
|
||||
*uvicorn*)
|
||||
printf 'Gateway is still initializing\n' > logs/gateway.log
|
||||
touch "$TEST_STARTED"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
nohup() { sh "$@"; }
|
||||
bash() {
|
||||
if [ "$1" = ./scripts/wait-for-port.sh ] && [ "$2" != 8001 ]; then return 0; fi
|
||||
command "$TEST_BASH" "$@"
|
||||
}
|
||||
export -f lsof sleep ss netstat powershell.exe
|
||||
source "$@"
|
||||
"""
|
||||
|
||||
|
||||
def _run_launcher(tmp_path: Path, *, timeout: str | None, ready_at: int = 145, args: tuple[str, ...] = ("--dev",), dotenv: bool = False) -> subprocess.CompletedProcess[str]:
|
||||
checkout = tmp_path / "checkout"
|
||||
scripts = checkout / "scripts"
|
||||
scripts.mkdir(parents=True)
|
||||
(checkout / "backend").mkdir()
|
||||
for name in ("serve.sh", "wait-for-port.sh", "detect_uv_extras.py"):
|
||||
shutil.copy2(REPO_ROOT / "scripts" / name, scripts / name)
|
||||
(scripts / "nginx-local-conf.sh").write_text("printf 'test-nginx.conf\\n'\n", encoding="utf-8", newline="\n")
|
||||
(scripts / "config-upgrade.sh").write_text("exit 0\n", encoding="utf-8", newline="\n")
|
||||
(scripts / "cleanup-containers.sh").write_text('printf "cleanup\\n" >> "$TEST_CLEANUP"\n', encoding="utf-8", newline="\n")
|
||||
(checkout / "config.yaml").write_text("models: []\n", encoding="utf-8", newline="\n")
|
||||
(tmp_path / "clock").write_text("0\n", encoding="utf-8", newline="\n")
|
||||
bash = require_script_bash()
|
||||
env = {key: value for key, value in os.environ.items() if key not in {"BASH_ENV", "ENV", "DEER_FLOW_GATEWAY_STARTUP_TIMEOUT", "DEER_FLOW_ENV_FILE"}}
|
||||
env.update(
|
||||
TEST_BASH=bash,
|
||||
TEST_PYTHON=Path(sys.executable).as_posix(),
|
||||
TEST_CLOCK=(tmp_path / "clock").as_posix(),
|
||||
TEST_STARTED=(tmp_path / "started").as_posix(),
|
||||
TEST_CLEANUP=(tmp_path / "cleanup").as_posix(),
|
||||
TEST_READY_AT=str(ready_at),
|
||||
DEER_FLOW_HOME=(tmp_path / "runtime").as_posix(),
|
||||
DEER_FLOW_PROJECT_ROOT=checkout.as_posix(),
|
||||
DEER_FLOW_CONFIG_PATH=(checkout / "config.yaml").as_posix(),
|
||||
)
|
||||
if timeout is not None:
|
||||
if dotenv:
|
||||
(checkout / ".env").write_text(f"DEER_FLOW_GATEWAY_STARTUP_TIMEOUT={timeout}\n", encoding="utf-8", newline="\n")
|
||||
else:
|
||||
env["DEER_FLOW_GATEWAY_STARTUP_TIMEOUT"] = timeout
|
||||
return subprocess.run(
|
||||
[bash, "-c", _PRELUDE, "startup-timeout-test", (scripts / "serve.sh").as_posix(), "--skip-install", *args],
|
||||
cwd=checkout,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=20,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [("--dev",), ("--prod",), ("--dev", "--daemon")])
|
||||
def test_configured_gateway_timeout_allows_slow_startup(tmp_path: Path, args: tuple[str, ...]) -> None:
|
||||
result = _run_launcher(tmp_path, timeout="240", args=args)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "Gateway started on localhost:8001" in result.stdout
|
||||
|
||||
|
||||
def test_gateway_timeout_can_be_loaded_from_dotenv(tmp_path: Path) -> None:
|
||||
result = _run_launcher(tmp_path, timeout="240", dotenv=True)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
|
||||
|
||||
def test_default_gateway_timeout_accepts_a_fast_startup(tmp_path: Path) -> None:
|
||||
result = _run_launcher(tmp_path, timeout=None, ready_at=5)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
|
||||
|
||||
def test_default_gateway_timeout_remains_bounded(tmp_path: Path) -> None:
|
||||
result = _run_launcher(tmp_path, timeout=None)
|
||||
assert result.returncode == 1, result.stdout + result.stderr
|
||||
assert "after 30s" in result.stdout
|
||||
assert "Gateway is still initializing" in result.stdout
|
||||
assert "DEER_FLOW_GATEWAY_STARTUP_TIMEOUT" in result.stdout
|
||||
assert (tmp_path / "cleanup").read_text(encoding="utf-8").splitlines() == ["cleanup", "cleanup"]
|
||||
|
||||
|
||||
def test_configured_gateway_timeout_still_times_out_and_cleans_up(tmp_path: Path) -> None:
|
||||
result = _run_launcher(tmp_path, timeout="40")
|
||||
assert result.returncode == 1, result.stdout + result.stderr
|
||||
assert "after 40s" in result.stdout
|
||||
assert (tmp_path / "cleanup").read_text(encoding="utf-8").splitlines() == ["cleanup", "cleanup"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["", "0", "-1", "1.5", "abc", "9999999999999999999999999999999"])
|
||||
@pytest.mark.parametrize("action", ["--dev", "--restart"])
|
||||
def test_invalid_gateway_timeout_does_not_stop_services(tmp_path: Path, value: str, action: str) -> None:
|
||||
result = _run_launcher(tmp_path, timeout=value, args=(action,))
|
||||
assert result.returncode == 1, result.stdout + result.stderr
|
||||
assert "DEER_FLOW_GATEWAY_STARTUP_TIMEOUT must be a positive integer" in result.stdout + result.stderr
|
||||
assert not (tmp_path / "cleanup").exists()
|
||||
assert not (tmp_path / "started").exists()
|
||||
|
||||
|
||||
def test_invalid_gateway_timeout_does_not_block_stop(tmp_path: Path) -> None:
|
||||
result = _run_launcher(tmp_path, timeout="invalid", args=("--stop",))
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert (tmp_path / "cleanup").exists()
|
||||
assert not (tmp_path / "started").exists()
|
||||
@ -51,6 +51,13 @@ trimming its contents. Offline CLI coverage:
|
||||
|
||||
## Service Startup Contracts
|
||||
|
||||
`serve.sh` uses `DEER_FLOW_GATEWAY_STARTUP_TIMEOUT` for the local Gateway's
|
||||
port-readiness wait (positive integer seconds, default 30). Read it after the
|
||||
root `.env`, and reject invalid values before any service cleanup or startup;
|
||||
`--stop` bypasses this startup-only validation. Keep frontend/nginx deadlines
|
||||
unchanged. `backend/tests/test_serve_gateway_startup_timeout.py` runs the full
|
||||
launcher and real wait loop with process launches, readiness and time stubbed.
|
||||
|
||||
Both launchers call `sandbox-mode.sh` via Bash; keep it Bash/awk-only.
|
||||
Dev passes `$PROJECT_ROOT/config.yaml`; prod passes `$DEER_FLOW_CONFIG_PATH`.
|
||||
For AIO, omitted/blank/empty-quoted/YAML-null URLs (including comments) select
|
||||
|
||||
@ -297,6 +297,11 @@ fi
|
||||
# not tear down a running stack. Gateway and frontend always bind loopback;
|
||||
# nginx is the only entry point, as in the Docker stack.
|
||||
if [ "$ACTION" != "stop" ]; then
|
||||
GATEWAY_STARTUP_TIMEOUT="${DEER_FLOW_GATEWAY_STARTUP_TIMEOUT-30}"
|
||||
if [[ ! "$GATEWAY_STARTUP_TIMEOUT" =~ ^[0-9]+$ ]] || ! [ "$GATEWAY_STARTUP_TIMEOUT" -gt 0 ] 2>/dev/null; then
|
||||
echo "✗ DEER_FLOW_GATEWAY_STARTUP_TIMEOUT must be a positive integer (seconds)." >&2
|
||||
exit 1
|
||||
fi
|
||||
NGINX_CONF="$(bash "$REPO_ROOT/scripts/nginx-local-conf.sh")" || exit 1
|
||||
fi
|
||||
|
||||
@ -494,6 +499,10 @@ run_service() {
|
||||
local logfile="logs/$(echo "$name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-').log"
|
||||
echo "✗ $name failed to start."
|
||||
[ -f "$logfile" ] && tail -20 "$logfile"
|
||||
if [ "$name" = "Gateway" ]; then
|
||||
echo " Check logs/gateway.log for startup errors. If Gateway is still initializing,"
|
||||
echo " increase DEER_FLOW_GATEWAY_STARTUP_TIMEOUT (seconds; current: $GATEWAY_STARTUP_TIMEOUT)."
|
||||
fi
|
||||
cleanup 1
|
||||
}
|
||||
echo "✓ $name started on localhost:$port"
|
||||
@ -507,7 +516,7 @@ mkdir -p temp/client_body_temp temp/proxy_temp temp/fastcgi_temp temp/uwsgi_temp
|
||||
# 1. Gateway API
|
||||
run_service "Gateway" \
|
||||
"cd backend && PYTHONPATH=. uv run --no-sync uvicorn app.gateway.app:app --host 127.0.0.1 --port 8001 $GATEWAY_EXTRA_FLAGS > ../logs/gateway.log 2>&1" \
|
||||
8001 30
|
||||
8001 "$GATEWAY_STARTUP_TIMEOUT"
|
||||
|
||||
# 2. Frontend
|
||||
run_service "Frontend" \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user