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

ffang pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
     new 2520e57c94f [CXF-9213]make RetryStrategy stateless (#3143)
2520e57c94f is described below

commit 2520e57c94fd35c57daecc8b3163a77bd90de0a1
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Tue Jun 2 20:43:03 2026 -0400

    [CXF-9213]make RetryStrategy stateless (#3143)
    
    (cherry picked from commit 37b2d8147e7bdf0b2c61db05fc232f9b251948e0)
---
 .../cxf/clustering/FailoverTargetSelector.java     |  41 +++--
 .../clustering/PerInvocationFailoverStrategy.java  |  39 +++++
 .../org/apache/cxf/clustering/RetryStrategy.java   |  60 +++++--
 .../apache/cxf/clustering/RetryStrategyTest.java   | 181 +++++++++++++++++++++
 .../jaxrs/failover/AbstractFailoverTest.java       |  36 +++-
 5 files changed, 328 insertions(+), 29 deletions(-)

diff --git 
a/rt/features/clustering/src/main/java/org/apache/cxf/clustering/FailoverTargetSelector.java
 
b/rt/features/clustering/src/main/java/org/apache/cxf/clustering/FailoverTargetSelector.java
index debfd2dfab2..666e576d694 100644
--- 
a/rt/features/clustering/src/main/java/org/apache/cxf/clustering/FailoverTargetSelector.java
+++ 
b/rt/features/clustering/src/main/java/org/apache/cxf/clustering/FailoverTargetSelector.java
@@ -53,6 +53,8 @@ public class FailoverTargetSelector extends 
AbstractConduitSelector {
 
     protected FailoverStrategy failoverStrategy;
     private ConcurrentHashMap<String, InvocationContext> inProgress = new 
ConcurrentHashMap<>();
+    // CXF-9213: per-invocation strategy instances for 
PerInvocationFailoverStrategy implementations.
+    private ConcurrentHashMap<String, FailoverStrategy> inProgressStrategies = 
new ConcurrentHashMap<>();
     private boolean supportNotAvailableErrorsOnly = true;
     private String clientBootstrapAddress;
 
@@ -113,6 +115,11 @@ public class FailoverTargetSelector extends 
AbstractConduitSelector {
                                       params,
                                       context);
             inProgress.putIfAbsent(key, invocation);
+            // CXF-9213: create a fresh per-invocation instance for strategies 
that carry state.
+            if (getStrategy() instanceof PerInvocationFailoverStrategy) {
+                inProgressStrategies.putIfAbsent(key,
+                    ((PerInvocationFailoverStrategy) 
getStrategy()).newStrategy());
+            }
         }
     }
 
@@ -179,6 +186,7 @@ public class FailoverTargetSelector extends 
AbstractConduitSelector {
 
         if (!failover) {
             inProgress.remove(key);
+            inProgressStrategies.remove(key);
             doComplete(exchange);
         }
     }
@@ -316,21 +324,19 @@ public class FailoverTargetSelector extends 
AbstractConduitSelector {
      */
     protected Endpoint getFailoverTarget(Exchange exchange,
                                        InvocationContext invocation) {
+        String key = getInvocationKey(exchange);
+        FailoverStrategy strategy = getStrategy(key);
         List<String> alternateAddresses = updateContextAlternatives(exchange, 
invocation);
         Endpoint failoverTarget = null;
         if (alternateAddresses != null) {
-            String alternateAddress =
-                getStrategy().selectAlternateAddress(alternateAddresses);
+            String alternateAddress = 
strategy.selectAlternateAddress(alternateAddresses);
             if (alternateAddress != null) {
                 // re-use current endpoint
-                //
                 failoverTarget = getEndpoint();
-
                 failoverTarget.getEndpointInfo().setAddress(alternateAddress);
             }
         } else {
-            failoverTarget = getStrategy().selectAlternateEndpoint(
-                                 invocation.getAlternateEndpoints());
+            failoverTarget = 
strategy.selectAlternateEndpoint(invocation.getAlternateEndpoints());
         }
         return failoverTarget;
     }
@@ -346,14 +352,12 @@ public class FailoverTargetSelector extends 
AbstractConduitSelector {
         final List<String> alternateAddresses;
         if (!invocation.hasAlternates()) {
             // no previous failover attempt on this invocation
-            //
-            alternateAddresses =
-                getStrategy().getAlternateAddresses(exchange);
+            FailoverStrategy strategy = 
getStrategy(getInvocationKey(exchange));
+            alternateAddresses = strategy.getAlternateAddresses(exchange);
             if (alternateAddresses != null) {
                 invocation.setAlternateAddresses(alternateAddresses);
             } else {
-                invocation.setAlternateEndpoints(
-                    getStrategy().getAlternateEndpoints(exchange));
+                
invocation.setAlternateEndpoints(strategy.getAlternateEndpoints(exchange));
             }
         } else {
             alternateAddresses = invocation.getAlternateAddresses();
@@ -361,6 +365,21 @@ public class FailoverTargetSelector extends 
AbstractConduitSelector {
         return alternateAddresses;
     }
 
+    /**
+     * Returns the per-invocation {@link FailoverStrategy} for the given key 
if one
+     * was registered by {@link PerInvocationFailoverStrategy#newStrategy()}, 
otherwise
+     * returns the shared strategy.
+     */
+    protected FailoverStrategy getStrategy(String key) {
+        if (key != null) {
+            FailoverStrategy perInvocation = inProgressStrategies.get(key);
+            if (perInvocation != null) {
+                return perInvocation;
+            }
+        }
+        return getStrategy();
+    }
+
     /**
      * Override the ENDPOINT_ADDRESS property in the request context
      *
diff --git 
a/rt/features/clustering/src/main/java/org/apache/cxf/clustering/PerInvocationFailoverStrategy.java
 
b/rt/features/clustering/src/main/java/org/apache/cxf/clustering/PerInvocationFailoverStrategy.java
new file mode 100644
index 00000000000..78ddd950ac6
--- /dev/null
+++ 
b/rt/features/clustering/src/main/java/org/apache/cxf/clustering/PerInvocationFailoverStrategy.java
@@ -0,0 +1,39 @@
+/**
+ * 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.cxf.clustering;
+
+/**
+ * Marker interface for {@link FailoverStrategy} implementations that carry
+ * per-invocation mutable state (e.g. a retry counter).
+ *
+ * <p>When {@link FailoverTargetSelector} detects that the configured strategy
+ * implements this interface it calls {@link #newStrategy()} at the start of
+ * each top-level invocation and uses the returned instance for all failover
+ * decisions belonging to that invocation.  The shared bean is therefore never
+ * mutated during normal processing and is safe to configure as a singleton.
+ */
+public interface PerInvocationFailoverStrategy {
+
+    /**
+     * Returns a new {@link FailoverStrategy} instance pre-configured with the
+     * same settings as {@code this} but with a fresh, zeroed mutable state.
+     * Called once per top-level invocation by {@link FailoverTargetSelector}.
+     */
+    FailoverStrategy newStrategy();
+}
diff --git 
a/rt/features/clustering/src/main/java/org/apache/cxf/clustering/RetryStrategy.java
 
b/rt/features/clustering/src/main/java/org/apache/cxf/clustering/RetryStrategy.java
index 8e823bb7cee..ece065756ef 100644
--- 
a/rt/features/clustering/src/main/java/org/apache/cxf/clustering/RetryStrategy.java
+++ 
b/rt/features/clustering/src/main/java/org/apache/cxf/clustering/RetryStrategy.java
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.cxf.clustering;
 
 import java.util.List;
@@ -25,44 +24,73 @@ import org.apache.cxf.endpoint.Endpoint;
 import org.apache.cxf.message.Exchange;
 
 /**
- * This strategy simply retries the invocation using the same Endpoint 
(CXF-2036).
+ * Retry strategy that retries the same endpoint for a configured number of
+ * attempts before advancing to the next alternate (CXF-2036).
+ *
+ * <p>This class implements {@link PerInvocationFailoverStrategy}: when used
+ * via {@link FailoverTargetSelector}, a fresh instance is created per
+ * top-level invocation through {@link #newStrategy()}, so the shared
+ * singleton bean is never mutated and concurrent invocations are fully
+ * isolated (CXF-9213).  The per-invocation instance carries its own retry
+ * counter as a plain instance field
+ *
+ * <p>Subclasses that accumulate cross-invocation state (e.g. call counters)
+ * should override {@link #newStrategy()} to return a delegate that writes
+ * that state back to the shared instance while keeping its own retry counter.
  */
-public class RetryStrategy extends SequentialStrategy {
+public class RetryStrategy extends SequentialStrategy implements 
PerInvocationFailoverStrategy {
 
     private int maxNumberOfRetries;
-    private int counter;
+    private int count;
 
-    /* (non-Javadoc)
-     * @see 
org.apache.cxf.clustering.AbstractStaticFailoverStrategy#getAlternateEndpoints(
-     * org.apache.cxf.message.Exchange)
-     */
     @Override
     public List<Endpoint> getAlternateEndpoints(Exchange exchange) {
-        return getEndpoints(exchange, stillTheSameAddress());
+        return getEndpoints(exchange, stillTheSameAddress(exchange));
     }
 
     @Override
     protected <T> T getNextAlternate(List<T> alternates) {
-        // is the amount of retries for the first alternate already exceeded?
         if (!stillTheSameAddress() && !alternates.isEmpty()) {
             alternates.remove(0);
         }
         return alternates.isEmpty() ? null : alternates.get(0);
     }
 
+    /**
+     * Exchange-aware variant; delegates to {@link #stillTheSameAddress()} so
+     * subclasses may override either form.
+     */
+    protected boolean stillTheSameAddress(Exchange exchange) {
+        return stillTheSameAddress();
+    }
+
     protected boolean stillTheSameAddress() {
         if (maxNumberOfRetries == 0) {
             return true;
         }
-        // let the target selector move to the next address
-        // and then stay on the same address for maxNumberOfRetries
-        if (++counter <= maxNumberOfRetries) {
+        if (++count <= maxNumberOfRetries) {
             return true;
         }
-        counter = 0;
+        count = 0;
         return false;
     }
 
+    /**
+     * Returns a new {@link RetryStrategy} with the same configuration but a
+     * zeroed counter.  Subclasses that need to accumulate state on the shared
+     * instance should override this method and return a delegating wrapper.
+     */
+    @Override
+    public FailoverStrategy newStrategy() {
+        RetryStrategy copy = new RetryStrategy();
+        copy.maxNumberOfRetries = this.maxNumberOfRetries;
+        List<String> addresses = getAlternateAddresses(null);
+        if (addresses != null) {
+            copy.setAlternateAddresses(addresses);
+        }
+        copy.setDelayBetweenRetries(getDelayBetweenRetries());
+        return copy;
+    }
 
     public void setMaxNumberOfRetries(int maxNumberOfRetries) {
         if (maxNumberOfRetries < 0) {
@@ -71,9 +99,7 @@ public class RetryStrategy extends SequentialStrategy {
         this.maxNumberOfRetries = maxNumberOfRetries;
     }
 
-
     public int getMaxNumberOfRetries() {
         return maxNumberOfRetries;
     }
-
-}
\ No newline at end of file
+}
diff --git 
a/rt/features/clustering/src/test/java/org/apache/cxf/clustering/RetryStrategyTest.java
 
b/rt/features/clustering/src/test/java/org/apache/cxf/clustering/RetryStrategyTest.java
new file mode 100644
index 00000000000..ad6c045e28f
--- /dev/null
+++ 
b/rt/features/clustering/src/test/java/org/apache/cxf/clustering/RetryStrategyTest.java
@@ -0,0 +1,181 @@
+/**
+ * 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.cxf.clustering;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for CXF-9213: RetryStrategy per-invocation isolation via
+ * {@link PerInvocationFailoverStrategy#newStrategy()}.
+ */
+public class RetryStrategyTest {
+
+    private static RetryStrategy strategyWith(int maxRetries) {
+        RetryStrategy s = new RetryStrategy();
+        s.setMaxNumberOfRetries(maxRetries);
+        return s;
+    }
+
+    // -----------------------------------------------------------------------
+    // Basic counter behaviour on a single instance
+    // -----------------------------------------------------------------------
+
+    @Test
+    public void testRetriesExactlyMaxTimes() {
+        RetryStrategy s = strategyWith(3);
+        assertTrue("attempt 1", s.stillTheSameAddress());
+        assertTrue("attempt 2", s.stillTheSameAddress());
+        assertTrue("attempt 3", s.stillTheSameAddress());
+        assertFalse("attempt 4 must exhaust", s.stillTheSameAddress());
+    }
+
+    @Test
+    public void testMaxRetriesZeroAlwaysReturnsSameAddress() {
+        RetryStrategy s = strategyWith(0);
+        for (int i = 0; i < 100; i++) {
+            assertTrue(s.stillTheSameAddress());
+        }
+    }
+
+    @Test
+    public void testCounterResetsAfterExhaustion() {
+        RetryStrategy s = strategyWith(2);
+        s.stillTheSameAddress(); // 1 – true
+        s.stillTheSameAddress(); // 2 – true
+        assertFalse(s.stillTheSameAddress()); // exhausted, resets to 0
+        assertTrue(s.stillTheSameAddress());  // new cycle starts
+    }
+
+    // -----------------------------------------------------------------------
+    // PerInvocationFailoverStrategy contract
+    // -----------------------------------------------------------------------
+
+    @Test
+    public void testImplementsPerInvocationFailoverStrategy() {
+        assertTrue(new RetryStrategy() instanceof 
PerInvocationFailoverStrategy);
+    }
+
+    @Test
+    public void testNewStrategyReturnsDistinctInstance() {
+        RetryStrategy template = strategyWith(3);
+        FailoverStrategy s1 = template.newStrategy();
+        FailoverStrategy s2 = template.newStrategy();
+        assertNotSame(template, s1);
+        assertNotSame(s1, s2);
+    }
+
+    @Test
+    public void testNewStrategyInheritsMaxRetries() {
+        RetryStrategy template = strategyWith(5);
+        RetryStrategy copy = (RetryStrategy) template.newStrategy();
+        assertEquals(5, copy.getMaxNumberOfRetries());
+    }
+
+    @Test
+    public void testNewStrategyCopiesAlternateAddresses() {
+        RetryStrategy template = strategyWith(2);
+        template.setAlternateAddresses(Arrays.asList("http://a";, "http://b";));
+        RetryStrategy copy = (RetryStrategy) template.newStrategy();
+        assertEquals(Arrays.asList("http://a";, "http://b";), 
copy.getAlternateAddresses(null));
+    }
+
+    @Test
+    public void testNewStrategyHasFreshCounter() {
+        RetryStrategy template = strategyWith(3);
+        // advance the template's own counter
+        template.stillTheSameAddress();
+        template.stillTheSameAddress();
+
+        // a new instance must start from zero regardless
+        RetryStrategy copy = (RetryStrategy) template.newStrategy();
+        assertTrue("copy attempt 1", copy.stillTheSameAddress());
+        assertTrue("copy attempt 2", copy.stillTheSameAddress());
+        assertTrue("copy attempt 3", copy.stillTheSameAddress());
+        assertFalse("copy attempt 4 must exhaust", copy.stillTheSameAddress());
+    }
+
+    @Test
+    public void testTwoInstancesAreIndependent() {
+        RetryStrategy template = strategyWith(3);
+        RetryStrategy i1 = (RetryStrategy) template.newStrategy();
+        RetryStrategy i2 = (RetryStrategy) template.newStrategy();
+
+        // advance i1 without exhausting it
+        i1.stillTheSameAddress();
+        i1.stillTheSameAddress();
+
+        // i2 must still be at zero
+        assertTrue("i2 attempt 1", i2.stillTheSameAddress());
+        assertTrue("i2 attempt 2", i2.stillTheSameAddress());
+        assertTrue("i2 attempt 3", i2.stillTheSameAddress());
+        assertFalse("i2 attempt 4 must exhaust", i2.stillTheSameAddress());
+    }
+
+    // -----------------------------------------------------------------------
+    // Concurrency: concurrent newStrategy() calls on a shared template
+    // -----------------------------------------------------------------------
+
+    @Test
+    public void testConcurrentPerInvocationInstancesAreIndependent() throws 
InterruptedException {
+        final int maxRetries = 4;
+        RetryStrategy template = strategyWith(maxRetries);
+        AtomicInteger successes1 = new AtomicInteger();
+        AtomicInteger successes2 = new AtomicInteger();
+        CountDownLatch start = new CountDownLatch(1);
+        CountDownLatch done = new CountDownLatch(2);
+
+        for (AtomicInteger counter : new AtomicInteger[]{successes1, 
successes2}) {
+            ExecutorService pool = Executors.newSingleThreadExecutor();
+            pool.submit(() -> {
+                try {
+                    start.await();
+                    // Simulates FailoverTargetSelector calling newStrategy() 
per invocation.
+                    RetryStrategy instance = (RetryStrategy) 
template.newStrategy();
+                    for (int i = 0; i < maxRetries; i++) {
+                        if (instance.stillTheSameAddress()) {
+                            counter.incrementAndGet();
+                        }
+                    }
+                    assertFalse("instance must be exhausted", 
instance.stillTheSameAddress());
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                } finally {
+                    done.countDown();
+                }
+            });
+            pool.shutdown();
+        }
+
+        start.countDown();
+        done.await();
+        assertEquals("invocation 1 retry count", maxRetries, successes1.get());
+        assertEquals("invocation 2 retry count", maxRetries, successes2.get());
+    }
+}
diff --git 
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/failover/AbstractFailoverTest.java
 
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/failover/AbstractFailoverTest.java
index b2045f01729..0ed8a266244 100644
--- 
a/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/failover/AbstractFailoverTest.java
+++ 
b/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/failover/AbstractFailoverTest.java
@@ -30,6 +30,7 @@ import jakarta.ws.rs.core.Response;
 import org.apache.cxf.Bus;
 import org.apache.cxf.bus.extension.ExtensionManagerBus;
 import org.apache.cxf.clustering.FailoverFeature;
+import org.apache.cxf.clustering.FailoverStrategy;
 import org.apache.cxf.clustering.FailoverTargetSelector;
 import org.apache.cxf.clustering.RandomStrategy;
 import org.apache.cxf.clustering.RetryStrategy;
@@ -376,12 +377,45 @@ public abstract class AbstractFailoverTest extends 
AbstractBusClientServerTestBa
     private static final class CustomRetryStrategy extends RetryStrategy {
         private int totalCount;
         private Map<String, Integer> map = new HashMap<>();
+
+        @Override
+        public FailoverStrategy newStrategy() {
+            // Return a per-invocation delegate whose retry counter is 
isolated but
+            // whose tracking (totalCount, map) accumulates on this shared 
instance.
+            final CustomRetryStrategy master = this;
+            RetryStrategy delegate = new RetryStrategy() {
+                @Override
+                protected <T> T getNextAlternate(List<T> alternates) {
+                    master.totalCount++;
+                    T next = super.getNextAlternate(alternates);
+                    if (next != null) {
+                        String address = (String) next;
+                        Integer count = master.map.get(address);
+                        if (count == null) {
+                            count = master.map.isEmpty() ? 1 /* count first 
call */ : 0;
+                        }
+                        count++;
+                        master.map.put(address, count);
+                    }
+                    return next;
+                }
+            };
+            delegate.setMaxNumberOfRetries(getMaxNumberOfRetries());
+            List<String> addresses = getAlternateAddresses(null);
+            if (addresses != null) {
+                delegate.setAlternateAddresses(addresses);
+            }
+            delegate.setDelayBetweenRetries(getDelayBetweenRetries());
+            return delegate;
+        }
+
+        // Direct use (outside FailoverTargetSelector) keeps original 
behaviour.
         @Override
         protected <T> T getNextAlternate(List<T> alternates) {
             totalCount++;
             T next = super.getNextAlternate(alternates);
             if (next != null) {
-                String address = (String)next;
+                String address = (String) next;
                 Integer count = map.get(address);
                 if (count == null) {
                     count = map.isEmpty() ? 1 /* count first call */ : 0;

Reply via email to