From f2834010766796429dcfa8c78a40b719ef39c714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 24 Apr 2026 17:43:15 +0200 Subject: [PATCH] Stop BRouter contention kills under rapid editing When a user rapidly moves waypoints in the Planner, BRouter's thread-priority watchdog kills older in-flight requests to free a thread for newer ones, surfacing to the user as "operation killed by thread-priority-watchdog" errors. Two fixes: 1. Bump the BRouter thread pool on the dedicated host from 4 to 16. `BROUTER_THREADS` is now a Dockerfile ENV (default 4 for flagship/CI/dev) overridden to 16 in `infrastructure/brouter-host/`. The host has 32 GB of RAM and idle cores; 4 threads was a flagship-era constraint that no longer applies. 2. Cancel the previous in-flight `/api/route` request when a new one starts. `use-routing.ts` now keeps an `AbortController` in a ref, aborts it at the top of each `computeRoute`, and bails quietly on `AbortError` so the newer call drives state. Together these eliminate both the "real" contention (at the BRouter level) and the wasted work (BRouter computing a route the client no longer cares about). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/planner/app/lib/use-routing.ts | 18 ++++++++++++++++-- docker/brouter/Dockerfile | 11 +++++++++-- infrastructure/brouter-host/docker-compose.yml | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apps/planner/app/lib/use-routing.ts b/apps/planner/app/lib/use-routing.ts index bb4eb37..cd96b9e 100644 --- a/apps/planner/app/lib/use-routing.ts +++ b/apps/planner/app/lib/use-routing.ts @@ -51,6 +51,10 @@ export function useRouting(yjs: YjsState | null, sessionId: string) { const debounceTimer = useRef>(undefined); const lastGoodWaypointsRef = useRef(null); const restoringRef = useRef(false); + // Cancels the in-flight /api/route call when a newer one starts. Without + // this, rapid edits pile up on BRouter's thread pool and older requests + // get killed by its contention watchdog, surfacing as spurious errors. + const inflightAbortRef = useRef(null); // Host election via Yjs awareness useEffect(() => { @@ -85,6 +89,9 @@ export function useRouting(yjs: YjsState | null, sessionId: string) { const snapshotBeforeCompute = getWaypointsFromYjs(yjs.waypoints); setComputing(true); + inflightAbortRef.current?.abort(); + const controller = new AbortController(); + inflightAbortRef.current = controller; try { const response = await fetch("/api/route", { method: "POST", @@ -95,6 +102,7 @@ export function useRouting(yjs: YjsState | null, sessionId: string) { noGoAreas: noGoAreas.length > 0 ? noGoAreas : undefined, sessionId, }), + signal: controller.signal, }); if (response.status === 429) { @@ -146,11 +154,17 @@ export function useRouting(yjs: YjsState | null, sessionId: string) { yjs.routeData.set("bikeroutes", JSON.stringify(enriched.bikeroutes)); } }); - } catch { + } catch (err) { + // A superseding request aborted this one — leave state alone so + // the newer call's result becomes authoritative. + if ((err as Error)?.name === "AbortError") return; setRouteError("failed"); restoreWaypoints(yjs, lastGoodWaypointsRef.current ?? snapshotBeforeCompute, restoringRef); } finally { - setComputing(false); + if (inflightAbortRef.current === controller) { + inflightAbortRef.current = null; + setComputing(false); + } } }, [yjs, isHost], diff --git a/docker/brouter/Dockerfile b/docker/brouter/Dockerfile index 477c1ab..c1dcf36 100644 --- a/docker/brouter/Dockerfile +++ b/docker/brouter/Dockerfile @@ -38,9 +38,16 @@ EXPOSE 17777 # The default keeps the flagship's single-instance footprint small. ENV JAVA_OPTS="-Xmx1024M -Xms256M -Xmn64M" +# Routing thread pool. When saturated, BRouter's thread-priority watchdog +# kills the oldest in-flight request ("contention! ms killed …") to free a +# thread for the new one — the caller then sees a cancelled response. Bump +# this on hosts with spare cores/RAM to absorb burst traffic. Planet host +# sets it to 16; flagship/CI keep the default. +ENV BROUTER_THREADS=4 + # BRouter server: -# Shell form so $JAVA_OPTS expands at container start. +# Shell form so $JAVA_OPTS and $BROUTER_THREADS expand at container start. CMD java $JAVA_OPTS -DmaxRunningTime=300 -cp brouter.jar \ btools.server.RouteServer \ /data/segments /data/profiles /data/profiles \ - 17777 4 + 17777 $BROUTER_THREADS diff --git a/infrastructure/brouter-host/docker-compose.yml b/infrastructure/brouter-host/docker-compose.yml index 429bbbf..4d82e4f 100644 --- a/infrastructure/brouter-host/docker-compose.yml +++ b/infrastructure/brouter-host/docker-compose.yml @@ -19,8 +19,12 @@ services: # Planet-scale coverage: segments live on the host and are mounted in. # 8 GB heap for segment cache; -Xms generous because routing is # memory-heavy and we don't benefit from a slow JVM warmup. + # 16 routing threads on the 32 GB / multi-core dedicated host absorbs + # burst traffic from "click around wildly" editing without triggering + # BRouter's contention watchdog on the 4-thread default. environment: JAVA_OPTS: "-Xmx8g -Xms512M" + BROUTER_THREADS: "16" volumes: - ./segments:/data/segments:ro networks: