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

ahuber pushed a commit to branch v4
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/v4 by this push:
     new 44e574d78e6 CAUSEWAY-3897: test stability attempt: replace delay with 
retry (RO)
44e574d78e6 is described below

commit 44e574d78e65c923b32657a7fb785ac398019694
Author: Andi Huber <[email protected]>
AuthorDate: Wed Jul 9 07:40:05 2025 +0200

    CAUSEWAY-3897: test stability attempt: replace delay with retry (RO)
---
 .../causeway/commons/handler/RetryHandler.java     | 78 ++++++++++++++++++++++
 .../test/scenarios/staff/Staff_IntegTest.java      | 30 +++++----
 2 files changed, 96 insertions(+), 12 deletions(-)

diff --git 
a/commons/src/main/java/org/apache/causeway/commons/handler/RetryHandler.java 
b/commons/src/main/java/org/apache/causeway/commons/handler/RetryHandler.java
new file mode 100644
index 00000000000..fd8bd11f90a
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/causeway/commons/handler/RetryHandler.java
@@ -0,0 +1,78 @@
+/*
+ *  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.causeway.commons.handler;
+
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import org.springframework.core.retry.RetryException;
+
+import org.apache.causeway.commons.functional.Try;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+
+/**
+ *
+ * @since 4.0
+ */
+public record RetryHandler(
+    int maxAttempts,
+    Duration delay) {
+
+    // canonical constructor with argument validation
+    public RetryHandler(int maxAttempts, Duration delay) {
+        if(maxAttempts<1) {
+            throw _Exceptions.illegalArgument("invalid argument 'maxAttempts' 
%d, at least one attempt is required", maxAttempts);
+        }
+        if(delay.isNegative()) {
+            throw _Exceptions.illegalArgument("invalid argument 'delay' %s, 
must be non negative", delay);
+        }
+        this.maxAttempts = maxAttempts;
+        this.delay = delay;
+    }
+
+    public <T, E extends Throwable> Try<T> retryUntilValid(Callable<T> task, 
Predicate<T> isValid, Supplier<String> onInvalidMessage) {
+
+        for(int attemptCount = 1; attemptCount<=maxAttempts; ++attemptCount) {
+            Try<T> tryT = Try.call(task);
+            if(tryT.isFailure()) return tryT; // if we don't even get to 
validate, return immediately
+
+            var optionalT = tryT.getValue();
+
+            if(optionalT.isPresent()
+                && isValid.test(optionalT.get())) {
+                return Try.success(optionalT.get());
+            }
+
+            if(attemptCount < maxAttempts) {
+                // if not last try, delay next try
+                try {
+                    Thread.sleep(delay.toMillis());
+                } catch (InterruptedException e) {
+                    return Try.failure(e);
+                }
+            }
+        }
+
+        // last attempt failed
+        return Try.failure(new RetryException(onInvalidMessage.get()));
+    }
+
+}
diff --git 
a/viewers/restfulobjects/test/src/test/java/org/apache/causeway/viewer/restfulobjects/test/scenarios/staff/Staff_IntegTest.java
 
b/viewers/restfulobjects/test/src/test/java/org/apache/causeway/viewer/restfulobjects/test/scenarios/staff/Staff_IntegTest.java
index a5aaaec22a6..8127f8f1b6f 100644
--- 
a/viewers/restfulobjects/test/src/test/java/org/apache/causeway/viewer/restfulobjects/test/scenarios/staff/Staff_IntegTest.java
+++ 
b/viewers/restfulobjects/test/src/test/java/org/apache/causeway/viewer/restfulobjects/test/scenarios/staff/Staff_IntegTest.java
@@ -18,7 +18,9 @@
  */
 package org.apache.causeway.viewer.restfulobjects.test.scenarios.staff;
 
+import java.time.Duration;
 import java.util.Map;
+import java.util.Optional;
 
 import org.approvaltests.Approvals;
 import org.approvaltests.reporters.DiffReporter;
@@ -35,6 +37,7 @@
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
+import org.apache.causeway.commons.handler.RetryHandler;
 import org.apache.causeway.commons.io.DataSource;
 import 
org.apache.causeway.viewer.restfulobjects.applib.client.ActionParameterModel;
 import org.apache.causeway.viewer.restfulobjects.test.domain.dom.Department;
@@ -116,29 +119,32 @@ void createStaffMemberWithPhoto(final Scenario scenario) {
         var entity = response.body(String.class);
         assertNotNull(entity);
 
-        Thread.sleep(2000);
-
-        epilog();
+        epilog(5);
     }
 
     // -- HELPER
 
     void prolog() {
-        final var bookmarkBeforeIfAny = 
transactionService.callTransactional(Propagation.REQUIRED, () -> {
-            final var staffMember = 
staffMemberRepository.findByName(staffName);
-            return bookmarkService.bookmarkFor(staffMember);
-        }).valueAsNonNullElseFail();
+        assertThat(staffMemberBookmark()).isEmpty();
+    }
 
-        assertThat(bookmarkBeforeIfAny).isEmpty();
+    /**
+     * For some reason this epilog might be called too early, so we retry.
+     */
+    void epilog(final int maxAttempts) {
+        var retryHandler = new RetryHandler(maxAttempts, 
Duration.ofMillis(200));
+        var bookmarkAfterIfAny = 
retryHandler.retryUntilValid(this::staffMemberBookmark, Optional::isPresent,
+            ()->"staffMemberBookmark is empty, even after 
maxAttempts=%d".formatted(maxAttempts))
+            .valueAsNonNullElseFail();
+
+        assertThat(bookmarkAfterIfAny).isNotEmpty();
     }
 
-    void epilog() {
-        // and also object is created in database
-        final var bookmarkAfterIfAny = 
transactionService.callTransactional(Propagation.REQUIRED, () -> {
+    Optional<Bookmark> staffMemberBookmark() {
+        return transactionService.callTransactional(Propagation.REQUIRED, () 
-> {
             final var staffMember = 
staffMemberRepository.findByName(staffName);
             return bookmarkService.bookmarkFor(staffMember);
         }).valueAsNonNullElseFail();
-        assertThat(bookmarkAfterIfAny).isNotEmpty();
     }
 
     Bookmark departmentBookmark(final String departmentName) {

Reply via email to