Skip to content

S3 backup repository: stop writing and depending on directory marker objects - #5128

Open
elangelo wants to merge 9 commits into
apache:mainfrom
elangelo:handle-s3-restores-ignore-marker
Open

elangelo wants to merge 9 commits into
apache:mainfrom
elangelo:handle-s3-restores-ignore-marker

Conversation

@elangelo

@elangelo elangelo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #4182 (closed as stale, and it can't be reopened after the branch was rebased). See that PR for the earlier review discussion.

Description

The S3 backup repository simulated directories with zero-byte "directory marker" objects (keys ending in /, content type application/x-directory), and relied on them when restoring. Tools that copy buckets, such as aws s3 sync, only copy the real files and drop those markers. A backup copied that way could no longer be restored: the configset directory, among others, was reported as missing.

Solution

Following the review on #4182, the S3 repository no longer uses directory markers at all, rather than adding a fallback for when they are missing:

  • S3StorageClient.createDirectory() is a no-op. On S3 a directory is just a key prefix that has objects under it.
  • exists() on a directory path and isDirectory() use a single prefix listing (maxKeys=1). This finds markerless directories as well as marker objects left by older Solr versions, so existing backups can still be restored. deleteDirectory() also removes those old markers.
  • Writing a file no longer checks that its parent "directory" exists, and reading a file's length takes a single HEAD request again. Both mean fewer S3 requests.
  • Core backup/restore code keeps calling createDirectory()/exists(). That keeps LocalFileSystemRepository, which needs real parent directories, and the other repositories working as before.
  • BackupRepository gets supportsEmptyDirectories(), true by default and false for S3. A collection backup on such a repository accepts a location that does not exist yet, as it comes into existence with the first file of the backup; before, a backup to a new S3 prefix was refused until something was stored there. The location is still checked, so an unreachable bucket fails up front. Restore, list and delete still require the location to exist.

On S3 an empty directory can't exist any more. The shared AbstractBackupRepositoryTest contract assumed it could; it now asks the repository through supportsEmptyDirectories(), so S3 runs the shared tests instead of copying and overriding them.

