8000 Add monthly full dumps of the search index to B2 by daoudclarke · Pull Request #537 · mwmbl/mwmbl · GitHub
[go: up one dir, main page]

Skip to content

Add monthly full dumps of the search index to B2 - #537

Merged
daoudclarke merged 7 commits into
mainfrom
claude/epic-johnson-ppm1oz
Oct 31, 2026
Merged

daoudclarke merged 7 commits into
mainfrom
claude/epic-johnson-ppm1oz

Conversation

@daoudclarke
@daoudclarke daoudclarke commented Oct 21, 2026 •
Copy link
Copy Markdown
Contributor

What changed

  • manage.py dump_index (mwmbl/indexer/dump.py):
    • Streams the whole index file, compressed with zstd, into one B2 object using a multipart upload. The part size scales with the file so the upload stays under the 10,000-part limit.
    • Reads each 4 MB chunk under a shared OFD lock on its byte range. Writers already take an exclusive lock on the page they change (TinyIndex.page), so no page in the dump is caught half-written. A half-written page would never decode again.
    • Writes a manifest (<object>.json) with the size and SHA-256 only after the upload finishes, so an object without a manifest is an upload that didn't finish.
    • Reads its credentials from MWMBL_DUMP_* environment variables when it runs, so the app still starts without them. It uses its own bucket and key, separate from the crawl bucket.
  • manage.py restore_index <key> <output>:
    • Downloads in 64 MB ranges with retries, so a dropped connection doesn't restart a 420 GB download.
    • Checks the size and SHA-256 against the manifest, then renames the file into place.
    • Refuses to overwrite an existing file.
  • app.json cron: runs the dump at 03:17 UTC on the 1st of each month, at low I/O priority.
  • docs/index-dumps.md: setup and restore steps.
  • moto added as a dev dependency for the tests.

Why

A dump does two jobs. It is the index's backup: the index is the only copy of seed crawls, curation and Staan copies, so rebuilding it from the crawl batches would recover only part of it. It is also the public release of the index under an open licence. That is why it is called a dump rather than a backup 10000 , and why it isn't encrypted: /page/{n} already serves the same data one page at a time. Hourly deltas of changed pages, tracked in Redis, would follow in a later PR.

How it was verified

  • make check passes. test/test_index_dump.py passes (13 tests).
  • New tests in test/test_index_dump.py, against moto's S3:
    • A multipart dump restores byte for byte, including across many ranges and after failed or short ranges.
    • The manifest is correct.
    • A restore that doesn't match its manifest isn't put in place, and a restore won't overwrite an existing file.
    • The reader waits for a writer holding a page. With the lock removed, this test fails.
  • Locally, the reader plus zstd handles about 290 MB/s, so the upload will be the bottleneck.

Before merging

  • Do the B2 setup in docs/index-dumps.md: create the public bucket and a key without delete rights, then set the environment variables.
  • Run one dump by hand.
  • Confirm that the Dokku cron container gets the /app/storage mount and DJANGO_SETTINGS_MODULE.

Generated by Claude Code

@codecov
codecov Bot commented Oct 21, 2026 •
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@daoudclarke
daoudclarke added this pull request to stack #568 October 30, 2026 13:53
@daoudclarke daoudclarke changed the title Add monthly full backups of the search index to B2 Add monthly full dumps of the search index to B2 Oct 30, 2026
@daoudclarke
daoudclarke force-pushed the claude/epic-johnson-ppm1oz branch from 6722bc6 to a49d0ff Compare October 31, 2026 09:01
The index is the only copy of seed crawls, curation and Staan copies, so it
cannot be rebuilt from the crawl batches. backup_index streams the whole file,
zstd-compressed and SSE-C encrypted, to a bucket of its own, reading each 4 MB
chunk under a shared lock on its byte range so no page is caught half-written.
A manifest with the size and SHA-256 is written once the upload finishes.

restore_index downloads in ranges, checks the result against the manifest and
only then renames it into place. A Dokku cron entry runs the backup monthly;
docs/index-backup.md covers setup and restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jZWrRVSzD1uuafxpb8fW2
…mory

From review of the backup PR:
- botocore retries a request but not an error reading its body, so a dropped
  connection killed the whole restore. Each range is now retried with backoff,
  and a short body counts as a failure.
- A failed restore removes its .partial file instead of leaving up to 420 GB
  on the live volume and blocking the next restore; a killed one's is written
  over.
- Chunks are 64 pages, not 1024, so writers wait on a shared lock for one
  quick pread. The cron runs at best-effort priority 7 rather than idle, which
  could leave a lock held while search I/O kept the disk busy.
- s3transfer buffered ten 64 MB parts of the non-seekable stream; now five.
- Docs: retention by a lifecycle rule on the prefix, since each backup is a
  new file; cancelling unfinished uploads; checking SSE-C against real B2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jZWrRVSzD1uuafxpb8fW2
Brings the backup module and both management commands to full coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4oCL1aRJtAnHhPyPct
The release check counts any pyproject.toml or uv.lock change as a crawler change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4oCL1aRJtAnHhPyPct
The index is going to be released under an open licence from the same bucket, and /page
already serves every document, so encryption protects nothing and losing the key would
lose every copy. "Dump" fits an object that is both the backup and the public release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4ofi2yAG1Bpw5cakYG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4ofi2yAG1Bpw5cakYG
@daoudclarke
daoudclarke force-pushed the claude/epic-johnson-ppm1oz branch from a49d0ff to 98c4374 Compare October 31, 2026 11:20
daoudclarke pushed a commit that referenced this pull request Oct 31, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTbu4ofi2yAG1Bpw5cakYG
@daoudclarke
daoudclarke merged commit a956f6b into main Oct 31, 2026
7 checks passed
@daoudclarke
daoudclarke deleted the claude/epic-johnson-ppm1oz branch October 31, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0