Skip to content

perf: reduce DuckDB push write amplification - #992

Merged
wesm merged 10 commits into
mainfrom
duckdb-write-perf
Jul 4, 2026
Merged

perf: reduce DuckDB push write amplification#992
wesm merged 10 commits into
mainfrom
duckdb-write-perf

Conversation

@cpcloud

@cpcloud cpcloud commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

DuckDB mirror pushes now avoid rewriting session dependents when the target already matches local message, tool, result-event, and usage fingerprints. Suffix-only active-session growth appends just the new messages and dependent rows, while historical or dependent-row mismatches still fall back to a full replacement.

The skip and append fast paths refresh per-session pinned messages and secret findings in the same mutation transaction, so a later partial push failure cannot leave those dependent rows stale while persisting fresh fingerprints.

Incremental DuckDB pushes also repair cached fingerprints whose mirror rows are missing by re-adding the unchanged local session to the push candidates before skip filtering. That keeps retry and partial-failure cleanup from advancing the watermark while the mirror is still missing a session.

The DuckDB backend runs threshold-gated checkpoint maintenance after committed mutating pushes, before local watermark advancement. PostgreSQL skip fingerprints now include tool-result events so both push backends agree on stale event detection.

The main review points are internal/duckdb/sync.go, internal/duckdb/push.go, internal/duckdb/push_fingerprint.go, internal/duckdb/checkpoint.go, and the lifecycle coverage in internal/duckdb/sync_fastpath_test.go. Checkpointing is intentionally partial reclamation; large historical rewrites may still require full-copy compaction outside the normal push path.

DuckDB mirror pushes rewrote a session's dependent rows whenever a modified session reached the push window, even when only metadata changed or an active transcript had only appended a suffix. That caused avoidable single-file churn and left reclaimed blocks for later DuckDB maintenance.

Add DuckDB push fingerprints for messages, tool calls, tool-result events, and usage events; skip unchanged message dependents; append only verified suffix messages for active-session growth; and fall back to full replacement on any historical or dependent-row mismatch. Keep PostgreSQL skip guards in parity by including tool-result events there too.

Run threshold-gated DuckDB CHECKPOINT maintenance after committed mutating pushes but before advancing local push state. Cover the lifecycle with tests that assert preserved row IDs, usage refresh, result-event fallback, and checkpoint gating.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (65173fd)

Summary verdict: one Medium issue needs fixing before merge.

Medium

  • internal/duckdb/push_fingerprint.go:180 - The new DuckDB message fast-path fingerprint excludes messages.id, even though DuckDB mirrors that ID and dependent mirror tables rely on it. A local full rewrite can reassign message IDs while leaving ordinals/content/tool metadata unchanged, causing the fast path to skip rewriting messages. A later curation refresh can then copy pins with the new local message_id while DuckDB still has old message IDs, breaking pins and ID-based joins.
    • Fix: Include messages.id in the DuckDB message fingerprint comparison, or force a full dependent rewrite whenever mirrored message IDs differ.

Reviewers: 2 done | Synthesis: codex, 7s | Total: 8m8s

cpcloud added 4 commits July 4, 2026 11:58
DuckDB append and skip decisions now include the mirrored message id stream in the local-vs-target fingerprint. That prevents curation refreshes from copying pins with newly assigned local message ids while DuckDB messages still carry an older id assignment.

The regression test rewrites a local session with identical transcript content but new message ids, then verifies the incremental push falls back to a dependent-row rewrite and leaves pinned messages joined to mirrored rows.
DuckDB checkpoint maintenance is a local-file concern. Quack pushes route storage through a remote server, so running pragma_database_size or CHECKPOINT on the client DuckDB handle can target the wrong catalog or fail after mutations have committed but before the push watermark advances.

Gate the checkpoint hook for remote targets and cover the gate with a regression test. Also remove misleading timestamp plumbing from the fast-path lifecycle test helper; DataVersion writes already stamp local_modified_at through the local store.
The quack checkpoint gate needs Push-level coverage because the failure mode happens after remote mutations commit and before local sync state is finalized. Extend the existing real quack push smoke test to assert that a successful remote push persists the watermark.
Legacy or manually repaired DuckDB mirrors can contain message rows where id is NULL even though normal pushes write the mirrored local primary key. The new message-id fingerprint should treat that as a mismatch to repair, not as a scan error that aborts incremental push.