Copilot (#4182) and Claude Code were used to develop this change.

Tests

All new tests run against S3Mock instead of Mockito mocks:

  • S3IncrementalBackupTest#testRestoreBackupCopiedWithoutDirectoryMarkers is the end-to-end regression test. It backs up a collection and checks the backup contains no marker objects. It then copies the backup file by file to another location, as aws s3 sync would, and restores it under a new configset name so the configset is uploaded from the backup. Finally it checks the configset and the document count.
  • S3IncrementalBackupTest#testBackupToLocationThatDoesNotExistYet backs up to a prefix that holds nothing yet, restores it, and checks that restoring from a missing location still fails.
  • S3BackupRepositoryTest#testLegacyDirectoryMarkers checks that a backup containing old-style markers can still be checked for existence, listed, read and fully deleted.
  • S3PathsTest, S3ReadWriteTest and S3BackupRepositoryTest are updated for markerless directories.

Ran :solr:modules:s3-repository:test -Ptests.nightly=true; all pass except S3InstallShardTest#testInstallSkipsWriteLockFile, which fails the same way on current main and is unrelated to this change. I also ran LocalFileSystemRepositoryTest and GCSBackupRepositoryTest to cover the shared contract test change, BackupRestoreApiErrorConditionsTest and TestLocalFSCloudBackupRestore to check that a local file system location must still exist, and check -x test on the changed modules.

Checklist

  • I have reviewed the guidelines for How to Contribute and my code conforms to the standards described there to the best of my ability.
  • I have created a Jira issue and added the issue ID to my pull request title.
  • I have given Solr maintainers access to contribute to my PR branch. (optional but recommended, not available for branches on forks living under an organisation)
  • I have developed this patch against the main branch.
  • I have run ./gradlew check.
  • I have added tests for my changes.
  • I have added documentation for the Reference Guide
  • I have added a changelog entry for my change

🤖 Generated with Claude Code

elangelo pushed a commit to elangelo/solr that referenced this pull request Oct 14, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
elangelo pushed a commit to elangelo/solr that referenced this pull request Oct 14, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@elangelo
elangelo force-pushed the handle-s3-restores-ignore-marker branch from 4d7c781 to 75585e9 Compare October 14, 2026 09:31
Samuel Verstraete and others added 8 commits October 14, 2026 11:36
When restoring a backup, use both `exists()` and `listAll()` to check
for the config directory. Object stores like S3 may not have an explicit
directory marker object, but the config files are still present under
that prefix. The previous check using only `exists()` would incorrectly
throw an error in such cases.
…configset

When a backup is copied through a local filesystem and re-uploaded to S3
(e.g. via `aws s3 sync`), the zero-byte directory marker objects created
by S3BackupRepository are typically lost. This caused restore to fail with
an error even though all configset files were present under the expected
key prefix.

Change BackupManager#uploadConfigDir to fall back to listing objects by
key prefix when the directory marker is absent, so restore succeeds as
long as the configset files themselves are present. A clear
IllegalArgumentException is still thrown when neither marker nor files
can be found.

- Add unit tests covering the no-marker, truly-absent, and normal cases
- Add documentation in the S3 backup/restore reference guide explaining
  the directory-marker behaviour and the graceful fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
S3StorageClient.isDirectory: when no explicit directory-marker object
exists (NoSuchKeyException), fall back to listing up to one object
under the path prefix. This handles "virtual directories" that arise
after an `aws s3 sync` round-trip, which silently drops zero-byte
directory marker keys, causing restore to fail with NoSuchKey 404.

CloudSolrClient: before computeIfAbsent for collectionRefreshes, evict
any already-completed future via computeIfPresent. Without this, a
stale done future is reused instead of submitting a fresh refresh,
so callers (e.g. stale-state retry) never observe an updated ref.

Also adds a unit test covering the virtual-directory fallback path and
a changelog entry for the S3 fix.
Replace all uses of `repository.exists()` and explicit `createDirectory()`
calls with virtual-directory convention throughout the backup/restore pipeline.

S3 does not require zero-byte marker objects for directories. Writing such
markers is an anti-pattern: tools like `aws s3 sync` silently drop them,
causing subsequent `exists()` checks to return false and breaking restore.

Changes:
- `S3StorageClient.createDirectory()` is now a no-op (no marker written)
- Remove `parentDirectoryExist()` guard from `pushStream()`; files can be
  written directly under any prefix
- Replace `repository.exists()` checks with `listAllOrEmpty()` length checks
  in `BackupCmd`, `BackupManager`, and `SnapShooter`
- `BackupFilePaths.createIncrementalBackupFolders()` and
  `BackupManager.createZkStateDir()` become no-ops
- `BackupManager.downloadConfigDir()` no longer pre-creates directories
- Consolidate two duplicate changelog entries into one

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…avior

S3 does not use explicit directory markers, so `exists()` on a directory
URI returns false even after `createDirectory()` — a directory is only
implied by the objects it contains.

- Update S3BackupRepositoryTest assertions to expect `false` from
  `exists()` on directories, and verify content is still listable
- Override `testCanDetermineWhetherFilesAndDirectoriesExist`,
  `testCanDistinguishBetweenFilesAndDirectories`,
  `testCanDeleteEmptyOrFullDirectories`, and `testCanListFullOrEmptyDirectories`
  with S3-specific expectations (no directory marker objects)
- Add `testSkipConfigset` override in S3IncrementalBackupTest to assert
  no explicit directory entries are created during S3 backup, while still
  verifying configset files are present/absent based on the backup flag
- Fix S3ReadWriteTest#testDirectoryLength to push a child file into the
  directory before testing, satisfying S3's implicit directory semantics

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review on PR apache#4182:

- Keep core backup/restore code unchanged: it still calls createDirectory()
  and exists(), so LocalFileSystemRepository (which needs real parent
  directories) keeps working. The S3 repository decides what a directory is.
- S3StorageClient: createDirectory() is a no-op; a directory is a key prefix
  with objects under it. exists() on a directory path and isDirectory() use
  a single prefix listing, which also finds legacy marker objects, so old
  backups still restore and get fully deleted. length() is back to a single
  HEAD request for regular files.
- Replace the Mockito-based BackupManagerUploadConfigTest with S3Mock tests:
  an end-to-end restore (configset included) of a backup copied without
  marker objects, and a test for backups containing legacy markers.
- AbstractBackupRepositoryTest: add supportsEmptyDirectories() so S3 can
  run the shared contract tests instead of overriding them.
- Drop the unrelated CloudSolrClient change; upstream rewrote that code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
S3 can't hold an empty directory, so a backup location only exists once
a file is stored under it, and a backup to a new prefix was refused.

BackupRepository gets supportsEmptyDirectories(), false for S3, replacing
the hook of the same name in AbstractBackupRepositoryTest. A collection
backup accepts a missing location on such a repository; the location is
still checked, so an unreachable bucket fails up front. Restore, list and
delete still require the location to exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant