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

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit 118dcca1ba595d7c95bdc63b2a1fe66b89a1e6c7
Author: Robert Lazarski <[email protected]>
AuthorDate: Sun May 17 11:48:17 2026 -1000

    AXIS2-5904 Fix policy cache race condition (millisecond granularity)
    
    AxisBindingMessage.getEffectivePolicy() used Date.after() to detect
    policy changes. Two policy updates within the same millisecond
    produced identical timestamps, causing isPolicyUpdated() to return
    false and a stale (potentially null) cached policy to be returned.
    This manifested as intermittent "Rampart policy configuration missing"
    errors on consecutive secured SOAP calls.
    
    Replace Date-based timestamp comparison with a monotonic AtomicLong
    counter in PolicySubject. Every mutation (attach, detach, update,
    clear) increments the global counter. AxisBindingMessage.isPolicyUpdated()
    now compares version numbers instead of timestamps — no granularity
    issues, no race.
    
    The deprecated Date-based getLastUpdatedTime()/setLastUpdatedTime()
    API is preserved for backward compatibility.
    
    Includes regression test (PolicyCacheRaceTest, 5 tests) that verifies
    version monotonicity and rapid-fire policy updates are all detected.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../axis2/description/AxisBindingMessage.java      |  64 +++++------
 .../apache/axis2/description/PolicySubject.java    |   9 ++
 .../axis2/description/PolicyCacheRaceTest.java     | 125 +++++++++++++++++++++
 3 files changed, 163 insertions(+), 35 deletions(-)

diff --git 
a/modules/kernel/src/org/apache/axis2/description/AxisBindingMessage.java 
b/modules/kernel/src/org/apache/axis2/description/AxisBindingMessage.java
index b3f6ecc1bb..85e3fb54ac 100644
--- a/modules/kernel/src/org/apache/axis2/description/AxisBindingMessage.java
+++ b/modules/kernel/src/org/apache/axis2/description/AxisBindingMessage.java
@@ -53,6 +53,7 @@ public class AxisBindingMessage extends AxisDescription {
 
     private volatile Policy effectivePolicy = null;
     private volatile Date lastPolicyCalculatedTime = null;
+    private volatile long lastPolicyCalculatedVersion = -1;
 
     public boolean isFault() {
         return fault;
@@ -217,11 +218,12 @@ public class AxisBindingMessage extends AxisDescription {
     }
 
     public Policy getEffectivePolicy() {
-        if (lastPolicyCalculatedTime == null || isPolicyUpdated()) {
+        if (isPolicyUpdated()) {
             synchronized (this) {
-                if (lastPolicyCalculatedTime == null || isPolicyUpdated()) {
+                if (isPolicyUpdated()) {
                     effectivePolicy = calculateEffectivePolicy();
                     lastPolicyCalculatedTime = new Date();
+                    lastPolicyCalculatedVersion = getMaxPolicyVersion();
                 }
             }
         }
@@ -293,64 +295,56 @@ public class AxisBindingMessage extends AxisDescription {
     }
     
     private boolean isPolicyUpdated() {
-        if (getPolicySubject().getLastUpdatedTime().after(
-                lastPolicyCalculatedTime)) {
-            return true;
-        }
+        return getMaxPolicyVersion() > lastPolicyCalculatedVersion;
+    }
+
+    /**
+     * Returns the maximum policy version across the entire description
+     * hierarchy. Uses monotonic counter instead of Date to avoid the
+     * millisecond-granularity race condition in AXIS2-5904.
+     */
+    private long getMaxPolicyVersion() {
+        long max = getPolicySubject().getVersion();
         // AxisBindingOperation
         AxisBindingOperation axisBindingOperation = getAxisBindingOperation();
-        if (axisBindingOperation != null
-                && axisBindingOperation.getPolicySubject().getLastUpdatedTime()
-                        .after(lastPolicyCalculatedTime)) {
-            return true;
+        if (axisBindingOperation != null) {
+            max = Math.max(max, 
axisBindingOperation.getPolicySubject().getVersion());
         }
         // AxisBinding
         AxisBinding axisBinding = (axisBindingOperation == null) ? null
                 : axisBindingOperation.getAxisBinding();
-        if (axisBinding != null
-                && axisBinding.getPolicySubject().getLastUpdatedTime().after(
-                lastPolicyCalculatedTime)) {
-            return true;
+        if (axisBinding != null) {
+            max = Math.max(max, axisBinding.getPolicySubject().getVersion());
         }
         // AxisEndpoint
         AxisEndpoint axisEndpoint = (axisBinding == null) ? null : axisBinding
                 .getAxisEndpoint();
-        if (axisEndpoint != null
-                && axisEndpoint.getPolicySubject().getLastUpdatedTime().after(
-                lastPolicyCalculatedTime)) {
-            return true;
+        if (axisEndpoint != null) {
+            max = Math.max(max, axisEndpoint.getPolicySubject().getVersion());
         }
         // AxisMessage
-        if (axisMessage != null
-                && axisMessage.getPolicySubject().getLastUpdatedTime().after(
-                lastPolicyCalculatedTime)) {
-            return true;
+        if (axisMessage != null) {
+            max = Math.max(max, axisMessage.getPolicySubject().getVersion());
         }
         // AxisOperation
         AxisOperation axisOperation = (axisMessage == null) ? null
                 : axisMessage.getAxisOperation();
-        if (axisOperation != null
-                && axisOperation.getPolicySubject().getLastUpdatedTime().after(
-                lastPolicyCalculatedTime)) {
-            return true;
+        if (axisOperation != null) {
+            max = Math.max(max, axisOperation.getPolicySubject().getVersion());
         }
         // AxisService
         AxisService axisService = (axisOperation == null) ? null
                 : axisOperation.getAxisService();
-        if (axisService != null
-                && axisService.getPolicySubject().getLastUpdatedTime().after(
-                lastPolicyCalculatedTime)) {
-            return true;
+        if (axisService != null) {
+            max = Math.max(max, axisService.getPolicySubject().getVersion());
         }
         // AxisConfiguration
         AxisConfiguration axisConfiguration = (axisService == null) ? null
                 : axisService.getAxisConfiguration();
-        if (axisConfiguration != null
-                && axisConfiguration.getPolicySubject().getLastUpdatedTime()
-                        .after(lastPolicyCalculatedTime)) {
-            return true;
+        if (axisConfiguration != null) {
+            max = Math.max(max, 
axisConfiguration.getPolicySubject().getVersion());
         }
-        return false;
+        return max;
     }
     
     @Override
diff --git a/modules/kernel/src/org/apache/axis2/description/PolicySubject.java 
b/modules/kernel/src/org/apache/axis2/description/PolicySubject.java
index 49e0b59969..bdfbc3e940 100644
--- a/modules/kernel/src/org/apache/axis2/description/PolicySubject.java
+++ b/modules/kernel/src/org/apache/axis2/description/PolicySubject.java
@@ -29,8 +29,12 @@ import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 public class PolicySubject {
+    private static final AtomicLong VERSION_COUNTER = new AtomicLong();
+    private volatile long version = VERSION_COUNTER.incrementAndGet();
+    @Deprecated
     private Date lastUpdatedTime = new Date();
     
     private ConcurrentHashMap<String, PolicyComponent> 
attachedPolicyComponents = new ConcurrentHashMap<String, PolicyComponent>();
@@ -113,5 +117,10 @@ public class PolicySubject {
 
     public void setLastUpdatedTime(Date lastUpdatedTime) {
         this.lastUpdatedTime = lastUpdatedTime;
+        this.version = VERSION_COUNTER.incrementAndGet();
+    }
+
+    public long getVersion() {
+        return version;
     }
 }
diff --git 
a/modules/kernel/test/org/apache/axis2/description/PolicyCacheRaceTest.java 
b/modules/kernel/test/org/apache/axis2/description/PolicyCacheRaceTest.java
new file mode 100644
index 0000000000..f0ddfa4ea8
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/description/PolicyCacheRaceTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.axis2.description;
+
+import org.apache.neethi.Policy;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Regression test for AXIS2-5904: Intermittent "Rampart policy configuration
+ * missing" error caused by millisecond-granularity race in
+ * {@link AxisBindingMessage#getEffectivePolicy()}.
+ *
+ * The old implementation used {@code Date.after()} to detect policy changes.
+ * Two policy updates within the same millisecond would produce identical
+ * timestamps, causing {@code isPolicyUpdated()} to return false and the
+ * cached (potentially stale/null) policy to be returned. The fix replaces
+ * Date comparison with a monotonic AtomicLong counter in PolicySubject.
+ */
+public class PolicyCacheRaceTest {
+
+    @Test
+    public void testPolicyVersionIncrements() {
+        PolicySubject subject = new PolicySubject();
+        long v1 = subject.getVersion();
+
+        Policy policy = new Policy();
+        policy.setId("test-policy-1");
+        subject.attachPolicy(policy);
+        long v2 = subject.getVersion();
+
+        assertTrue("Version must increment after attachPolicy", v2 > v1);
+
+        Policy policy2 = new Policy();
+        policy2.setId("test-policy-2");
+        subject.attachPolicy(policy2);
+        long v3 = subject.getVersion();
+
+        assertTrue("Version must increment on each mutation", v3 > v2);
+    }
+
+    @Test
+    public void testPolicyVersionIncrementsOnDetach() {
+        PolicySubject subject = new PolicySubject();
+        Policy policy = new Policy();
+        policy.setId("detach-test");
+        subject.attachPolicy(policy);
+        long vBefore = subject.getVersion();
+
+        subject.detachPolicyComponent("detach-test");
+        long vAfter = subject.getVersion();
+
+        assertTrue("Version must increment after detach", vAfter > vBefore);
+    }
+
+    @Test
+    public void testPolicyVersionIncrementsOnClear() {
+        PolicySubject subject = new PolicySubject();
+        long vBefore = subject.getVersion();
+
+        subject.clear();
+        long vAfter = subject.getVersion();
+
+        assertTrue("Version must increment after clear", vAfter > vBefore);
+    }
+
+    @Test
+    public void testEffectivePolicyDetectsUpdate() {
+        // Build a minimal AxisBindingMessage hierarchy
+        AxisBindingMessage bindingMessage = new AxisBindingMessage();
+
+        // First call — should compute and cache (returns null since no
+        // policies are attached, but the caching mechanism still runs)
+        Policy first = bindingMessage.getEffectivePolicy();
+
+        // Attach a policy to the binding message's own PolicySubject
+        Policy policy = new Policy();
+        policy.setId("axis2-5904-test");
+        bindingMessage.getPolicySubject().attachPolicy(policy);
+
+        // Second call — must detect the version change and recompute.
+        // Before the fix, if both calls happened within the same
+        // millisecond, isPolicyUpdated() returned false and the stale
+        // cached value was returned.
+        Policy second = bindingMessage.getEffectivePolicy();
+
+        // The recomputed policy should include the attached policy
+        assertNotNull("Effective policy must not be null after attaching a 
policy", second);
+    }
+
+    @Test
+    public void testRapidFirePolicyUpdatesAllDetected() {
+        // Simulate the AXIS2-5904 scenario: multiple policy mutations
+        // within the same millisecond must all be detected
+        AxisBindingMessage bindingMessage = new AxisBindingMessage();
+
+        for (int i = 0; i < 100; i++) {
+            Policy policy = new Policy();
+            policy.setId("rapid-fire-" + i);
+            bindingMessage.getPolicySubject().attachPolicy(policy);
+
+            // Every call must see the latest policy, not a stale cache
+            Policy effective = bindingMessage.getEffectivePolicy();
+            assertNotNull("Effective policy must not be null on iteration " + 
i, effective);
+        }
+    }
+}

Reply via email to