textmachine/platform/docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt

183 lines
6.6 KiB
Text

package main
// P8-REVIEW axis 3 probe. Reproduces the SCHEDULE half of the sweep composition: `sweep`'s tick runs
// its five passes one after another on ONE goroutine, so the RUNS pass — the only thing that settles
// money, restarts a run a reboot lost and re-issues a stop the systemd call dropped — runs once per
// (sum of every pass's duration), not once per TM_PLATFORM_SWEEP_EVERY.
//
// Scaled, and only in the direction that makes it observable: the readmodel pass here is slowed by a
// fake engine that takes 400 ms per book instead of the real one's minutes, and the tick is 100 ms
// instead of 15 s. No constant of the zone is changed. The real numbers substitute directly:
// refreshSweepBudget 10m + intakeSweepBudget 21m + two 2m passes on top of the runs pass's own 2m.
//
// The runs pass is counted by the one side effect it has on a stopped run: `reconcile` re-issues
// `Runner.Stop` on every pass while the unit is alive and an intent stands (reconcile.go:387-397).
import (
"context"
"crypto/rand"
"encoding/hex"
"log/slog"
"net/url"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/jackc/pgx/v5"
"textmachine/platform/internal/ingest"
"textmachine/platform/internal/metrics"
"textmachine/platform/internal/money"
"textmachine/platform/internal/pgstore"
"textmachine/platform/internal/readmodel"
"textmachine/platform/internal/runner"
"textmachine/platform/internal/runs"
)
type countingRunner struct{ stops atomic.Int64 }
func (c *countingRunner) Start(context.Context, runner.Spec) error { return nil }
func (c *countingRunner) Stop(context.Context, string) error { c.stops.Add(1); return nil }
func (c *countingRunner) Alive(context.Context, string) (bool, error) {
return true, nil
}
// slowEngine is a readmodel engine that answers, slowly, and then says no. Slow enough to dominate a
// pass and fast enough for a test — the real one is bounded by readmodel.MaterializeBudget (5 min).
type slowEngine struct{ d time.Duration }
func (s slowEngine) Manifest(ctx context.Context, binary, workdir string) (ingest.Manifest, error) {
select {
case <-time.After(s.d):
case <-ctx.Done():
}
return ingest.Manifest{}, os.ErrDeadlineExceeded
}
func (s slowEngine) Export(ctx context.Context, binary, workdir string) (ingest.Export, error) {
return ingest.Export{}, os.ErrDeadlineExceeded
}
func probeStore(t *testing.T) (*pgstore.Store, context.Context) {
t.Helper()
admin := os.Getenv("TM_PLATFORM_TEST_DSN")
if admin == "" {
t.Skip("TM_PLATFORM_TEST_DSN not set")
}
ctx := t.Context()
var suffix [6]byte
if _, err := rand.Read(suffix[:]); err != nil {
t.Fatal(err)
}
name := "tm_p8rev_sweep_" + hex.EncodeToString(suffix[:])
conn, err := pgx.Connect(ctx, admin)
if err != nil {
t.Fatal(err)
}
if _, err := conn.Exec(ctx, "create database "+pgx.Identifier{name}.Sanitize()); err != nil {
conn.Close(ctx)
t.Skipf("cannot create a scratch database: %v", err)
}
t.Cleanup(func() {
c, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, _ = conn.Exec(c, "drop database if exists "+pgx.Identifier{name}.Sanitize()+" with (force)")
conn.Close(c)
})
u, _ := url.Parse(admin)
u.Path = "/" + name
if err := pgstore.Migrate(ctx, u.String()); err != nil {
t.Fatal(err)
}
s, err := pgstore.Open(ctx, u.String())
if err != nil {
t.Fatal(err)
}
t.Cleanup(s.Close)
return s, ctx
}
// oneStoppedRun leaves the database with exactly one live run whose stop is on file and whose unit
// still exists: the shape whose only cure is the sweep re-issuing the signal.
func oneStoppedRun(t *testing.T, db *pgstore.Store, ctx context.Context, now time.Time) {
t.Helper()
if _, err := db.Pool().Exec(ctx, `insert into users (id, email) values ('u1','u1@example.org')`); err != nil {
t.Fatal(err)
}
amount, _ := money.ParseUSD("50")
if _, err := db.Grant(ctx, "u1", amount, "test", "seed", "", now); err != nil {
t.Fatal(err)
}
bookID, err := db.AddBook(ctx, pgstore.NewBook{OwnerID: "u1", Title: "b", SourceLang: "zh",
TargetLang: "ru", ChapterCount: 100, Workdir: t.TempDir(), Now: now})
if err != nil {
t.Fatal(err)
}
started, err := db.StartRun(ctx, pgstore.StartRunInput{UserID: "u1", BookID: bookID,
CeilingChapters: 10, Ceiling: money.MicroUSD(1_000_000), Now: now}, 0,
func(context.Context, pgstore.Tx, string) error { return nil })
if err != nil {
t.Fatal(err)
}
ok, err := db.RecordSpawn(ctx, pgstore.SpawnRecord{AttemptID: started.AttemptID,
Unit: "tm-run-x-1", Binary: "/opt/tm/tmctl", EngineRunID: pgstore.EngineStreamID(started.ID, 1),
Ceiling: money.MicroUSD(1_000_000), CeilingArg: money.MicroUSD(1_000_000), Baseline: 0})
if err != nil || !ok {
t.Fatalf("record spawn: %v %v", ok, err)
}
if _, _, err := db.RequestStop(ctx, "u1", started.ID, now); err != nil {
t.Fatal(err)
}
}
// booksOwingAReadingSurface leaves n quiet books whose surface is due, which is what the readmodel
// pass works through.
func booksOwingAReadingSurface(t *testing.T, db *pgstore.Store, ctx context.Context, now time.Time, n int) {
t.Helper()
for range n {
id, err := db.AddBook(ctx, pgstore.NewBook{OwnerID: "u1", Title: "owed", SourceLang: "zh",
TargetLang: "ru", ChapterCount: 10, Workdir: t.TempDir(), Now: now})
if err != nil {
t.Fatal(err)
}
if _, err := db.Pool().Exec(ctx,
`update books set read_model_owed_at = now() where id = $1`, id); err != nil {
t.Fatal(err)
}
}
}
func TestProbeTheRunsPassRunsOncePerSumOfEveryPass(t *testing.T) {
const window = 4 * time.Second
const tick = 100 * time.Millisecond
run := func(t *testing.T, withReadModel bool) int64 {
db, ctx := probeStore(t)
now := time.Now().UTC().Truncate(time.Millisecond)
oneStoppedRun(t, db, ctx, now)
rn := &countingRunner{}
svc := &runs.Service{Store: db, Runner: rn, Now: func() time.Time { return now }}
s := sweeps{runs: svc, db: db, metrics: metrics.New()}
if withReadModel {
booksOwingAReadingSurface(t, db, ctx, now, 8)
s.reader = &readmodel.Service{Store: db, Engine: slowEngine{400 * time.Millisecond},
Binary: "/opt/tm/tmctl"}
}
c, cancel := context.WithCancel(ctx)
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); sweep(c, s, tick, 2*time.Minute, slog.New(slog.DiscardHandler)) }()
time.Sleep(window)
cancel()
wg.Wait()
return rn.stops.Load()
}
var alone, behind int64
t.Run("runs pass alone", func(t *testing.T) { alone = run(t, false) })
t.Run("runs pass behind a busy readmodel pass", func(t *testing.T) { behind = run(t, true) })
t.Logf("in %v at a %v tick: runs passes alone = %d, behind the readmodel pass = %d",
window, tick, alone, behind)
if behind >= alone {
t.Logf("NOT REPRODUCED: the runs pass kept its cadence")
}
}