Skip to content

Fix deduplicate plan timeout: index + batch RFC822 duplicate-group lookup - #512

Draft
jesserobbins wants to merge 4 commits into
kenn-io:mainfrom
jesserobbins:fix/dedup-rfc822-n1
Draft

Fix deduplicate plan timeout: index + batch RFC822 duplicate-group lookup#512
jesserobbins wants to merge 4 commits into
kenn-io:mainfrom
jesserobbins:fix/dedup-rfc822-n1

Conversation

@jesserobbins

Copy link
Copy Markdown
Contributor

Motivation

Running deduplicate --content-hash on my own archive, the CLI died after 30 minutes with context deadline exceeded and zero output. Traced it to an N+1 query with no supporting index — Scan was querying the store once per RFC822 duplicate group, and on a real archive that's tens of thousands of round trips before content-hash comparison even starts.

Summary

  • Add a plain index on messages.rfc822_message_id — a partial index (mirroring the existing attachment-hash indexes) turns out to be unusable by SQLite's planner for this query shape, verified via EXPLAIN QUERY PLAN before picking the plain form.
  • Add Store.GetDuplicateGroupMessagesBatch, replacing the per-group query with a handful of chunked IN (...) queries.
  • Switch Engine.Scan to the batched method.

Fixes #510.

Test plan

  • New tests: index-usage assertion, batch/per-group equivalence across multiple chunk rounds, source-scoped filtering.
  • make test passes (one unrelated pre-existing failure in internal/config — a macOS /var vs /private/var symlink artifact in a file this PR never touches).
  • Real archive: deduplicate --collection local --content-hash --no-backup -v --dry-run against a personal archive with 161,113 duplicate groups. Before this fix, the equivalent run never completed — it hit the CLI's 30-minute client timeout with zero output, having made partial progress through roughly 22,025 of those groups. After: completes in ~311s.

Out of scope, tracked in #510 but not touched here: BackfillRFC822IDs writing to the DB unconditionally during /plan before confirmation, and the server not respecting client cancellation once a request times out client-side.


Authored by Jesse Robbins (@jesserobbins)

Fixes kenn-io#510. GetDuplicateGroupMessages ran one
unindexed query per RFC822 duplicate group; 22,025 groups at
~190ms/lookup burned the CLI's 30-minute plan-request timeout
before content-hash comparison even started.

Authored-By: Jesse Robbins (@jesserobbins) <https://jesserobbins.com>
Second half of the kenn-io#510 fix. Adds
GetDuplicateGroupMessagesBatch, turning N per-group queries into a
handful of chunked IN (...) queries. Engine.Scan is switched over in
the next commit; GetDuplicateGroupMessages itself is kept as the
equivalence test's reference implementation.

Authored-By: Jesse Robbins (@jesserobbins) <https://jesserobbins.com>
Completes the kenn-io#510 fix. Scan() no longer queries the
store once per RFC822 duplicate group; it fetches all groups' messages
in one batched, chunked call and looks them up from a map.

Authored-By: Jesse Robbins (@jesserobbins) <https://jesserobbins.com>
- Equivalence test now exercises sourceIDs + multi-chunk together,
  matching Engine.Scan's actual call shape (always non-empty sourceIDs).
- Fixed GetDuplicateGroupMessagesBatch's godoc; moved the "kept as
  equivalence-test reference" rationale onto GetDuplicateGroupMessages,
  which is what it actually describes.
- Extracted the shared SELECT column list into one const, removing
  ~13 lines of duplicated SQL between the two query methods.
- Engine.Scan nils msgsByGroup after use instead of holding it live
  through the content-hash comparison pass.

Authored-By: Jesse Robbins (@jesserobbins) <https://jesserobbins.com>
@roborev-ci

roborev-ci Bot commented Jul 26, 2026

Copy link
Copy Markdown

roborev: Combined Review (3ea7f4e)

Verdict: One medium-severity cancellation issue; no high or critical findings.

Medium

  • internal/dedup/dedup.go:331 — Batched fetch ignores Scan’s context. Cancellation was previously checked before each per-group query, but chunks now execute using background-context queries. This can allow substantial database work to continue after Ctrl-C or client disconnect.
    • Fix: Pass ctx into GetDuplicateGroupMessagesBatch, use QueryContext, and check for cancellation between chunks.

Reviewers: 2 done | Synthesis: codex, 8s | Total: 6m49s

@jesserobbins
jesserobbins marked this pull request as draft July 26, 2026 18:12
@jesserobbins

Copy link
Copy Markdown
Contributor Author

Holding off to do more testing

@jesserobbins

Copy link
Copy Markdown
Contributor Author

I am running into multiple issues on my super old email archive.

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.

deduplicate --content-hash plan times out — N+1 query on missing rfc822_message_id index

1 participant