logistics-rate-pipeline — measured results

Eight synthetic ocean-freight rate feeds → a concurrent Go ingestion service (bounded worker pool, retries, graceful shutdown, Prometheus/pprof) → a PySpark normalizer producing a rate matrix with named reject rules. All feeds are invented; the engineering is the point.

1.82×worker pool vs. sequential (6.85 s → 3.77 s)
−64 %allocs/op on CSV parse after pprof
20,810 → 2,294raw rows → normalized lanes
0data races · goroutine leaks · records lost on cancel

1. Worker-pool benchmark

Median wall time over 5 reps per worker count, 20,810 records per run, Go go1.27.0, ingestd 3663789.

1 worker6.85 s · 1.00×2 workers3.77 s · 1.82×4 workers3.77 s · 1.82×8 workers3.77 s · 1.82×16 workers3.77 s · 1.82×
workersmedian wallspeeduprecordspeak RSS
16.85 s1.00×20,81024 MB
23.77 s1.82×20,81028 MB
43.77 s1.82×20,81029 MB
83.77 s1.82×20,81033 MB
163.77 s1.82×20,81033 MB

Why it plateaus at 1.82× and not the 4× the plan assumed: the pool cannot beat the critical path. One feed (aurora) serves two sequential cursor pages at ~1.9 s each, so no worker count finishes under ≈ 3.8 s while the other seven feeds sum to ≈ 2.9 s. The number is committed as measured (decision D-40); the only fix is page-level parallelism, which cursor pagination forbids by contract.

Source: bench/results/20260917T070639Z-pool.md and the JSON beside it.

2. pprof-driven optimization — CSV parse, 20 000 rows

Allocation profile showed 55 % of bytes in ComputeHash (a growing strings.Builder, its final copy, and hex encoding). Fix: a sync.Pool-ed scratch buffer, sha256.Sum256, hex into a stack array, one string.

allocs/op before220,018allocs/op after80,030B/op before16.1 MBB/op after8.2 MB
metricbeforeafterchange
ns/op17,631,46314,986,805≈ −15 %
B/op16,066,6558,233,314−49 %
allocs/op220,01880,030−64 %

Source: bench/results/20260917T071149Z-parse.md (raw go test -bench output and pprof top included).

3. Normalization funnel (PySpark 4.2.0)

raw rows in20,810duplicates removed18,450rejected (named rules)66normalized rows out2,294

Dedup is a window over (carrier, lane, container, validity) keeping the highest-priority source; rejects keep their raw values and the rule name so nothing is silently dropped.

Rejects by named rule (66 rows)

same_port50expired_as_of6unknown_port3bad_price2dates_reversed2unknown_container2unknown_currency1

Stage durations (local[2], 8 GB laptop)

dedup0.8 sread4.7 stransform3.5 svalidate1.1 swrite2.8 s

Source: bench/results/20260917T070639Z-normalize.json — run id fixture, as-of 2026-09-01.

4. Correctness checks

checkresultwhere
go test -race ./...cleanCI, every push
goroutine leaks (goleak in every pool test, live /debug/pprof/goroutineleak)0internal/pool, internal/server
Cancelled mid-runno manifest, 0 promoted partsTestCancelMidRunWritesNoManifestAndNoParts, tests/e2e/shutdown.sh
Spark rules25 tests incl. every rule pass/fail and an exact-count fixturespark/tests

Still pending

Container image sizes, the in-cluster shutdown test on kind, the EKS run, Databricks Free Edition and Redshift loads, and end-to-end latency — each lands here as a committed result file once run. This page is regenerated by scripts/render_results.py; nothing on it is typed by hand.