materializer + replay ports + vectord substrate fix verified at scale
Two threads landing together — the doc edits interleave so they ship in a single commit. 1. **vectord substrate fix verified at original scale** (closes the 2026-05-01 thread). Re-ran multitier 5min @ conc=50: 132,211 scenarios at 438/sec, 6/6 classes at 0% failure (was 4/6 pre-fix). Throughput dropped 1,115 → 438/sec because previously-broken scenarios now do real HNSW Add work — honest cost of correctness. The fix (i.vectors side-store + safeGraphAdd recover wrappers + smallIndexRebuildThreshold=32 + saveTask coalescing) holds at the footprint that originally surfaced the bug. 2. **Materializer port** — internal/materializer + cmd/materializer + scripts/materializer_smoke.sh. Ports scripts/distillation/transforms.ts (12 transforms) + build_evidence_index.ts (idempotency, day-partition, receipt). On-wire JSON shape matches TS so Bun and Go runs are interchangeable. 14 tests green. 3. **Replay port** — internal/replay + cmd/replay + scripts/replay_smoke.sh. Ports scripts/distillation/replay.ts (retrieve → bundle → /v1/chat → validate → log). Closes audit-FULL phase 7 live invocation on the Go side. Both runtimes append to the same data/_kb/replay_runs.jsonl (schema=replay_run.v1). 14 tests green. Side effect on internal/distillation/types.go: EvidenceRecord gained prompt_tokens, completion_tokens, and metadata fields to mirror the TS shape the materializer transforms produce. STATE_OF_PLAY refreshed to 2026-05-02; ARCHITECTURE_COMPARISON decisions tracker moves the materializer + replay items from _open_ to DONE and adds the substrate-fix scale verification row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,15 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -417,3 +421,105 @@ func TestSearchK_DefaultsAndMax(t *testing.T) {
|
||||
t.Errorf("maxK=%d unreasonably large", maxK)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveTask_Coalesces locks the multitier_100k follow-up: a
|
||||
// burst of triggers must collapse into at most 2 actual saves
|
||||
// (the in-flight one + one catch-up). Without coalescing, every
|
||||
// trigger would yield a save and concurrent writers would
|
||||
// serialize on the index RLock during Encode (the original
|
||||
// 1-2.5s tail-latency cause).
|
||||
func TestSaveTask_Coalesces(t *testing.T) {
|
||||
var (
|
||||
s saveTask
|
||||
saveCnt atomic.Int32
|
||||
started = make(chan struct{}, 1)
|
||||
release = make(chan struct{})
|
||||
)
|
||||
save := func() error {
|
||||
// First save blocks until released so we can pile up
|
||||
// triggers behind it. Subsequent saves return fast so the
|
||||
// catch-up logic completes promptly.
|
||||
n := saveCnt.Add(1)
|
||||
if n == 1 {
|
||||
started <- struct{}{}
|
||||
<-release
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Trigger first save and wait for it to enter the blocked region.
|
||||
s.trigger(save)
|
||||
<-started
|
||||
// Pile up triggers while the first is blocked. None of these
|
||||
// should start their own goroutines — they should mark "pending".
|
||||
for i := 0; i < 50; i++ {
|
||||
s.trigger(save)
|
||||
}
|
||||
// Release the first save. The trigger logic should run ONE
|
||||
// catch-up save for all 50 piled-up triggers, then return.
|
||||
close(release)
|
||||
// Wait for the goroutine to drain.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
s.mu.Lock()
|
||||
idle := !s.inflight && !s.pending
|
||||
s.mu.Unlock()
|
||||
if idle {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
got := saveCnt.Load()
|
||||
if got != 2 {
|
||||
t.Errorf("save count = %d, want 2 (one in-flight + one catch-up)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveTask_RunsOnce — single trigger fires exactly one save.
|
||||
func TestSaveTask_RunsOnce(t *testing.T) {
|
||||
var s saveTask
|
||||
var n atomic.Int32
|
||||
done := make(chan struct{})
|
||||
s.trigger(func() error {
|
||||
n.Add(1)
|
||||
close(done)
|
||||
return nil
|
||||
})
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("trigger goroutine never ran")
|
||||
}
|
||||
// Wait briefly for the goroutine to mark inflight=false.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if got := n.Load(); got != 1 {
|
||||
t.Errorf("save count = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveTask_LogsSaveError — a save error doesn't break the
|
||||
// coalescing state machine; subsequent triggers still work.
|
||||
func TestSaveTask_LogsSaveError(t *testing.T) {
|
||||
var s saveTask
|
||||
var n atomic.Int32
|
||||
wantErr := errors.New("boom")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
s.trigger(func() error {
|
||||
defer wg.Done()
|
||||
n.Add(1)
|
||||
return wantErr
|
||||
})
|
||||
wg.Wait()
|
||||
// State must reset so the next trigger fires another save.
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
wg.Add(1)
|
||||
s.trigger(func() error {
|
||||
defer wg.Done()
|
||||
n.Add(1)
|
||||
return nil
|
||||
})
|
||||
wg.Wait()
|
||||
if got := n.Load(); got != 2 {
|
||||
t.Errorf("save count = %d, want 2 (failure must not stall the task)", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user