Skip to content

Commit 3fb4275

Browse files
committed
fix: retry poisoned comparison preloads cleanly
1 parent 6a07b22 commit 3fb4275

3 files changed

Lines changed: 120 additions & 51 deletions

File tree

internal/postgres/push.go

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,10 @@ type batchResult struct {
556556
skippedConflicts int
557557
}
558558

559+
var errPushComparisonPreload = errors.New(
560+
"push comparison preload failed",
561+
)
562+
559563
// pushBatch pushes a slice of sessions within a single
560564
// transaction. On success it appends to pushed and returns
561565
// ok=true with session/message counts. On a session-level
@@ -570,6 +574,35 @@ func (s *Sync) pushBatch(
570574
legacyMarkerMachines []string,
571575
sessionUsageFingerprints map[string]string,
572576
pushed *[]db.Session,
577+
) (batchResult, error) {
578+
preloadComparisons := len(batch) > 0 && !full
579+
result, err := s.pushBatchAttempt(
580+
ctx, batch, full, markerID, legacyMarkerMachines,
581+
sessionUsageFingerprints, pushed, preloadComparisons,
582+
)
583+
if err == nil || !errors.Is(err, errPushComparisonPreload) {
584+
return result, err
585+
}
586+
log.Printf(
587+
"pgsync: preloading pg comparison fingerprints failed, "+
588+
"retrying batch without preload: %v",
589+
err,
590+
)
591+
return s.pushBatchAttempt(
592+
ctx, batch, full, markerID, legacyMarkerMachines,
593+
sessionUsageFingerprints, pushed, false,
594+
)
595+
}
596+
597+
func (s *Sync) pushBatchAttempt(
598+
ctx context.Context,
599+
batch []db.Session,
600+
full bool,
601+
markerID string,
602+
legacyMarkerMachines []string,
603+
sessionUsageFingerprints map[string]string,
604+
pushed *[]db.Session,
605+
preloadComparisons bool,
573606
) (batchResult, error) {
574607
tx, err := s.pg.BeginTx(ctx, nil)
575608
if err != nil {
@@ -586,19 +619,17 @@ func (s *Sync) pushBatch(
586619
sessionIDs = append(sessionIDs, sess.ID)
587620
}
588621
comparisons := (*pushMessageComparison)(nil)
589-
if len(sessionIDs) > 0 && !full {
622+
if preloadComparisons && len(sessionIDs) > 0 {
590623
comparisonsBatch, err := readPushSessionMessageComparisons(
591624
ctx, tx, sessionIDs,
592625
)
593626
if err != nil {
594-
log.Printf(
595-
"pgsync: preloading pg comparison fingerprints: %v",
596-
err,
627+
_ = tx.Rollback()
628+
return batchResult{}, fmt.Errorf(
629+
"%w: %w", errPushComparisonPreload, err,
597630
)
598-
comparisons = nil
599-
} else {
600-
comparisons = comparisonsBatch
601631
}
632+
comparisons = comparisonsBatch
602633
}
603634

604635
for _, sess := range batch {
@@ -1315,46 +1346,46 @@ func (s *Sync) pushMessages(
13151346
return 0, nil
13161347
}
13171348

1318-
var pgCount int
1319-
var pgContentSum, pgContentMax, pgContentMin int64
1320-
// Exact string fingerprint for the system-message ordinal set:
1321-
// STRING_AGG produces e.g. "0,2,5" — impossible to collide for
1322-
// distinct ordinal sets (unlike SUM or SUM+SUM-of-squares).
1323-
var pgSystemFP sql.NullString
1324-
var pgToolCallCount int
1325-
var pgTCContentSum int64
1326-
if err := tx.QueryRowContext(ctx,
1327-
`SELECT COUNT(*),
1328-
COALESCE(SUM(content_length), 0),
1329-
COALESCE(MAX(content_length), 0),
1330-
COALESCE(MIN(content_length), 0),
1331-
STRING_AGG(ordinal::text, ',' ORDER BY ordinal)
1332-
FILTER (WHERE is_system)
1333-
FROM messages
1334-
WHERE session_id = $1`,
1335-
sessionID,
1336-
).Scan(
1337-
&pgCount, &pgContentSum,
1338-
&pgContentMax, &pgContentMin,
1339-
&pgSystemFP,
1340-
); err != nil {
1341-
return 0, fmt.Errorf(
1342-
"counting pg messages: %w", err,
1343-
)
1344-
}
1345-
if err := tx.QueryRowContext(ctx,
1346-
`SELECT COUNT(*),
1347-
COALESCE(SUM(result_content_length), 0)
1348-
FROM tool_calls
1349-
WHERE session_id = $1`,
1350-
sessionID,
1351-
).Scan(&pgToolCallCount, &pgTCContentSum); err != nil {
1352-
return 0, fmt.Errorf(
1353-
"counting pg tool_calls: %w", err,
1354-
)
1349+
pgAgg, pgToolAgg, hasPreloadedComparisons := comparisonAggregates(
1350+
sessionID, comparisons,
1351+
)
1352+
if !hasPreloadedComparisons {
1353+
if err := tx.QueryRowContext(ctx,
1354+
`SELECT COUNT(*),
1355+
COALESCE(SUM(content_length), 0),
1356+
COALESCE(MAX(content_length), 0),
1357+
COALESCE(MIN(content_length), 0),
1358+
COALESCE(
1359+
STRING_AGG(ordinal::text, ',' ORDER BY ordinal)
1360+
FILTER (WHERE is_system),
1361+
''
1362+
)
1363+
FROM messages
1364+
WHERE session_id = $1`,
1365+
sessionID,
1366+
).Scan(
1367+
&pgAgg.Count, &pgAgg.Sum,
1368+
&pgAgg.Max, &pgAgg.Min,
1369+
&pgAgg.SysFP,
1370+
); err != nil {
1371+
return 0, fmt.Errorf(
1372+
"counting pg messages: %w", err,
1373+
)
1374+
}
1375+
if err := tx.QueryRowContext(ctx,
1376+
`SELECT COUNT(*),
1377+
COALESCE(SUM(result_content_length), 0)
1378+
FROM tool_calls
1379+
WHERE session_id = $1`,
1380+
sessionID,
1381+
).Scan(&pgToolAgg.Count, &pgToolAgg.Sum); err != nil {
1382+
return 0, fmt.Errorf(
1383+
"counting pg tool_calls: %w", err,
1384+
)
1385+
}
13551386
}
13561387

1357-
if !full && pgCount == localCount && pgCount > 0 {
1388+
if !full && pgAgg.Count == localCount && pgAgg.Count > 0 {
13581389
localFP := pushLocalMessageFingerprint{}
13591390

13601391
localFP.Sum, localFP.Max, localFP.Min, err = s.local.MessageContentFingerprint(
@@ -1490,15 +1521,15 @@ func (s *Sync) pushMessages(
14901521
)
14911522
}
14921523

1493-
if localFP.Sum == pgContentSum &&
1494-
localFP.Max == pgContentMax &&
1495-
localFP.Min == pgContentMin &&
1524+
if localFP.Sum == pgAgg.Sum &&
1525+
localFP.Max == pgAgg.Max &&
1526+
localFP.Min == pgAgg.Min &&
14961527
localFP.ContentHashFP == pgContentHashFP &&
14971528
localFP.RoleTimeFP == pgRoleTimeFP &&
14981529
localFP.FlagsFP == pgFlagsFP &&
1499-
localFP.SystemFP == pgSystemFP.String &&
1500-
localFP.ToolCallCount == pgToolCallCount &&
1501-
localFP.ToolCallSum == pgTCContentSum &&
1530+
localFP.SystemFP == pgAgg.SysFP &&
1531+
localFP.ToolCallCount == pgToolAgg.Count &&
1532+
localFP.ToolCallSum == pgToolAgg.Sum &&
15021533
localFP.ToolCallFP == pgTCFP &&
15031534
localFP.TokenFP == pgTokenFP &&
15041535
localFP.UsageEventFP == pgUsageFP {

internal/postgres/push_fingerprint.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ type pushLocalMessageFingerprint struct {
5252
UsageEventFP string
5353
}
5454

55+
func comparisonAggregates(
56+
sessionID string,
57+
comparisons *pushMessageComparison,
58+
) (pushMessageAggregate, pushToolCallAggregate, bool) {
59+
if comparisons == nil {
60+
return pushMessageAggregate{}, pushToolCallAggregate{}, false
61+
}
62+
return comparisons.MessageAggregates[sessionID],
63+
comparisons.ToolCallAggregates[sessionID],
64+
true
65+
}
66+
5567
func readPushSessionMessageComparisons(
5668
ctx context.Context,
5769
tx *sql.Tx,

internal/postgres/push_fingerprint_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,29 @@ func TestShouldSkipSessionMessagesGuardsCountAndNilMaps(t *testing.T) {
5050
"sess", 2, localFP, false, comparisons,
5151
))
5252
}
53+
54+
func TestComparisonAggregates(t *testing.T) {
55+
msgAgg, toolAgg, ok := comparisonAggregates("missing", nil)
56+
assert.False(t, ok)
57+
assert.Equal(t, pushMessageAggregate{}, msgAgg)
58+
assert.Equal(t, pushToolCallAggregate{}, toolAgg)
59+
60+
comparisons := &pushMessageComparison{
61+
MessageAggregates: map[string]pushMessageAggregate{
62+
"sess": {Count: 3, Sum: 9, Max: 5, Min: 1, SysFP: "0,2"},
63+
},
64+
ToolCallAggregates: map[string]pushToolCallAggregate{
65+
"sess": {Count: 2, Sum: 11},
66+
},
67+
}
68+
69+
msgAgg, toolAgg, ok = comparisonAggregates("sess", comparisons)
70+
require.True(t, ok)
71+
assert.Equal(t,
72+
pushMessageAggregate{
73+
Count: 3, Sum: 9, Max: 5, Min: 1, SysFP: "0,2",
74+
},
75+
msgAgg,
76+
)
77+
assert.Equal(t, pushToolCallAggregate{Count: 2, Sum: 11}, toolAgg)
78+
}

0 commit comments

Comments
 (0)