From f1263b0848efaa832e64c002f6b0ce3798e5b26a Mon Sep 17 00:00:00 2001 From: oliver Date: Sun, 2 Aug 2026 18:20:13 +0800 Subject: [PATCH] Start API and collector worker together from one-click scripts. Default start_netx.ps1/sh now set NETX_RUN_INLINE_SCHEDULERS=false and launch netx_api.worker; stop scripts tear the worker down with the API. Co-authored-by: Cursor --- .env.example | 6 ++-- netx_api/runtime_budget.py | 11 ++++--- scripts/start_netx.ps1 | 62 +++++++++++++++++++++++++++++++++++--- scripts/start_netx.sh | 41 +++++++++++++++++++++++-- scripts/stop_netx.ps1 | 22 ++++++++++++-- scripts/stop_netx.sh | 9 ++++++ 6 files changed, 135 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 6b8bbe9..6bb4706 100644 --- a/.env.example +++ b/.env.example @@ -84,5 +84,7 @@ NETX_UME_NOTIFICATION_TOPIC=ALARM # NETX_UME_RAW_JSON_MAX_BYTES=65536 # NETX_NE_COLLECTION_KEEP_DAYS=14 # Heavier fleets: raise CLI/DB together; also ensure Postgres max_connections and bastion session limits. -# One-click start keeps collectors inline: .\scripts\start_netx.ps1 -Background -WithWeb -# (do not require a separate worker process unless you opt out of inline schedulers) +# One-click start (recommended): .\scripts\start_netx.ps1 -Background -WithWeb +# -> API with NETX_RUN_INLINE_SCHEDULERS=false + auto-started netx_api.worker + optional Vite +# Legacy single-process: add -InlineSchedulers +# Stop all: .\scripts\stop_netx.ps1 diff --git a/netx_api/runtime_budget.py b/netx_api/runtime_budget.py index 9b003e3..28b4e79 100644 --- a/netx_api/runtime_budget.py +++ b/netx_api/runtime_budget.py @@ -51,8 +51,11 @@ def log_runtime_budget(*, role: str = "api") -> None: inline = bool(getattr(settings, "run_inline_schedulers", True)) if inline: _log.info( - "runtime budget: inline schedulers ON (one-click start via scripts/start_netx.ps1). " - "Optional split: NETX_RUN_INLINE_SCHEDULERS=false + python -m netx_api.worker" + "runtime budget: inline schedulers ON " + "(legacy single-process; start scripts default to API+worker split)" + ) + else: + _log.info( + "runtime budget: external worker mode " + "(start_netx.ps1/sh auto-starts python -m netx_api.worker)" ) - elif host not in {"127.0.0.1", "localhost", "::1"}: - _log.info("runtime budget: external worker mode on bind=%s", host) diff --git a/scripts/start_netx.ps1 b/scripts/start_netx.ps1 index 7e966b1..ab6852d 100644 --- a/scripts/start_netx.ps1 +++ b/scripts/start_netx.ps1 @@ -4,7 +4,9 @@ param( [int]$WebPort = 5173, [switch]$SkipInstall = $false, [switch]$Background = $false, - [switch]$WithWeb = $false + [switch]$WithWeb = $false, + # Keep collectors inside the API process (legacy). Default: split API + worker. + [switch]$InlineSchedulers = $false ) $ErrorActionPreference = "Stop" @@ -20,6 +22,9 @@ if (-not (Test-Path $runDir)) { $pidFile = Join-Path $runDir "netx.pid" $logFile = Join-Path $runDir "netx.out.log" $errFile = Join-Path $runDir "netx.err.log" +$workerPidFile = Join-Path $runDir "worker.pid" +$workerLogFile = Join-Path $runDir "worker.out.log" +$workerErrFile = Join-Path $runDir "worker.err.log" $webPidFile = Join-Path $runDir "web.pid" $webLogFile = Join-Path $runDir "web.out.log" $webErrFile = Join-Path $runDir "web.err.log" @@ -66,6 +71,13 @@ $env:NETX_HOST = $BindHost $env:NETX_PORT = "$Port" # netx_api is a source tree (not always pip -e installed); ensure imports work from any launcher cwd. $env:PYTHONPATH = $projectRoot +# Split collectors from API so HTTP/WebCRT stay responsive under multi-user load. +# Override with -InlineSchedulers to keep legacy single-process mode. +if ($InlineSchedulers) { + $env:NETX_RUN_INLINE_SCHEDULERS = "true" +} else { + $env:NETX_RUN_INLINE_SCHEDULERS = "false" +} $baseUrl = "http://$BindHost`:$Port" $webUrl = "http://$BindHost`:$WebPort" @@ -76,6 +88,11 @@ Write-Host "Health: $baseUrl/health" Write-Host "Ready: $baseUrl/health/ready" Write-Host "Metrics: $baseUrl/metrics" Write-Host "Integrations: $baseUrl/v1/integrations/status" +if (-not $InlineSchedulers) { + Write-Host "Schedulers: external worker (auto-started by this script)" +} else { + Write-Host "Schedulers: inline with API (-InlineSchedulers)" +} if ($WithWeb) { Write-Host "" Write-Host "==> netx UI URL" @@ -114,11 +131,45 @@ function Test-NetxApiListening { return $false } +function Start-NetxWorker { + if ($InlineSchedulers) { + return + } + Set-Content -Path $workerLogFile -Value "" -Encoding utf8 + Set-Content -Path $workerErrFile -Value "" -Encoding utf8 + Write-Host "==> Starting netx worker (config_sync / lldp / port_traffic)" + $workerProc = Start-Process -FilePath $pythonExe ` + -ArgumentList @("-m", "netx_api.worker") ` + -WorkingDirectory $projectRoot ` + -WindowStyle Hidden ` + -RedirectStandardOutput $workerLogFile ` + -RedirectStandardError $workerErrFile ` + -PassThru + Set-Content -Path $workerPidFile -Value "$($workerProc.Id)" + Write-Host "worker.pid = $workerPidFile" + Write-Host "PID = $($workerProc.Id)" + Write-Host "Log = $workerLogFile" + Write-Host "Err = $workerErrFile" + Start-Sleep -Seconds 1 + $alive = $false + try { + $alive = -not $workerProc.HasExited + } catch { + $alive = $false + } + if (-not $alive) { + Write-Host "[ERR] netx worker exited immediately (PID $($workerProc.Id))." -ForegroundColor Red + Show-LogTail -Path $workerErrFile + Show-LogTail -Path $workerLogFile + exit 1 + } +} + if ($Background) { # Truncate logs so a failed start is not confused with an old run. Set-Content -Path $logFile -Value "" -Encoding utf8 Set-Content -Path $errFile -Value "" -Encoding utf8 - Write-Host "==> Starting netx in background" + Write-Host "==> Starting netx API in background" $proc = Start-Process -FilePath $pythonExe ` -ArgumentList @("-m", "netx_api.main") ` -WorkingDirectory $projectRoot ` @@ -152,6 +203,7 @@ if ($Background) { exit 1 } Write-Host "==> netx API ready: http://${BindHost}:${Port}/health" -ForegroundColor Green + Start-NetxWorker if ($WithWeb) { Write-Host "==> Starting Vite dev server in background" $webRoot = Join-Path $projectRoot "web" @@ -169,7 +221,7 @@ if ($Background) { Write-Host "Err = $webErrFile" } Write-Host "" - Write-Host "==> Background services started; this script exits (API/web keep running)." -ForegroundColor Cyan + Write-Host "==> Background services started; this script exits (API/worker/web keep running)." -ForegroundColor Cyan exit 0 } @@ -190,6 +242,6 @@ if ($WithWeb) { Write-Host "Err = $webErrFile" } -Write-Host "==> Starting netx in foreground" +Start-NetxWorker +Write-Host "==> Starting netx API in foreground" & $pythonExe -m netx_api.main - diff --git a/scripts/start_netx.sh b/scripts/start_netx.sh index dd76856..9d19866 100644 --- a/scripts/start_netx.sh +++ b/scripts/start_netx.sh @@ -22,10 +22,13 @@ Options: --web-host Web bind host. Default: 0.0.0.0 --web-port Web port. Default: 8505 --api-only Start API only (no Node/npm on this host) + --inline-schedulers Keep collectors inside API (no worker process) --skip-install Skip pip/npm install steps -h, --help Show help Notes: + - By default starts API + worker (config_sync / lldp / port_traffic) + optional web. + - Sets NETX_RUN_INLINE_SCHEDULERS=false unless --inline-schedulers is passed. - Runs with system/host Python (no .venv). - Web uses isolated Node at /usr/local/nodejs/node-v24.16.0-linux-x64 (not system PATH). - Override with NODE_CMD / --node-cmd if install path differs. @@ -94,6 +97,9 @@ mkdir -p "${RUN_DIR}" PID_FILE="${RUN_DIR}/netx.pid" LOG_FILE="${RUN_DIR}/netx.out.log" ERR_FILE="${RUN_DIR}/netx.err.log" +WORKER_PID_FILE="${RUN_DIR}/worker.pid" +WORKER_LOG_FILE="${RUN_DIR}/worker.out.log" +WORKER_ERR_FILE="${RUN_DIR}/worker.err.log" WEB_PID_FILE="${RUN_DIR}/web.pid" WEB_LOG_FILE="${RUN_DIR}/web.out.log" WEB_ERR_FILE="${RUN_DIR}/web.err.log" @@ -106,6 +112,7 @@ WEB_BIND_HOST="0.0.0.0" WEB_PORT="8505" SKIP_INSTALL="0" API_ONLY="0" +INLINE_SCHEDULERS="0" while [[ $# -gt 0 ]]; do case "$1" in @@ -117,6 +124,7 @@ while [[ $# -gt 0 ]]; do --web-host) WEB_BIND_HOST="${2:-}"; shift 2 ;; --web-port) WEB_PORT="${2:-}"; shift 2 ;; --api-only) API_ONLY="1"; shift ;; + --inline-schedulers) INLINE_SCHEDULERS="1"; shift ;; --skip-install) SKIP_INSTALL="1"; shift ;; -h|--help) usage; exit 0 ;; *) echo "[ERR] Unknown arg: $1" >&2; usage; exit 2 ;; @@ -139,6 +147,12 @@ fi export NETX_HOST="${API_BIND_HOST}" export NETX_PORT="${API_PORT}" +export PYTHONPATH="${ROOT_DIR}${PYTHONPATH:+:${PYTHONPATH}}" +if [[ "${INLINE_SCHEDULERS}" == "1" ]]; then + export NETX_RUN_INLINE_SCHEDULERS="true" +else + export NETX_RUN_INLINE_SCHEDULERS="false" +fi if [[ ! -x "${PYTHON_CMD}" ]]; then echo "[ERR] python_not_found: ${PYTHON_CMD}" >&2 @@ -183,7 +197,14 @@ echo "" echo "==> netx API URL" echo "Base: ${BASE_URL}/" echo "Health: ${BASE_URL}/health" +echo "Ready: ${BASE_URL}/health/ready" +echo "Metrics: ${BASE_URL}/metrics" echo "Integrations: ${BASE_URL}/v1/integrations/status" +if [[ "${INLINE_SCHEDULERS}" == "1" ]]; then + echo "Schedulers: inline with API (--inline-schedulers)" +else + echo "Schedulers: external worker (auto-started by this script)" +fi if [[ "${API_ONLY}" == "1" ]]; then echo "" echo "==> Web: skipped (--api-only). Serve web/dist via Nginx or run dev on another host." @@ -206,6 +227,20 @@ echo "PID = ${API_PID}" echo "Log = ${LOG_FILE}" echo "Err = ${ERR_FILE}" +if [[ "${INLINE_SCHEDULERS}" != "1" ]]; then + echo "==> Starting netx worker (config_sync / lldp / port_traffic)" + ( + cd "${ROOT_DIR}" + nohup "${PYTHON_CMD}" -m netx_api.worker >"${WORKER_LOG_FILE}" 2>"${WORKER_ERR_FILE}" & + echo $! > "${WORKER_PID_FILE}" + ) + WORKER_PID="$(cat "${WORKER_PID_FILE}")" + echo "worker.pid = ${WORKER_PID_FILE}" + echo "PID = ${WORKER_PID}" + echo "Log = ${WORKER_LOG_FILE}" + echo "Err = ${WORKER_ERR_FILE}" +fi + if [[ "${API_ONLY}" != "1" ]]; then echo "==> Starting Vite dev server in background" ( @@ -223,7 +258,7 @@ fi echo "" if [[ "${API_ONLY}" == "1" ]]; then - echo "==> API started (--api-only)." + echo "==> API(+worker) started (--api-only)." else - echo "==> Background services started; API/web keep running." -fi + echo "==> Background services started; API/worker/web keep running." +fi \ No newline at end of file diff --git a/scripts/stop_netx.ps1 b/scripts/stop_netx.ps1 index edf7699..8b4fbaa 100644 --- a/scripts/stop_netx.ps1 +++ b/scripts/stop_netx.ps1 @@ -8,6 +8,7 @@ $ErrorActionPreference = "Continue" $runDir = Join-Path $PSScriptRoot ".run" $pidFile = Join-Path $runDir "netx.pid" +$workerPidFile = Join-Path $runDir "worker.pid" $webPidFile = Join-Path $runDir "web.pid" function Get-ListenPids { @@ -48,14 +49,31 @@ if (Test-Path $pidFile) { if ($procId -gt 0) { try { Stop-Process -Id $procId -Force:$Force -ErrorAction Stop - Write-Host "Stopped PID=$procId" + Write-Host "Stopped API PID=$procId" } catch { Write-Host "[WARN] PID file process not running: $procId" } } Remove-Item -Path $pidFile -Force -ErrorAction SilentlyContinue } else { - Write-Host "==> No PID file, try by port $Port" + Write-Host "==> No API PID file, try by port $Port" +} + +if (Test-Path $workerPidFile) { + $workerPidText = (Get-Content -Path $workerPidFile -ErrorAction SilentlyContinue | Select-Object -First 1) + $workerProcId = 0 + [void][int]::TryParse("$workerPidText", [ref]$workerProcId) + if ($workerProcId -gt 0) { + try { + Stop-Process -Id $workerProcId -Force:$Force -ErrorAction Stop + Write-Host "Stopped worker PID=$workerProcId" + } catch { + Write-Host "[WARN] worker PID file process not running: $workerProcId" + } + } + Remove-Item -Path $workerPidFile -Force -ErrorAction SilentlyContinue +} else { + Write-Host "[INFO] No worker PID file" } if (Test-Path $webPidFile) { diff --git a/scripts/stop_netx.sh b/scripts/stop_netx.sh index b905511..a3c9da0 100644 --- a/scripts/stop_netx.sh +++ b/scripts/stop_netx.sh @@ -20,6 +20,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RUN_DIR="${ROOT_DIR}/scripts/.run" PID_FILE="${RUN_DIR}/netx.pid" +WORKER_PID_FILE="${RUN_DIR}/worker.pid" WEB_PID_FILE="${RUN_DIR}/web.pid" API_PORT="8890" @@ -93,6 +94,14 @@ else echo "[INFO] No API PID file: ${PID_FILE}" fi +if [[ -f "${WORKER_PID_FILE}" ]]; then + WORKER_PID="$(head -n 1 "${WORKER_PID_FILE}" | tr -d '[:space:]' || true)" + kill_pid "${WORKER_PID}" "worker" + rm -f "${WORKER_PID_FILE}" || true +else + echo "[INFO] No worker PID file: ${WORKER_PID_FILE}" +fi + if [[ -f "${WEB_PID_FILE}" ]]; then WEB_PID="$(head -n 1 "${WEB_PID_FILE}" | tr -d '[:space:]' || true)" kill_pid "${WEB_PID}" "web"