This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/replaycache in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 506f9f50b471630aac4138be50a2dd735a0711be Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Oct 23 08:16:40 2026 +0100 Make JCacheReplayCache replay checks atomic --- .../security/cache/jcache/JCacheReplayCache.java | 18 ++++ .../cache/jcache/JCacheReplayCacheTest.java | 107 +++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/rt/ws/security/src/main/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCache.java b/rt/ws/security/src/main/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCache.java index 435b089512c..a103649eb99 100644 --- a/rt/ws/security/src/main/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCache.java +++ b/rt/ws/security/src/main/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCache.java @@ -103,6 +103,24 @@ class JCacheReplayCache implements ReplayCache { cache.put(identifier, new EHCacheValue(identifier, expiry)); } + /** + * Add the given identifier to the cache if, and only if, it is not already contained in it, as a + * single atomic operation. This overrides ReplayCache.addIfAbsent, whose default implementation is + * only atomic within a single JVM, whereas a JCache provider may be distributed. + * @param identifier The identifier to be added + * @param expiry A custom expiry time for the identifier. Can be null in which case, the default + * expiry is used. + * @return true if the identifier was added, false if it was already contained in the cache or is + * null or empty + */ + public boolean addIfAbsent(String identifier, Instant expiry) { + if (identifier == null || identifier.length() == 0) { + return false; + } + + return cache.putIfAbsent(identifier, new EHCacheValue(identifier, expiry)); + } + /** * Return true if the given identifier is contained in the cache * @param identifier The identifier to check diff --git a/rt/ws/security/src/test/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCacheTest.java b/rt/ws/security/src/test/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCacheTest.java new file mode 100644 index 00000000000..0f9a682648f --- /dev/null +++ b/rt/ws/security/src/test/java/org/apache/cxf/ws/security/cache/jcache/JCacheReplayCacheTest.java @@ -0,0 +1,107 @@ +/** + * 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.ws.security.cache.jcache; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicIntegerArray; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Some unit tests for the JCacheReplayCache + */ +public class JCacheReplayCacheTest { + + @Test + public void testAddIfAbsent() throws Exception { + JCacheReplayCache replayCache = new JCacheReplayCache("cxf.jcache.replay.test-" + UUID.randomUUID()); + try { + String id = UUID.randomUUID().toString(); + assertTrue(replayCache.addIfAbsent(id, null)); + assertTrue(replayCache.contains(id)); + assertFalse(replayCache.addIfAbsent(id, null)); + + // An identifier added with add() is also seen + id = UUID.randomUUID().toString(); + replayCache.add(id); + assertFalse(replayCache.addIfAbsent(id, null)); + + // An empty identifier cannot be cached, so is never reported as added + assertFalse(replayCache.addIfAbsent(null, null)); + assertFalse(replayCache.addIfAbsent("", null)); + assertFalse(replayCache.contains("")); + } finally { + replayCache.close(); + } + } + + /** + * Concurrent requests carrying the same identifier must not all be accepted: exactly one + * addIfAbsent call per identifier may succeed. + */ + @Test + public void testConcurrentAddIfAbsent() throws Exception { + JCacheReplayCache replayCache = new JCacheReplayCache("cxf.jcache.replay.test-" + UUID.randomUUID()); + + final int threads = 8; + final int identifiers = 500; + List<String> ids = new ArrayList<>(identifiers); + for (int i = 0; i < identifiers; i++) { + ids.add(UUID.randomUUID().toString()); + } + AtomicIntegerArray accepted = new AtomicIntegerArray(identifiers); + CyclicBarrier barrier = new CyclicBarrier(threads); + + ExecutorService executor = Executors.newFixedThreadPool(threads); + try { + List<Future<?>> futures = new ArrayList<>(threads); + for (int t = 0; t < threads; t++) { + futures.add(executor.submit(() -> { + barrier.await(); + for (int i = 0; i < identifiers; i++) { + if (replayCache.addIfAbsent(ids.get(i), null)) { + accepted.incrementAndGet(i); + } + } + return null; + })); + } + for (Future<?> future : futures) { + future.get(); + } + } finally { + executor.shutdown(); + replayCache.close(); + } + + for (int i = 0; i < identifiers; i++) { + assertEquals("Identifier " + i + " was accepted more than once", 1, accepted.get(i)); + } + } +}