Scan message ids as nullable and add regression coverage that corrupts a mirrored id to NULL, then verifies the next incremental push rewrites the session and restores the local id.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (3e83740)

Medium issue found: the new fast-path tests may miss the intended incremental push path due to stale local modification timestamps.

Medium

  • internal/duckdb/sync_fastpath_test.go:395 and internal/duckdb/sync_fastpath_test.go:464
    Helpers rewrite session messages but leave LocalModifiedAt at the old fixed fixture timestamp. After the preceding push advances the DuckDB watermark to time.Now(), the next incremental Push(false) will not select the session, so the tests expecting a rewrite/push can fail without exercising the intended path.
    Fix: Set sess.LocalModifiedAt = time.Now().UTC().Format(localSyncTimestampLayout) before WriteSessionBatchAtomic in both helpers.

Reviewers: 2 done | Synthesis: codex, 7s | Total: 8m58s

@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (2894ebe)

Medium: The PR has one actionable medium-severity issue around pinned-message refresh behavior in DuckDB fast paths.

  • Medium - internal/duckdb/push.go:244
    • Problem: The new skip/append fast paths update sessions, messages, usage, and secret findings, but do not replace per-session pinned rows. Pinned rows are only refreshed later by the global curation pass, which is skipped when any session push error occurs. finalizeState can still persist fingerprints for successfully pushed sessions, so a pin-only change can be filtered out on later retries without ever reaching DuckDB.
    • Fix: Mirror pinned messages inside the skip and append fast paths with a delete+insert helper, or avoid persisting pushed fingerprints unless the curation refresh ran successfully.

Reviewers: 2 done | Synthesis: codex, 9s | Total: 7m5s

DuckDB incremental push tests rewrite local sessions after the initial push watermark has advanced. Leaving fixture LocalModifiedAt values at their original timestamps meant those tests could skip the session selection path they were intended to exercise.

Set fresh local modification times in the rewrite helpers so the follow-up incremental pushes deterministically cover the message-id repair and lifecycle fallback paths.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (03f46ea)

Medium issue found; no Critical or High findings.

  • Medium - internal/duckdb/push.go:244
    The new DuckDB fast-path branches return before reconciling pinned_messages for the session. If Push later skips the global curation refresh because another session errors, successfully pushed sessions may still have their boundary fingerprints written. A pin-only or append session can then be marked up-to-date while DuckDB pins remain stale.

    Suggested fix: Reconcile pinned messages in the skip/append branches with a delete-and-reinsert helper, or avoid recording the pushed fingerprint until curation refresh succeeds.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 8m52s

The previous review fix wrote Session.LocalModifiedAt in helpers that use WriteSessionBatchAtomic, but that field is ignored by the session upsert. These helper writes already pass DataVersion, which is the path that stamps local_modified_at for incremental push selection.

Remove the inert assignments and document the DataVersion behavior at the write sites so the tests continue to rely on the persisted timestamp mechanism they actually exercise.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (25d39eb)

Medium issue found; no High or Critical findings.

Medium

  • internal/duckdb/sync.go:348 — A checkpoint failure after deleteHardDeletedMirrorSessions commits returns before finalizeState, so deleted session IDs are only removed from priorFingerprints in memory. On the next retry, those IDs are no longer present in DuckDB, making staleIDs empty and allowing old fingerprints to be copied forward. If the local session is later restored unchanged, filterUnchangedSessions can skip it while the mirror still lacks the row.
    • Suggested fix: Prune cached fingerprints against the current local session ID set during finalize/retry, or persist stale-fingerprint cleanup even when checkpointing fails.

Reviewers: 2 done | Synthesis: codex, 14s | Total: 11m54s

A checkpoint failure can happen after hard-deleted DuckDB rows have already been removed but before the push boundary fingerprints are finalized. On the next retry those rows are no longer reported as stale, so old cached fingerprints could be copied forward and later suppress a restored or missing-row session push.

