Skip to content

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

Merged
wesm merged 2 commits into
kenn-io:mainfrom
jesserobbins:fix/dedup-rfc822-n1
Jul 29, 2026
Merged

Fix deduplicate plan timeout: index + batch RFC822 duplicate-group lookup#512
wesm merged 2 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)

@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.

@jesserobbins

Copy link
Copy Markdown
Contributor Author

Okay, was thrown off by #526.

@jesserobbins
jesserobbins marked this pull request as ready for review July 28, 2026 17:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ea7f4e9eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/dedup/dedup.go Outdated
Comment on lines +331 to +333
msgsByGroup, err := e.store.GetDuplicateGroupMessagesBatch(
rfc822IDs, e.config.AccountSourceIDs...,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor cancellation before batching duplicate rows

When the dedup plan context is canceled after FindDuplicatesByRFC822ID starts returning results (for example, the daemon client hits its long request timeout or the user cancels the CLI), this new batched call still runs every chunk because GetDuplicateGroupMessagesBatch/queryInChunks do not receive ctx, and Scan only checks ctx.Err() after all rows are materialized. Previously the per-group loop checked cancellation before each lookup, so at most one lookup ran after cancellation; please check/pass the context before and during the batch so canceled plans stop promptly.

Useful? React with 👍 / 👎.

@jesserobbins
jesserobbins marked this pull request as draft July 28, 2026 18:06
@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (33b1f50)

Medium-severity issue found in the batched deduplication path.

Medium

  • internal/dedup/dedup.go:331 — The new batch fetch runs every chunk with background contexts before checking ctx again. Previously, cancellation was checked before each group query, so a timed-out or disconnected request can now continue processing the entire archive.
    • Fix: Add context-aware batch and chunk-query methods using QueryContext, pass Scan’s context through, and check cancellation between chunks.

Reviewers: 2 done | Synthesis: codex, 7s | Total: 4m12s

jesserobbins and others added 2 commits July 29, 2026 08:44
GetDuplicateGroupMessages ran one unindexed query per RFC822 duplicate group; large archives could exhaust the CLI plan-request timeout before content-hash comparison began.

- perf: batch GetDuplicateGroupMessages into chunked queries
- perf: switch Engine.Scan to GetDuplicateGroupMessagesBatch
- refactor: address final review findings on dedup rfc822 fix
- merge current main before review

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

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
A timed-out dedup scan could continue issuing every RFC822 lookup chunk because the batching helper replaced the request context with a background context. Large archives therefore kept consuming database work after the caller disconnected.

Thread the scan context through the batch API and chunk queries so in-flight database work is interruptible and no later chunk starts after cancellation. The background-context wrapper remains for callers that do not yet carry request scope.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm force-pushed the fix/dedup-rfc822-n1 branch from 33b1f50 to 2c25920 Compare July 29, 2026 12:51
@roborev-ci

roborev-ci Bot commented Jul 29, 2026

Copy link
Copy Markdown

roborev: Combined Review (2c25920)

The batching and indexing changes appear sound, with no Medium, High, or Critical findings.


Reviewers: 2 done | Synthesis: codex, 19s | Total: 4m19s

@wesm
wesm marked this pull request as ready for review July 29, 2026 14:01
@wesm
wesm merged commit 0d93ca2 into kenn-io:main Jul 29, 2026
18 checks passed
wesm added a commit to salmonumbrella/msgvault that referenced this pull request Jul 30, 2026
* origin/main:
  Fix deduplicate plan timeout: index + batch RFC822 duplicate-group lookup (kenn-io#512)
  Index relationship analytics for fast, memory-bounded queries (kenn-io#528)
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

2 participants