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>
123 lines
3.1 KiB
Go
123 lines
3.1 KiB
Go
package landscape
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"gis/internal/raster"
|
|
)
|
|
|
|
// fakeIO keeps rasters in memory keyed by path.
|
|
type fakeIO struct {
|
|
mu sync.Mutex
|
|
files map[string]*fakeRaster
|
|
|
|
// failReadAfter, when set for a path, makes every ReadFloat64 call
|
|
// against that path fail once the cumulative read count (across all
|
|
// readers/workers) exceeds the configured threshold. Used to simulate a
|
|
// mid-run I/O failure.
|
|
failReadAfter map[string]int
|
|
readCounts map[string]int
|
|
|
|
// opens counts every successful Open. Each pool worker opens one reader
|
|
// per input, so the count bounds the number of concurrent handles.
|
|
opens int
|
|
}
|
|
|
|
// openCount returns how many readers have been opened so far.
|
|
func (f *fakeIO) openCount() int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.opens
|
|
}
|
|
|
|
type fakeRaster struct {
|
|
grid raster.Grid
|
|
data []float64 // row-major
|
|
opts raster.WriterOptions
|
|
}
|
|
|
|
func newFakeIO() *fakeIO { return &fakeIO{files: map[string]*fakeRaster{}} }
|
|
|
|
func (f *fakeIO) put(path string, g raster.Grid, data []float64) {
|
|
f.files[path] = &fakeRaster{grid: g, data: data}
|
|
}
|
|
|
|
// failReadAfterN makes ReadFloat64 against path fail starting from the
|
|
// (n+1)-th call to it.
|
|
func (f *fakeIO) failReadAfterN(path string, n int) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.failReadAfter == nil {
|
|
f.failReadAfter = map[string]int{}
|
|
f.readCounts = map[string]int{}
|
|
}
|
|
f.failReadAfter[path] = n
|
|
}
|
|
|
|
type fakeReader struct {
|
|
f *fakeIO
|
|
path string
|
|
r *fakeRaster
|
|
}
|
|
|
|
func (r *fakeReader) Grid() raster.Grid { return r.r.grid }
|
|
func (r *fakeReader) NoData() (float64, bool) { return -9999, true }
|
|
func (r *fakeReader) Close() error { return nil }
|
|
func (r *fakeReader) ReadFloat64(w raster.Window, dst []float64) error {
|
|
if r.f != nil {
|
|
r.f.mu.Lock()
|
|
if n, ok := r.f.failReadAfter[r.path]; ok {
|
|
r.f.readCounts[r.path]++
|
|
if r.f.readCounts[r.path] > n {
|
|
r.f.mu.Unlock()
|
|
return fmt.Errorf("fake: simulated read failure for %s", r.path)
|
|
}
|
|
}
|
|
r.f.mu.Unlock()
|
|
}
|
|
for y := 0; y < w.Height; y++ {
|
|
copy(dst[y*w.Width:(y+1)*w.Width], r.r.data[(w.Y+y)*r.r.grid.Width+w.X:(w.Y+y)*r.r.grid.Width+w.X+w.Width])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type fakeWriter struct {
|
|
f *fakeIO
|
|
r *fakeRaster
|
|
}
|
|
|
|
func (w *fakeWriter) WriteInt16(win raster.Window, src []int16) error {
|
|
w.f.mu.Lock()
|
|
defer w.f.mu.Unlock()
|
|
for y := 0; y < win.Height; y++ {
|
|
for x := 0; x < win.Width; x++ {
|
|
w.r.data[(win.Y+y)*w.r.grid.Width+win.X+x] = float64(src[y*win.Width+x])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (w *fakeWriter) Close() error { return nil }
|
|
|
|
func (f *fakeIO) Open(_ context.Context, path string) (raster.Reader, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
r, ok := f.files[path]
|
|
if !ok {
|
|
return nil, fmt.Errorf("fake: %s not found", path)
|
|
}
|
|
f.opens++
|
|
return &fakeReader{f: f, path: path, r: r}, nil
|
|
}
|
|
|
|
func (f *fakeIO) Create(_ context.Context, path string, g raster.Grid, o raster.WriterOptions) (raster.Writer, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
r := &fakeRaster{grid: g, data: make([]float64, g.Width*g.Height), opts: o}
|
|
f.files[path] = r
|
|
return &fakeWriter{f: f, r: r}, nil
|
|
}
|
|
|
|
func (f *fakeIO) SameGrid(a, b raster.Grid) bool { return a == b }
|