Delete reconciliation now returns the mirror session IDs that remain after stale cleanup, and incremental skip filtering runs only after cached fingerprints have been pruned against that mirror state. A regression covers the checkpoint-failure retry path without advancing the watermark.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (8c2d6e6)

Medium finding remains after deduplication; no High or Critical issues reported.

Medium

  • internal/duckdb/sync.go:307 - Missing mirror fingerprints are pruned after the push candidate list is built from the modified-time window. If a local session still exists but its DuckDB row is missing and it has not changed since lastPush, its fingerprint is removed, the watermark can advance, and the session is never re-added to sessions, leaving the mirror missing that row until a full push or later local modification.

    Fix: When a pruned ID still exists in the in-scope local session set, add it to the push candidates before fingerprint filtering, or force a full push / avoid advancing the watermark; only silently prune IDs that are truly gone locally.


Reviewers: 2 done | Synthesis: codex, 6s | Total: 6m38s

Incremental DuckDB pushes could prune a cached fingerprint after discovering a missing mirror row, then advance the watermark without adding the unchanged local session back to the push candidate set. That left the mirror missing the row until a full push or later local modification.

Treat pruned fingerprints that still have in-scope local sessions as repair candidates before fingerprint filtering, so the normal push path reinserts the session and dependents. The regression test deletes a mirrored row after a successful full push and verifies the next incremental push restores it.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (91a91cb)

Medium issue found; no High or Critical findings.

Medium

  • internal/duckdb/push.go:244 - Incremental skip/append paths update sessions, usage, and secret findings, then return without refreshing pinned_messages. The normal end-of-push curation refresh would fix this, but it is skipped when any session push errors. As a result, successful fast-path sessions can persist a new fingerprint while their pins remain stale.

    Suggested fix: Refresh pinned messages per session in the skip and append paths, or avoid persisting pin-inclusive fingerprints unless the curation refresh succeeds.


Reviewers: 2 done | Synthesis: codex, 6s | Total: 9m20s

DuckDB skip and append fast paths updated the session, usage, and secret rows but depended on the later global curation refresh to mirror pin changes. If another session failed in the same push, that refresh was skipped while successful sessions could still have their fingerprints persisted.

Refresh pinned_messages for the session inside the skip and append transactions, and reuse that session-scoped helper for filtered curation refreshes. The new regressions call the fast paths directly after changing a pin so the global curation pass cannot mask stale mirrored pins.
@roborev-ci

roborev-ci Bot commented Jul 4, 2026

Copy link
Copy Markdown

roborev: Combined Review (cf41997)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 7m11s

@wesm
wesm merged commit 3ce0acd into main Jul 4, 2026
19 checks passed
@wesm
wesm deleted the duckdb-write-perf branch July 4, 2026 20:09
cpcloud added a commit that referenced this pull request Jul 7, 2026
Session push fingerprints included local stat fields that can churn on no-op rewrites or file touches, making unchanged sessions push candidates and rewriting mirror rows.

Exclude file_mtime/local_modified_at in PostgreSQL and file_size/file_mtime/file_inode/file_device/local_modified_at in DuckDB while keeping file_path and the content-derived file_hash. Mirror write paths still write every stat column when a session really pushes, but those mirror stat columns can now go stale while session content is unchanged.

Stored fingerprints from older builds use the previous field set, so the first push after upgrade will re-push sessions once before the new fingerprints settle, matching the upgrade shape of the earlier #992 fingerprint change.
cpcloud added a commit that referenced this pull request Jul 7, 2026
Session push fingerprints included local stat fields that can churn on no-op rewrites or file touches, making unchanged sessions push candidates and rewriting mirror rows.

Exclude file_mtime/local_modified_at in PostgreSQL and file_size/file_mtime/file_inode/file_device/local_modified_at in DuckDB while keeping file_path and the content-derived file_hash. Mirror write paths still write every stat column when a session really pushes, but those mirror stat columns can now go stale while session content is unchanged.

Stored fingerprints from older builds use the previous field set, so the first push after upgrade will re-push sessions once before the new fingerprints settle, matching the upgrade shape of the earlier #992 fingerprint change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants