Skip to content

Commit b39673d

Browse files
committed
fix(postgres): recheck full reconciliations before witnessing them
1 parent f1dc6da commit b39673d

3 files changed

Lines changed: 144 additions & 4 deletions

File tree

internal/postgres/sync.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ type Sync struct {
173173
// vectorSource, when set, supplies the local vectors.db active generation
174174
// pushed as a phase at the end of Push. Nil disables the phase.
175175
vectorSource VectorPushSource
176+
// afterVectorApply is a full/scoped post-apply test hook.
177+
afterVectorApply func()
176178
// afterVectorGenerationLookup is a scoped-promotion test hook.
177179
afterVectorGenerationLookup func()
178180
// afterScopedVectorApply is a scoped-retry test hook.

internal/postgres/vector_push.go

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"slices"
1111
"strconv"
1212
"strings"
13+
"time"
1314
)
1415

1516
// VectorGenerationInfo identifies the local embedding generation being pushed.
@@ -131,6 +132,7 @@ const vectorProgressStride = 2000
131132
// reset — is what survives id reuse safely.
132133
type vectorGeneration struct {
133134
id int64
135+
createdAt time.Time
134136
halfvecType string
135137
machineRecorded bool
136138
}
@@ -403,6 +405,11 @@ func (s *Sync) pushVectors(
403405
); err != nil {
404406
return res, err
405407
}
408+
if s.afterVectorApply != nil {
409+
hook := s.afterVectorApply
410+
s.afterVectorApply = nil
411+
hook()
412+
}
406413
if scope != nil {
407414
if s.afterScopedVectorApply != nil {
408415
hook := s.afterScopedVectorApply
@@ -439,6 +446,37 @@ func (s *Sync) pushVectors(
439446
resolved.id, finalProbe.id,
440447
)
441448
}
449+
} else {
450+
retryGenerationWide := func(msg string, args ...any) (VectorPushResult, error) {
451+
log.Printf(msg, args...)
452+
_ = export.Close()
453+
export = nil
454+
return s.pushVectors(
455+
ctx, full, nil, lastReconciledGeneration, failedSessions, onProgress,
456+
)
457+
}
458+
finalProbe, found, err := s.lookupVectorGeneration(ctx, gen.Fingerprint, witnessKey)
459+
if err != nil {
460+
return res, err
461+
}
462+
if !found {
463+
return retryGenerationWide(
464+
"vector push: generation %q disappeared before recording the generation-wide witness; retrying generation-wide",
465+
gen.Fingerprint,
466+
)
467+
}
468+
if finalProbe.id != resolved.id {
469+
return retryGenerationWide(
470+
"vector push: active generation id changed from %d to %d before recording the generation-wide witness; retrying generation-wide",
471+
resolved.id, finalProbe.id,
472+
)
473+
}
474+
if !finalProbe.createdAt.Equal(resolved.createdAt) {
475+
return retryGenerationWide(
476+
"vector push: generation %d was recreated before recording the generation-wide witness; retrying generation-wide",
477+
resolved.id,
478+
)
479+
}
442480
}
443481
if scope == nil && res.SessionsDeferred == 0 {
444482
if err := s.recordVectorGenerationMachine(ctx, resolved.id, witnessKey); err != nil {
@@ -454,13 +492,21 @@ func (s *Sync) pushVectors(
454492
func (s *Sync) lookupVectorGeneration(
455493
ctx context.Context, fingerprint, witnessKey string,
456494
) (vectorGeneration, bool, error) {
457-
genID, _, ok, err := LookupVectorGeneration(ctx, s.pg, fingerprint)
458-
if err != nil {
459-
return vectorGeneration{}, false, err
495+
var genID int64
496+
var createdAt time.Time
497+
err := s.pg.QueryRowContext(ctx,
498+
`SELECT id, created_at FROM vector_generations WHERE fingerprint = $1`,
499+
fingerprint,
500+
).Scan(&genID, &createdAt)
501+
if err == sql.ErrNoRows {
502+
return vectorGeneration{}, false, nil
460503
}
461-
if !ok {
504+
if isUndefinedTable(err) {
462505
return vectorGeneration{}, false, nil
463506
}
507+
if err != nil {
508+
return vectorGeneration{}, false, fmt.Errorf("looking up vector generation: %w", err)
509+
}
464510
extSchema, err := vectorExtensionSchema(ctx, s.pg)
465511
if err != nil {
466512
return vectorGeneration{}, false, err
@@ -477,6 +523,7 @@ SELECT EXISTS (
477523
}
478524
return vectorGeneration{
479525
id: genID,
526+
createdAt: createdAt,
480527
halfvecType: extSchema + ".halfvec",
481528
machineRecorded: machineRecorded,
482529
}, true, nil
@@ -824,6 +871,12 @@ func (s *Sync) resolveVectorGeneration(
824871
if err != nil {
825872
return vectorGeneration{}, err
826873
}
874+
var createdAt time.Time
875+
if err := s.pg.QueryRowContext(ctx,
876+
`SELECT created_at FROM vector_generations WHERE id = $1`, genID,
877+
).Scan(&createdAt); err != nil {
878+
return vectorGeneration{}, fmt.Errorf("reading vector generation created_at: %w", err)
879+
}
827880
var machineRecorded bool
828881
if err := s.pg.QueryRowContext(ctx, `
829882
SELECT EXISTS (
@@ -834,6 +887,7 @@ SELECT EXISTS (
834887
}
835888
return vectorGeneration{
836889
id: genID,
890+
createdAt: createdAt,
837891
halfvecType: extSchema + ".halfvec",
838892
machineRecorded: machineRecorded,
839893
}, nil

internal/postgres/vector_push_pg_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,90 @@ SELECT COUNT(*) FROM vector_push_state
928928
"both beta sessions have state rows under the recreated generation")
929929
}
930930

931+
// TestVectorPushFullRechecksGenerationBeforeRecordingWitness pins the last
932+
// incarnation race: a full reconciliation must verify the generation survived
933+
// intact before it records the durable witness that later scoped pushes trust.
934+
func TestVectorPushFullRechecksGenerationBeforeRecordingWitness(t *testing.T) {
935+
pgURL := testPGURL(t)
936+
sync, localDB, pg := newVectorPushTestSync(
937+
t, pgURL, "agentsview_vector_push_full_witness_race_test")
938+
ctx := context.Background()
939+
940+
seedVectorSession(t, localDB, "A")
941+
seedVectorSession(t, localDB, "B")
942+
src := &fakeVectorSource{
943+
gen: VectorGenerationInfo{Fingerprint: "fp-full-race", Model: "m", Dimension: 4},
944+
hasGen: true,
945+
hashes: map[string]string{"A": "a1", "B": "b1"},
946+
docs: map[string][]VectorPushDoc{
947+
"A": {vdoc("A", "A#0", 0, "ca1", "a1", []float32{1, 0, 0, 0})},
948+
"B": {vdoc("B", "B#0", 0, "cb1", "b1", []float32{0, 1, 0, 0})},
949+
},
950+
}
951+
sync.vectorSource = src
952+
953+
_, err := sync.Push(ctx, false, nil)
954+
require.NoError(t, err, "baseline Push")
955+
956+
var gen1 int64
957+
require.NoError(t, pg.QueryRow(
958+
`SELECT id FROM vector_generations WHERE fingerprint = $1`, "fp-full-race",
959+
).Scan(&gen1), "gen1 id")
960+
witnessKey, err := sync.vectorGenerationWitnessKey()
961+
require.NoError(t, err, "vectorGenerationWitnessKey")
962+
963+
src.genScopes = nil
964+
src.hashScopes = nil
965+
src.hashes = map[string]string{"A": "a2", "B": "b2"}
966+
src.docs = map[string][]VectorPushDoc{
967+
"A": {vdoc("A", "A#0", 0, "ca2", "a2", []float32{0, 0, 1, 0})},
968+
"B": {vdoc("B", "B#0", 0, "cb2", "b2", []float32{0, 0, 0, 1})},
969+
}
970+
sync.afterVectorApply = func() {
971+
for _, q := range []string{
972+
`DROP TABLE IF EXISTS ` + vectorChunkTable(gen1),
973+
`DROP TABLE IF EXISTS vector_push_state`,
974+
`DROP TABLE IF EXISTS vector_generation_machines`,
975+
`DROP TABLE IF EXISTS vector_documents`,
976+
`DROP TABLE IF EXISTS vector_generations`,
977+
} {
978+
_, err := pg.Exec(q)
979+
require.NoError(t, err, q)
980+
}
981+
unavailable, err := ensureVectorBaseSchemaPG(ctx, pg)
982+
require.NoError(t, err, "recreate vector base schema")
983+
require.Empty(t, unavailable, "pgvector must stay available in test")
984+
gen2, err := ensureVectorGeneration(
985+
ctx, pg, src.gen.Fingerprint, src.gen.Model, src.gen.Dimension,
986+
)
987+
require.NoError(t, err, "re-register generation")
988+
require.Equal(t, gen1, gen2,
989+
"recreated tables restart the id sequence, which is the case under test")
990+
}
991+
992+
vres, err := sync.pushVectors(ctx, false, nil, gen1, nil, nil)
993+
require.NoError(t, err, "full push across a pre-witness recreation")
994+
assert.Equal(t, 2, vres.SessionsPushed,
995+
"the recreated generation is repopulated generation-wide before success returns")
996+
require.Len(t, src.genScopes, 2)
997+
assert.Nil(t, src.genScopes[0])
998+
assert.Nil(t, src.genScopes[1],
999+
"the retry must reopen the full generation after the recreation")
1000+
require.Len(t, src.hashScopes, 2)
1001+
assert.Nil(t, src.hashScopes[0])
1002+
assert.Nil(t, src.hashScopes[1],
1003+
"both passes are generation-wide reads")
1004+
assert.Equal(t, gen1, vres.GenerationID,
1005+
"the full retry reconciles the recreated generation even when the id is reused")
1006+
assert.Equal(t, 1, countRows(t, pg, `
1007+
SELECT COUNT(*) FROM vector_generation_machines
1008+
WHERE generation_id = $1 AND machine = $2`, gen1, witnessKey),
1009+
"the final witness is recorded only after the stable retry")
1010+
assert.Equal(t, 2, countRows(t, pg, `
1011+
SELECT COUNT(*) FROM vector_push_state WHERE generation_id = $1`, gen1),
1012+
"both sessions have state rows under the recreated generation")
1013+
}
1014+
9311015
func TestVectorPushRoundTrip(t *testing.T) {
9321016
pgURL := testPGURL(t)
9331017
sync, localDB, pg := newVectorPushTestSync(

0 commit comments

Comments
 (0)