Commit graph

8 commits

Author SHA1 Message Date
56f53e2c23 perf(subsUnion): unión en árbol, menos vértices, timeout y cota de memoria
All checks were successful
build-image / test (push) Successful in 2m49s
build-image / build (push) Successful in 13m4s
Cuatro endurecimientos salidos del banco de carga (bench/union-bench.js), cada
uno con su número:

1. Unión en ÁRBOL en vez de en cadena. La cadena unía siempre contra un polígono
   acumulado que no paraba de crecer. Con 1.000 suscripciones repartidas por el
   mundo —el peor caso, círculos que no se solapan— eran 238 s; en árbol son
   3,6 s, con un documento idéntico. Con datos realistas (España) 10.000
   suscripciones bajan de 39,3 s a 23,5 s. test/server/calcUnionAsync.test.js
   comprueba la equivalencia contra una referencia calculada en cadena.

2. 64 vértices por círculo en vez de 144, configurable con
   `private.unionCircleSteps`. Con 10.000 suscripciones: 23,5 s → 10,0 s y el
   pico de RSS de 777 MB → 421 MB. En pantalla un círculo de 64 lados sigue
   siendo un círculo, y la capa de zonas es un adorno difuso; el host de
   despliegue tiene 5,9 GB para todo el stack.

3. Timeout del worker (10 min por defecto, `private.unionWorkerTimeoutMs`). Un
   worker que no contestaba dejaba la promesa colgada y con ella la cola entera:
   `busy` se quedaba en true y no se volvía a calcular ninguna unión hasta
   reiniciar, sin que nada fallase. Se reintenta una vez ante un error real,
   nunca ante un timeout.

4. Cota de heap del worker (1 GB, `private.unionWorkerMaxHeapMb`), para que un
   caso patológico muera con ERR_WORKER_OUT_OF_MEMORY —que esta capa convierte
   en un rechazo, dejando la unión anterior intacta— en vez de que el kernel se
   lleve por delante el proceso de Meteor.

Además unionTelemetry.js manda a GlitchTip las uniones que pasan de un minuto y
las que hay que degradar por tamaño, y registra duración/tamaño de cada una. El
único aviso de que la unión iba mal era, hasta ahora, que el mapa se quedaba
viejo.

Smoke REST byte-idéntico. 90 tests en verde.
2026-08-01 22:32:37 +02:00
570cb49764 fix(subsUnion): drop duplicated programs/server prefix in npm path
All checks were successful
build-image / build (push) Successful in 11m53s
process.cwd() for this process is already /app/programs/server (same
basis WORKER_PATH already relied on), so prefixing 'programs/server/'
again produced .../programs/server/programs/server/npm/node_modules —
still Cannot find module '@turf/circle', just with a longer wrong path.
2026-07-30 09:04:11 +02:00
324936242b fix(subsUnion): resolve turf deps by absolute path, not bare specifier
All checks were successful
build-image / build (push) Successful in 12m9s
The previous side-effect-import fix didn't work: the app's own npm deps
(installed by `meteor build`) live in programs/server/npm/node_modules,
a SIBLING of programs/server/assets, not an ancestor of it. Node's
require() only walks up parent directories looking for node_modules,
so require('@turf/circle') from the worker asset script could never
find it regardless of what gets imported elsewhere in the app — it
kept failing with "Cannot find module '@turf/circle'" even after that
fix was deployed.

calcUnionAsync.js (Meteor-compiled code, which already knows how to
locate the app's npm deps) now resolves the three turf package paths
itself and hands them to the worker via workerData; the worker
requires them by absolute path instead of bare specifier.
2026-07-30 08:48:50 +02:00
4879d3482d fix(subsUnion): force-bundle turf deps used only by the worker thread
Some checks failed
build-image / build (push) Has been cancelled
Meteor's build only includes npm packages it sees imported/required
somewhere in its own traced module graph. private/workers/unionWorker.js
is a plain asset (loaded at runtime via new Worker(), never
import/require'd from Meteor-compiled code), so @turf/circle,
@turf/union and @turf/truncate got silently dropped from
programs/server/node_modules — the worker crashed on every call with
"Cannot find module '@turf/circle'", which incrementalAdd/process
swallowed and stored as a "null" union (typeof null === 'object', so
the null-check that guards against failed unions doesn't catch it).
Side-effect imports here force Meteor to bundle them.
2026-07-30 08:27:40 +02:00
110ad66e86 feat(subsUnion): incremental union merge for new subscriptions
All checks were successful
build-image / build (push) Successful in 13m24s
A full recreate() over 7k+ subscriptions takes ~20 minutes even in a
worker thread — fine for not blocking the site, but means a new zone
takes ages to show up on the map. Since union only grows when adding a
circle, a new subscription can just be merged into the union already
stored (one turf.union call) instead of rebuilding the whole chain.

changed/removed still trigger a full recreate() (union can't be
"subtracted" from safely), and incrementalAdd falls back to a full
recreate whenever the stored count isn't exactly one behind what's
expected, rather than risk merging into stale state. Both paths now
share one serialized queue (FIFO for adds, coalesced for recomputes)
so they never race on the same stored union.
2026-07-30 08:07:00 +02:00
fbb746fba2 fix(subsUnion): move geo-union math to a worker thread, not just yields
All checks were successful
build-image / build (push) Successful in 13m13s
The previous fix (yielding to the event loop between turf.union calls)
was not enough: once the merged polygon gets complex with thousands of
subscriptions, a single turf.union call can itself take seconds, and
yielding between iterations doesn't help when one iteration alone
blocks that long. Confirmed in staging: the site was still fully
unresponsive (Cloudflare 524, curl hanging 2+ minutes, healthcheck
failing) while a recompute ran.

Validation/decoration (addNoisy/noNoisy, cheap) stays on the main
thread; the actual circle+union chain now runs in a worker_thread
(private/workers/unionWorker.js, plain CommonJS so meteor build copies
it verbatim instead of compiling it) so the main event loop serving
DDP/HTTP is never blocked by it, regardless of how slow any single
turf call gets.
2026-07-30 07:48:17 +02:00
3d52e2f709 docs(subsUnion): note why legacy telegram subs are skipped, not errors
All checks were successful
build-image / build (push) Successful in 11m55s
2026-07-30 06:58:09 +02:00
b4e5511cd0 fix(subsUnion): stop blocking DDP on subscription union recompute
Every subscription add/change/remove recomputed the full geo union
over all 7k+ subscriptions with a synchronous turf.union chain,
freezing the single Node event loop (and thus DDP/HTTP) for minutes.
The same recompute also runs at startup ("Subs union outdated"),
so every restart froze the site too.

calcUnionAsync yields to the event loop periodically during the
union chain, and subsUnion.js now fires recomputes without blocking
Meteor.startup or the observer callbacks, serializing overlapping
triggers instead of stacking them.
2026-07-30 06:36:37 +02:00