package store import ( "context" "database/sql" "fmt" "path/filepath" "sort" "sync" "sync/atomic" "testing" "time" ) // conncap_test.go measures whether a read queues behind a write at // SetMaxOpenConns(1), which is what openAt sets (V-642). It is a measurement // harness, not an assertion: the numbers it prints are the evidence, and the // decision to move the cap or leave it belongs in docs/evals. // // Run it with -v, and note that it is skipped under -short because it spends // seconds on purpose. // openCapped opens a plaintext store at the given connection cap. In-package, // so it can reach the handle openAt caps at 1. func openCapped(t *testing.T, cap int) *Store { t.Helper() path := filepath.Join(t.TempDir(), "cap.db") db, err := openAt(context.Background(), path) if err != nil { t.Fatalf("openAt: %v", err) } db.SetMaxOpenConns(cap) s := &Store{db: db} t.Cleanup(func() { _ = s.Close() }) return s } func percentile(d []time.Duration, p float64) time.Duration { if len(d) == 0 { return 0 } i := int(float64(len(d)-1) * p) return d[i] } // seedFacts writes n facts so a read has rows to decode. func seedFacts(t *testing.T, s *Store, n int) { t.Helper() ctx := context.Background() now := time.Now().UTC() for i := 0; i < n; i++ { key := fmt.Sprintf("seed_%d", i) if _, err := s.SetValue(ctx, KindSelf, key, "tap:test", map[string]int{"ml": i}, now.Add(time.Duration(i)*time.Millisecond)); err != nil { t.Fatalf("seed %d: %v", i, err) } } } // measureReadsUnderWrites reports read latency percentiles while a writer // writes at a fixed pace. The pace matters: an unpaced writer completes a // different number of writes at each cap, because at a higher cap it competes // with the readers for the write lock instead of taking turns on one // connection. Two runs that did different work cannot be compared. // It runs for a fixed wall-clock window rather than a fixed read count, so the // paced writer does the same work at every cap. Tying the window to a read // count made the faster configuration receive fewer writes. func measureReadsUnderWrites(t *testing.T, s *Store, window, pace time.Duration) []time.Duration { t.Helper() ctx := context.Background() var stop atomic.Bool var writes atomic.Int64 var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() now := time.Now().UTC() for i := 0; !stop.Load(); i++ { key := fmt.Sprintf("hot_%d", i%16) if _, err := s.SetValue(ctx, KindSelf, key, "tap:test", map[string]int{"n": i}, now.Add(time.Duration(i)*time.Millisecond)); err != nil { t.Errorf("write: %v", err) return } writes.Add(1) time.Sleep(pace) } }() var lat []time.Duration deadline := time.Now().Add(window) for time.Now().Before(deadline) { start := time.Now() if _, err := s.RecentFacts(ctx, 50); err != nil { t.Fatalf("RecentFacts: %v", err) } lat = append(lat, time.Since(start)) } stop.Store(true) wg.Wait() t.Logf("in %v: %d reads, %d writes", window, len(lat), writes.Load()) sort.Slice(lat, func(i, j int) bool { return lat[i] < lat[j] }) return lat } // TestConnCap_ReadLatencyUnderWrites is the V-642 measurement: read latency at // cap 1 against cap 4, same workload, same schema, same driver. func TestConnCap_ReadLatencyUnderWrites(t *testing.T) { if testing.Short() { t.Skip("measurement harness; runs for seconds") } for _, cap := range []int{1, 4} { t.Run(fmt.Sprintf("cap=%d", cap), func(t *testing.T) { s := openCapped(t, cap) seedFacts(t, s, 500) lat := measureReadsUnderWrites(t, s, 2*time.Second, 2*time.Millisecond) t.Logf("cap=%d reads=%d p50=%v p95=%v max=%v", cap, len(lat), percentile(lat, 0.50), percentile(lat, 0.95), lat[len(lat)-1]) }) } } // TestConnCap_ReadBlocksBehindOpenSnapshot is the sharper claim: at cap 1 an // open read-only transaction holds the only connection, so an unrelated read // cannot proceed until it commits. This is why the store exposes no way to // begin one — `Store.DB` used to, and was deleted in V-642 with no caller. The // test stays as the reason, so re-adding that seam fails a measurement rather // than shipping a stall. func TestConnCap_ReadBlocksBehindOpenSnapshot(t *testing.T) { if testing.Short() { t.Skip("measurement harness; waits on a timeout") } for _, cap := range []int{1, 4} { t.Run(fmt.Sprintf("cap=%d", cap), func(t *testing.T) { s := openCapped(t, cap) seedFacts(t, s, 50) tx, err := s.db.BeginTx(context.Background(), &sql.TxOptions{ReadOnly: true}) if err != nil { t.Fatalf("BeginTx: %v", err) } defer func() { _ = tx.Rollback() }() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() start := time.Now() _, err = s.RecentFacts(ctx, 10) t.Logf("cap=%d read alongside an open snapshot: waited %v, err=%v", cap, time.Since(start).Round(time.Millisecond), err) }) } }