feat: add manifest.bolt.gz during upload to eliminate S3 Walk during restore#1405
feat: add manifest.bolt.gz during upload to eliminate S3 Walk during restore#1405minguyen9988 wants to merge 4 commits into
Conversation
During upload, record every file (path, size, last-modified) in a manifest.json stored alongside metadata.json. During restore, download the manifest first and use it to enumerate files directly via GetObject, skipping the expensive recursive Walk (ListObjectsV2). For a backup with 50k files across 500 parts, this eliminates ~50 ListObjectsV2 pages of 1000 keys each, replacing them with a single GetObject for the ~2MB manifest. Key design decisions: - Manifest is built incrementally during upload (thread-safe via mutex) - Graceful fallback: older backups without manifest.json transparently fall back to Walk - Manifest upload failure is non-fatal (logged as warning) - ManifestEntry implements RemoteFile interface for seamless integration with existing download code paths - No changes to compressed (tar.gz) backup format — manifest optimization applies to DirectoryFormat where Walk is the bottleneck Closes Altinity#1375
|
CI failures are pre-existing infrastructure issues, not related to this PR. Both failing jobs failed before reaching any test code:
The Could a maintainer please re-run the failed jobs? (I don't have admin rights to trigger re-runs on this repo.) |
|
Updated CI analysis after all jobs completed. Result: 0 failures are related to the manifest changes in this PR. Full breakdown of all 14 failed Test jobs:
Positive signal: The manifest code IS running successfully in integration tests that got far enough to execute upload paths: No manifest-related errors in any job's logs. |
…eature/manifest-skip-walk
|
Scalability concerns with the current implementation:
I'll try to rework the manifest as a bbolt file (same library we already use for resumable |
The JSON manifest didn't scale to backups with millions of files: indented uncompressed JSON grew to hundreds of MB in the bucket, was fully loaded into memory on both upload and restore, and FilesUnderPrefix was a linear scan per part, O(parts x files). Rework the manifest as a bbolt database (same library as resumable .state2), gzip-compressed for transfer: - upload: ManifestWriter batches file paths into a local temp bolt file, 10k keys per transaction, so memory is bounded by batch size - restore: ManifestReader opens the decompressed bolt file read-only (mmap), per-part lookup is a sorted-key prefix seek instead of a full scan - store only file paths as keys: size and mtime were never read on the restore path, dropping them also removes the per-file os.Stat during upload - keep graceful fallback to Walk when the manifest is missing or unreadable, and keep manifest failures non-fatal for the backup
|
thats a very good idea actually, let me know when you finish, I will get this idea in. Yes we should not load to memory everything, it should be like a cache that spill to disk type of in mem DB |
Summary
Implements #1375: upload-time file manifest to eliminate the expensive S3
ListObjectsV2Walk during restore.Problem
During
restore_remote, each part directory triggers aWalk(recursiveListObjectsV2) to enumerate files before downloading. For large backups with thousands of parts, this produces hundreds of paginated API calls (1000 keys per page), adding significant latency — often minutes — before any actual data transfer begins.Solution
During
upload, record every file's relative path, size, and last-modified time in amanifest.jsonfile stored alongsidemetadata.jsonin the backup root. Duringrestore_remote, download this single manifest file first and use it to enumerate files directly viaGetObject, completely skipping the Walk.Performance Impact
For a backup with 50,000 files across 500 parts:
ListObjectsV2calls (1 per part × ~1 page each) + pagination overhead = ~50 paginated requests at the S3 levelGetObjectfor the ~2MB manifest.jsonDesign Decisions
manifest.jsontransparently fall back to the existing Walk behavior — full backward compatibilityManifestEntryimplementsRemoteFileinterface: Manifest entries integrate seamlessly with existing download code pathsmanifest.jsonincludes a version field for future format evolutionFiles Changed
pkg/storage/manifest.goDownloadPathWithManifestpkg/storage/manifest_test.gopkg/backup/backuper.gofileManifestfield + thread-safe recording helperspkg/backup/upload.gopkg/backup/download.gomanifest.json Format
{ "version": 1, "backup_name": "my_backup_2025", "created_at": "2025-06-01T12:00:00Z", "total_size": 1073741824, "total_files": 5000, "files": [ { "path": "shadow/default/orders/default/20250101_1_1_0/data.bin", "size": 1048576, "last_modified": "2025-06-01T12:00:00Z" } ] }Testing
RemoteFileinterface conversion, capacity pre-allocation, large file counts (10k/50k)go vetclean on bothpkg/storage/andpkg/backup/Closes #1375