From ddd4c0026c8ee5a836fae39af157cba0bdd54ec7 Mon Sep 17 00:00:00 2001 From: Alpha <2748109647@qq.com> Date: Thu, 15 Oct 2026 06:15:22 +0800 Subject: [PATCH] Retry Swift container cleanup after stale listings --- .../OpenStackSwiftBlobStore.java | 35 +++++++++ ...penStackSwiftContainerDeleteRetryTest.java | 75 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/test/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftContainerDeleteRetryTest.java diff --git a/src/main/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftBlobStore.java b/src/main/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftBlobStore.java index 415a96f8a..c49abee41 100644 --- a/src/main/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftBlobStore.java +++ b/src/main/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftBlobStore.java @@ -188,6 +188,8 @@ public final class OpenStackSwiftBlobStore implements BlobStore { private static final int STATUS_CONFLICT = 409; private static final int STATUS_PRECONDITION_FAILED = 412; private static final int STATUS_RANGE_NOT_SATISFIABLE = 416; + private static final int DELETE_CONTAINER_MAX_ATTEMPTS = 6; + private static final long DELETE_CONTAINER_RETRY_BASE_MILLIS = 100L; /** * Write an HTTP date, which RFC 7231 fixes the width of: the day of month @@ -692,6 +694,39 @@ private boolean containsOnlyOrphanSegments(String container) { } } + @Override + public void deleteContainer(String container) { + deleteContainerWithRetries( + () -> BlobStore.super.deleteContainer(container)); + } + + static void deleteContainerWithRetries(Runnable deleteAttempt) { + for (int attempt = 0; attempt < DELETE_CONTAINER_MAX_ATTEMPTS; + attempt++) { + try { + deleteAttempt.run(); + return; + } catch (S3Exception se) { + if (!"BucketNotEmpty".equals(S3Exceptions.errorCode(se)) || + attempt + 1 == DELETE_CONTAINER_MAX_ATTEMPTS) { + throw se; + } + // Swift acknowledges writes before every container listing + // reflects them. Re-running the full clear after a short, + // bounded delay lets late entries become visible before the + // next delete attempt. + try { + Thread.sleep(DELETE_CONTAINER_RETRY_BASE_MILLIS << + attempt); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + se.addSuppressed(ie); + throw se; + } + } + } + } + @Override public void deleteBucket(String container) { var response = deleteContainerResponse(container); diff --git a/src/test/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftContainerDeleteRetryTest.java b/src/test/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftContainerDeleteRetryTest.java new file mode 100644 index 000000000..73dc3fed7 --- /dev/null +++ b/src/test/java/org/gaul/s3proxy/openstackswift/OpenStackSwiftContainerDeleteRetryTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2026 Andrew Gaul + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.gaul.s3proxy.openstackswift; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.gaul.s3proxy.blobstore.S3Exceptions; +import org.junit.jupiter.api.Test; + +import software.amazon.awssdk.services.s3.model.S3Exception; + +public final class OpenStackSwiftContainerDeleteRetryTest { + + @Test + public void testRetriesBucketNotEmptyAndSucceeds() { + var attempts = new AtomicInteger(); + + OpenStackSwiftBlobStore.deleteContainerWithRetries(() -> { + if (attempts.incrementAndGet() < 3) { + throw S3Exceptions.bucketNotEmpty("container"); + } + }); + + assertThat(attempts).hasValue(3); + } + + @Test + public void testDoesNotRetryOtherErrors() { + var attempts = new AtomicInteger(); + var failure = S3Exception.builder() + .message("temporary service failure") + .statusCode(503) + .build(); + + assertThatThrownBy(() -> + OpenStackSwiftBlobStore.deleteContainerWithRetries(() -> { + attempts.incrementAndGet(); + throw failure; + })).isSameAs(failure); + + assertThat(attempts).hasValue(1); + } + + @Test + public void testBoundsRetriesWhenContainerRemainsNonEmpty() { + var attempts = new AtomicInteger(); + + assertThatThrownBy(() -> + OpenStackSwiftBlobStore.deleteContainerWithRetries(() -> { + attempts.incrementAndGet(); + throw S3Exceptions.bucketNotEmpty("container"); + })) + .isInstanceOf(S3Exception.class) + .hasMessageContaining("container is not empty"); + + assertThat(attempts).hasValue(6); + } +}