This is an automated email from the ASF dual-hosted git repository.

fmariani pushed a commit to branch ci-fix/test-infra-container-retry
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 9e6a049ba9d2249e7fbff69e61eb96a9dc9b9a5f
Author: Croway <[email protected]>
AuthorDate: Thu Apr 2 13:49:24 2026 +0200

    ci: retry test-infra container initialization on transient failures
    
    Add retry with exponential backoff and jitter to 
TestServiceUtil.tryInitialize()
    for ContainerFetchException (image pull 404) and ContainerLaunchException
    (container start timeout). Configurable via system properties
    camel.test.infra.container.retries (default 3) and
    camel.test.infra.container.retry.delay.ms (default 5000).
---
 .../infra/common/services/TestServiceUtil.java     |  40 ++++++-
 .../common/services/TestServiceUtilRetryTest.java  | 133 +++++++++++++++++++++
 2 files changed, 168 insertions(+), 5 deletions(-)

diff --git 
a/test-infra/camel-test-infra-common/src/main/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
 
b/test-infra/camel-test-infra-common/src/main/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
index ea40bb13e337..2460c4083a6f 100644
--- 
a/test-infra/camel-test-infra-common/src/main/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
+++ 
b/test-infra/camel-test-infra-common/src/main/java/org/apache/camel/test/infra/common/services/TestServiceUtil.java
@@ -17,33 +17,63 @@
 
 package org.apache.camel.test.infra.common.services;
 
+import java.util.concurrent.ThreadLocalRandom;
+
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.ContainerFetchException;
+import org.testcontainers.containers.ContainerLaunchException;
 
 /**
  * Utility class for the test services
  */
 public final class TestServiceUtil {
     private static final Logger LOG = 
LoggerFactory.getLogger(TestServiceUtil.class);
+    private static final int MAX_RETRIES = 
Integer.getInteger("camel.test.infra.container.retries", 3);
+    private static final long BASE_DELAY_MS = 
Long.getLong("camel.test.infra.container.retry.delay.ms", 5000);
 
     private TestServiceUtil() {
 
     }
 
     /**
-     * Try to initialize the service, logging failures if they happen
+     * Try to initialize the service with retry for transient container 
errors. Retries on
+     * {@link ContainerFetchException} (image pull failures) and {@link 
ContainerLaunchException} (container start
+     * failures) with exponential backoff and jitter. Non-container exceptions 
fail immediately. Retry count and delay
+     * are configurable via system properties {@code 
camel.test.infra.container.retries} (default 3) and
+     * {@code camel.test.infra.container.retry.delay.ms} (default 5000).
      *
      * @param  service          the service to initialize
      * @param  extensionContext JUnit's extension context
      * @throws Exception        exception thrown while initializing (if any)
      */
     public static void tryInitialize(TestService service, ExtensionContext 
extensionContext) throws Exception {
-        try {
-            service.initialize();
-        } catch (Exception e) {
-            logAndRethrow(service, extensionContext, e);
+        for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+            try {
+                service.initialize();
+                return;
+            } catch (Exception e) {
+                if (attempt < MAX_RETRIES && isRetryableContainerException(e)) 
{
+                    long jitter = ThreadLocalRandom.current().nextLong(0, 
BASE_DELAY_MS / 2);
+                    long delay = BASE_DELAY_MS * attempt + jitter;
+                    LOG.warn("Service {} initialization failed (attempt 
{}/{}), retrying in {}ms: {}",
+                            service.getClass().getSimpleName(), attempt, 
MAX_RETRIES, delay, e.getMessage());
+                    Thread.sleep(delay);
+                } else {
+                    logAndRethrow(service, extensionContext, e);
+                }
+            }
+        }
+    }
+
+    private static boolean isRetryableContainerException(Throwable e) {
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            if (t instanceof ContainerFetchException || t instanceof 
ContainerLaunchException) {
+                return true;
+            }
         }
+        return false;
     }
 
     /**
diff --git 
a/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtilRetryTest.java
 
b/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtilRetryTest.java
new file mode 100644
index 000000000000..bffdbad7e6a6
--- /dev/null
+++ 
b/test-infra/camel-test-infra-common/src/test/java/org/apache/camel/test/infra/common/services/TestServiceUtilRetryTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.apache.camel.test.infra.common.services;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.ContainerFetchException;
+import org.testcontainers.containers.ContainerLaunchException;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestServiceUtilRetryTest {
+
+    @Test
+    void retriesOnContainerFetchException() {
+        AtomicInteger attempts = new AtomicInteger();
+
+        TestService service = new StubTestService(() -> {
+            if (attempts.incrementAndGet() < 3) {
+                throw new ContainerFetchException("404: image not found");
+            }
+        });
+
+        assertDoesNotThrow(() -> TestServiceUtil.tryInitialize(service, null));
+        assertEquals(3, attempts.get());
+    }
+
+    @Test
+    void retriesOnContainerLaunchException() {
+        AtomicInteger attempts = new AtomicInteger();
+
+        TestService service = new StubTestService(() -> {
+            if (attempts.incrementAndGet() < 2) {
+                throw new ContainerLaunchException("Container startup failed");
+            }
+        });
+
+        assertDoesNotThrow(() -> TestServiceUtil.tryInitialize(service, null));
+        assertEquals(2, attempts.get());
+    }
+
+    @Test
+    void failsImmediatelyOnNonContainerException() {
+        AtomicInteger attempts = new AtomicInteger();
+
+        TestService service = new StubTestService(() -> {
+            attempts.incrementAndGet();
+            throw new IllegalStateException("not a container error");
+        });
+
+        assertThrows(IllegalStateException.class, () -> 
TestServiceUtil.tryInitialize(service, null));
+        assertEquals(1, attempts.get());
+    }
+
+    @Test
+    void failsAfterMaxRetries() {
+        AtomicInteger attempts = new AtomicInteger();
+
+        TestService service = new StubTestService(() -> {
+            attempts.incrementAndGet();
+            throw new ContainerFetchException("always fails");
+        });
+
+        assertThrows(ContainerFetchException.class, () -> 
TestServiceUtil.tryInitialize(service, null));
+        assertEquals(3, attempts.get());
+    }
+
+    @Test
+    void retriesOnWrappedContainerException() {
+        AtomicInteger attempts = new AtomicInteger();
+
+        TestService service = new StubTestService(() -> {
+            if (attempts.incrementAndGet() < 2) {
+                throw new RuntimeException("wrapper", new 
ContainerLaunchException("nested container error"));
+            }
+        });
+
+        assertDoesNotThrow(() -> TestServiceUtil.tryInitialize(service, null));
+        assertEquals(2, attempts.get());
+    }
+
+    @Test
+    void succeedsOnFirstAttempt() {
+        AtomicInteger attempts = new AtomicInteger();
+
+        TestService service = new StubTestService(attempts::incrementAndGet);
+
+        assertDoesNotThrow(() -> TestServiceUtil.tryInitialize(service, null));
+        assertEquals(1, attempts.get());
+    }
+
+    private static class StubTestService implements TestService {
+        private final Runnable action;
+
+        StubTestService(Runnable action) {
+            this.action = action;
+        }
+
+        @Override
+        public void initialize() {
+            action.run();
+        }
+
+        @Override
+        public void registerProperties() {
+        }
+
+        @Override
+        public void shutdown() {
+        }
+
+        @Override
+        public void close() {
+        }
+    }
+}

Reply via email to