fix(landscape): guard redelivery, detach terminal writes, bound workers

F2: Execute had no status guard, so a redelivered or stale message reran a
finished run. Runs that are neither queued nor running are logged and acked
without work; "running" still executes so a worker that crashed mid-run
recovers.

F4: failRun, finishNodeFailed and MarkRunFailed wrote the terminal status on
the caller's context, which is exactly the context that is already cancelled
when the run failed because of a shutdown or a sibling node — the write was
refused and the run stayed stuck in "running". They now write on a
context.WithoutCancel copy with a 10s timeout; the consumer's best-effort
MarkRunFailed does the same.

F5: peak memory scaled with the host's core count. LP_MAX_WORKERS (default 8)
is now the total window-worker budget of a run and LP_MAX_NODES (default 2) the
node fan-out; each node gets max(1, workers/nodes) through the new
landscape.ComputeWithWorkers. Compute and service.ComputeFunc keep their
signatures: Compute delegates with MaxWorkersPerNode, and a legacy ComputeFunc
handed to NewLandscapeService is wrapped.

F6: the stage loop used a plain errgroup, so a failing node let its siblings
run to completion. It now uses errgroup.WithContext and passes the group
context into runNode, cancelling siblings mid-compute.

F7: wrap every float32 product in an explicit conversion so the compiler
cannot fuse it into an FMA, keeping the kernels bit-identical to numpy's
per-op rounding.

F8: README's builder package list now matches the Dockerfile (adds g++), and
the Landscape section documents the memory model and the two new knobs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 14:44:34 +05:00
parent bcda76003c
commit 4051506b95
12 changed files with 563 additions and 69 deletions

View File

@@ -12,7 +12,6 @@ import (
"gis/api"
"gis/internal/config"
"gis/internal/domain"
"gis/internal/landscape"
"gis/internal/messaging/rabbitmq"
"gis/internal/parser"
"gis/internal/platform/logger"
@@ -92,9 +91,15 @@ func New(ctx context.Context) (*App, error) {
categories: service.NewCategoryService(categoryRepo),
datasets: datasetsSvc,
groups: service.NewDatasetGroupService(groupRepo, datasetRepo, store),
// A nil ComputeFunc selects landscape.ComputeWithWorkers, so each node
// gets the per-node share of the configured worker budget.
landscape: service.NewLandscapeService(
lpRepo, datasetRepo, groupRepo, categoryRepo, datasetsSvc, store,
lpJobs, raster.NewGodalIO(), landscape.Compute, log,
lpJobs, raster.NewGodalIO(), nil, log,
service.LandscapeOptions{
MaxWorkers: cfg.Landscape.MaxWorkers,
MaxConcurrentNodes: cfg.Landscape.MaxConcurrentNodes,
},
),
eventRepo: eventRepo,
}, nil