Scale biz_state collects with dedicated workers and non-blocking UI poll.

Add PG claim/NE mutex, persist pool, and biz_state_worker replicas; fix double-SSH and row-count bugs; stop 32m collectNow while-loop from freezing page switches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-22 17:00:37 +08:00
parent 0eb65a839e
commit 3a0de0d7fb
22 changed files with 1210 additions and 165 deletions

View file

@ -34,6 +34,7 @@ $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"
$bizStateWorkerPidDir = Join-Path $runDir "biz_state_workers"
$webPidFile = Join-Path $runDir "web.pid"
$webLogFile = Join-Path $runDir "web.out.log"
$webErrFile = Join-Path $runDir "web.err.log"
@ -187,6 +188,41 @@ function Start-NetxWorker {
}
}
function Start-BizStateWorkers {
if ($InlineSchedulers) {
return
}
$replicas = 2
if ($env:NETX_BIZ_STATE_WORKER_REPLICAS) {
try { $replicas = [int]$env:NETX_BIZ_STATE_WORKER_REPLICAS } catch { $replicas = 2 }
}
if ($replicas -lt 1) { $replicas = 1 }
if (-not (Test-Path $bizStateWorkerPidDir)) {
New-Item -ItemType Directory -Path $bizStateWorkerPidDir | Out-Null
}
Write-Host "==> Starting biz_state workers (replicas=$replicas)"
for ($i = 0; $i -lt $replicas; $i++) {
$outLog = Reset-LogFile -Path (Join-Path $bizStateWorkerPidDir "worker$i.out.log")
$errLog = Reset-LogFile -Path (Join-Path $bizStateWorkerPidDir "worker$i.err.log")
$pidPath = Join-Path $bizStateWorkerPidDir "worker$i.pid"
$proc = Start-Process -FilePath $pythonExe `
-ArgumentList @("-m", "netx_api.biz_state_worker") `
-WorkingDirectory $projectRoot `
-WindowStyle Hidden `
-RedirectStandardOutput $outLog `
-RedirectStandardError $errLog `
-PassThru
Set-Content -Path $pidPath -Value "$($proc.Id)"
Write-Host "biz_state_worker[$i] PID=$($proc.Id)"
Start-Sleep -Milliseconds 400
if ($proc.HasExited) {
Write-Host "[ERR] biz_state_worker[$i] exited immediately." -ForegroundColor Red
Show-LogTail -Path $errLog
exit 1
}
}
}
if ($Background) {
# Truncate logs so a failed start is not confused with an old run.
Set-Content -Path $logFile -Value "" -Encoding utf8
@ -226,6 +262,7 @@ if ($Background) {
}
Write-Host "==> netx API ready: http://${BindHost}:${Port}/health" -ForegroundColor Green
Start-NetxWorker
Start-BizStateWorkers
if ($WithWeb) {
Write-Host "==> Starting Vite dev server in background"
$webRoot = Join-Path $projectRoot "web"
@ -283,5 +320,6 @@ if ($WithWeb) {
}
Start-NetxWorker
Start-BizStateWorkers
Write-Host "==> Starting netx API in foreground"
& $pythonExe -m netx_api.main

View file

@ -239,6 +239,21 @@ if [[ "${INLINE_SCHEDULERS}" != "1" ]]; then
echo "PID = ${WORKER_PID}"
echo "Log = ${WORKER_LOG_FILE}"
echo "Err = ${WORKER_ERR_FILE}"
REPLICAS="${NETX_BIZ_STATE_WORKER_REPLICAS:-2}"
if [[ "${REPLICAS}" -lt 1 ]]; then REPLICAS=1; fi
BIZ_DIR="${RUN_DIR}/biz_state_workers"
mkdir -p "${BIZ_DIR}"
echo "==> Starting biz_state workers (replicas=${REPLICAS})"
for ((i=0; i<REPLICAS; i++)); do
(
cd "${ROOT_DIR}"
nohup "${PYTHON_CMD}" -m netx_api.biz_state_worker \
>"${BIZ_DIR}/worker${i}.out.log" 2>"${BIZ_DIR}/worker${i}.err.log" &
echo $! > "${BIZ_DIR}/worker${i}.pid"
)
echo "biz_state_worker[${i}] PID=$(cat "${BIZ_DIR}/worker${i}.pid")"
done
fi
if [[ "${API_ONLY}" != "1" ]]; then

View file

@ -13,6 +13,7 @@ $runDir = Join-Path $PSScriptRoot ".run"
$pidFile = Join-Path $runDir "netx.pid"
$workerPidFile = Join-Path $runDir "worker.pid"
$webPidFile = Join-Path $runDir "web.pid"
$bizStateWorkerPidDir = Join-Path $runDir "biz_state_workers"
function Stop-OnePid {
param([int]$ProcId, [string]$Label)
@ -62,13 +63,19 @@ function Get-ListenPids {
function Stop-NetxByCommandLine {
# Orphan workers often have no worker.pid but still hold worker.out.log.
$hits = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -match 'netx_api\.(main|worker)' })
Where-Object { $_.CommandLine -match 'netx_api\.(main|worker|biz_state_worker)' })
if ($hits.Count -eq 0) {
Write-Host "[INFO] No netx_api.main/worker process by command line"
Write-Host "[INFO] No netx_api.main/worker/biz_state_worker process by command line"
return
}
foreach ($p in $hits) {
$kind = if ($p.CommandLine -match 'netx_api\.worker') { "worker(cmd)" } else { "api(cmd)" }
$kind = if ($p.CommandLine -match 'biz_state_worker') {
"biz_state_worker(cmd)"
} elseif ($p.CommandLine -match 'netx_api\.worker') {
"worker(cmd)"
} else {
"api(cmd)"
}
Stop-OnePid -ProcId ([int]$p.ProcessId) -Label $kind
}
}
@ -99,6 +106,18 @@ if (Test-Path $workerPidFile) {
Write-Host "[INFO] No worker PID file"
}
if (Test-Path $bizStateWorkerPidDir) {
Get-ChildItem -Path $bizStateWorkerPidDir -Filter "*.pid" -ErrorAction SilentlyContinue | ForEach-Object {
$t = (Get-Content -Path $_.FullName -ErrorAction SilentlyContinue | Select-Object -First 1)
$id = 0
[void][int]::TryParse("$t", [ref]$id)
if ($id -gt 0) {
Stop-OnePid -ProcId $id -Label "biz_state_worker"
}
Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue
}
}
if (Test-Path $webPidFile) {
$webPidText = (Get-Content -Path $webPidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
$webProcId = 0

View file

@ -102,6 +102,16 @@ else
echo "[INFO] No worker PID file: ${WORKER_PID_FILE}"
fi
BIZ_DIR="${RUN_DIR}/biz_state_workers"
if [[ -d "${BIZ_DIR}" ]]; then
for f in "${BIZ_DIR}"/*.pid; do
[[ -f "${f}" ]] || continue
BPID="$(head -n 1 "${f}" | tr -d '[:space:]' || true)"
kill_pid "${BPID}" "biz_state_worker"
rm -f "${f}" || true
done
fi
if [[ -f "${WEB_PID_FILE}" ]]; then
WEB_PID="$(head -n 1 "${WEB_PID_FILE}" | tr -d '[:space:]' || true)"
kill_pid "${WEB_PID}" "web"