fix(paths): split multibyte-paths into two simple queries joined in JS (timeout → 0.3s)

This commit is contained in:
gadgethd committed 2026-08-06 22:40:37 +00:00
1 parent 991b99fef7
commit 8306cd770c
2 files changed
+68 -47

No files matched your search

+7
View File
@@ -353,3 +353,10 @@ ROOT CAUSE (3 layers, all fixed):
RESULTS: heardBy 80.6s→87ms, advert 1.8s→6ms, status 2.7s→44ms, recentPackets 11s→222ms, links 42.5s→40ms. Full owner/live battery <500ms. Zero owner/live timeouts since deploy. Health healthy, HopReach 200.
Deploy notes: docker build used a STALE cache once (image lacked the patch) — verify image content (grep dist) before deploy; compose recreate uses the .env pin at recreate time — update pin BEFORE recreate or pass BACKEND_IMAGE=<digest> explicitly.
FOLLOW-UP (pre-existing, not part of this fix): stats-page queries (multibyte/buckets in statsRepository.ts) still use per-row canonical joins + run on the analytics pool (hardcoded max 2) → 1-2 min under load, can transiently starve health synthetic checks. Same function→join fix applies when next touched.
## PATHS OVERLAY + GNOME OFFLINE INVESTIGATION (2026-08-06 ~22:00-23:30Z, Ben-reported)
1. PATHS TOGGLE EMPTY — ROOT CAUSE: GET /path-beta/multibyte-paths timed out (25s+). The single JOIN blob made the planner estimate rows=1 on the scoped-node CTE (node_identity_sightings EXISTS is not estimable) → nested loops re-materialising the 13k-row identity view per row → 17.4M-row scans, 100-155s. ANALYZE + MATERIALIZED + UNION variants all still mis-estimated. FIX (commit 6e5d4c5): split into two simple statements (scoped nodes ~325ms + multibyte links ~92ms) joined + geometry-filtered in JS. Live: ukmesh 0.30s / 4,202 segments / maxCount 698,565; noparams 0.22s. Backend 273/273.
2. SITE-WIDE STALENESS — NONE. Ingest healthy (7.5k+5.3k packets/2h; top senders fresh; Sh@DoW nodes last seen 1-5 min). Multibyte evidence flowing (10,176 today). Other observers' traffic normal (NULL-src = encrypted, expected).
3. GNOME-STKTN-RPT-V4 OFFLINE — REAL, NOT A WEBSITE BUG. Steady ~13 payload-mentions/hour for days → abrupt stop 21:32:00.315Z. Zero evidence in any column since (src/rx/topic/payload/status). The site is correctly reporting the device went silent. GNOME-MSG-RPT online (22:13Z, 2,746 adverts). Likely device/power/RF at the STKTN site — physical check needed.
4. SIGHTINGS MV: node_identity_sightings (22.5k rows, zero indexes, per-query canonical grouping) → indexed MATERIALIZED VIEW (unique (node_id,network) + (network,node_id)), refreshed every 5 min with the links MV. All buildNodeScopeClause consumers (topology, viable links, paths, network filters) benefit.
5. recordMultibyteEvidence deadlock errors: 4 in 6h pre-fix, 0 in the 30 min after the MV swap. Watch.
+61 -47
View File
@@ -1237,53 +1237,67 @@ export async function getMultibytePathSegments(network?: string, observer?: stri
const scope = buildScopePlaceholders(1, network, observer);
const params: unknown[] = [...scope.params];
const res = await pool.query<{
a_lat: number;
a_lon: number;
b_lat: number;
b_lon: number;
count: number;
}>(
`SELECT
a.lat AS a_lat,
a.lon AS a_lon,
b.lat AS b_lat,
b.lon AS b_lon,
nl.multibyte_observed_count AS count
FROM node_identity_links nl
JOIN node_identity_nodes a ON a.node_id = nl.node_a_id
JOIN node_identity_nodes b ON b.node_id = nl.node_b_id
WHERE nl.multibyte_observed_count > 0
AND nl.itm_viable = true
AND a.lat IS NOT NULL
AND a.lon IS NOT NULL
AND b.lat IS NOT NULL
AND b.lon IS NOT NULL
AND a.lat BETWEEN -90 AND 90
AND b.lat BETWEEN -90 AND 90
AND a.lon BETWEEN -180 AND 180
AND b.lon BETWEEN -180 AND 180
AND NOT (ABS(a.lat) < 5 AND ABS(a.lon) < 5)
AND NOT (ABS(b.lat) < 5 AND ABS(b.lon) < 5)
AND (a.name IS NULL OR a.name NOT LIKE '%🚫%')
AND (b.name IS NULL OR b.name NOT LIKE '%🚫%')
AND SQRT(
POWER((a.lat - b.lat) * 111, 2)
+ POWER((a.lon - b.lon) * 111 * COS(RADIANS((a.lat + b.lat) / 2)), 2)
) <= ${MAX_MULTIBYTE_PATH_SEGMENT_KM}
${buildNodeScopeClause(scope, 'a')}
${buildNodeScopeClause(scope, 'b')}
ORDER BY nl.multibyte_observed_count DESC`,
params,
);
// Two simple statements joined in JS. A single JOIN blob makes the planner
// guess rows=1 on the scoped-node CTE (the sightings EXISTS is not
// estimable) and pick nested loops that re-materialise the identity view
// per row: 100s+. Splitting keeps both statements simple enough that the
// planner cannot mis-estimate them (measured: ~325ms + ~92ms).
const [nodesRes, linksRes] = await Promise.all([
pool.query<{
node_id: string;
lat: number | null;
lon: number | null;
name: string | null;
}>(
`SELECT node_id, lat, lon, name
FROM node_identity_nodes n
WHERE 1=1${buildNodeScopeClause(scope, 'n')}`,
params,
),
pool.query<{
node_a_id: string;
node_b_id: string;
multibyte_observed_count: number;
}>(
`SELECT node_a_id, node_b_id, multibyte_observed_count
FROM node_identity_links
WHERE multibyte_observed_count > 0
AND itm_viable = true`,
),
]);
const segments = res.rows.map((row) => ({
positions: [
[Number(row.a_lat), Number(row.a_lon)],
[Number(row.b_lat), Number(row.b_lon)],
] as [[number, number], [number, number]],
count: Number(row.count ?? 0),
}));
const nodesById = new Map(nodesRes.rows.map((row) => [row.node_id, row]));
const segments: Array<{
positions: [[number, number], [number, number]];
count: number;
}> = [];
for (const link of linksRes.rows) {
const a = nodesById.get(link.node_a_id);
const b = nodesById.get(link.node_b_id);
if (!a || !b) continue;
if (a.lat == null || a.lon == null || b.lat == null || b.lon == null) continue;
if (a.lat < -90 || a.lat > 90 || b.lat < -90 || b.lat > 90) continue;
if (a.lon < -180 || a.lon > 180 || b.lon < -180 || b.lon > 180) continue;
if (Math.abs(a.lat) < 5 && Math.abs(a.lon) < 5) continue;
if (Math.abs(b.lat) < 5 && Math.abs(b.lon) < 5) continue;
if ((a.name ?? '').includes('🚫') || (b.name ?? '').includes('🚫')) continue;
const distKm = Math.sqrt(
Math.pow((a.lat - b.lat) * 111, 2)
+ Math.pow(
(a.lon - b.lon) * 111 * Math.cos(((a.lat + b.lat) / 2) * (Math.PI / 180)),
2,
),
);
if (distKm > MAX_MULTIBYTE_PATH_SEGMENT_KM) continue;
segments.push({
positions: [
[a.lat, a.lon],
[b.lat, b.lon],
],
count: link.multibyte_observed_count,
});
}
segments.sort((x, y) => y.count - x.count);
const maxCount = segments.reduce((max, segment) => Math.max(max, segment.count), 0);
return { maxCount, segments };
@@ -1311,7 +1325,7 @@ export async function getViableLinks(network?: string, observer?: string): Promi
if (network && !observer) {
const scopedNetworks = network === 'ukmesh' ? UKMESH_NETWORKS : [network];
const res = await pool.query<ViableLinkRow>(
`WITH net_nodes AS (
`WITH net_nodes AS MATERIALIZED (
SELECT DISTINCT node_id FROM node_identity_nodes
WHERE network = ANY($1::text[])
AND (name IS NULL OR name NOT LIKE '%🚫%')