From 7cb5aff63d86197e35cfff957995c7f4ce6ea95f Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Mon, 23 Mar 2026 15:10:00 +0100 Subject: [PATCH] fix(QTDI-2567) prevent CacheMap eviction issue --- .../sdk/component/server/lang/MapCache.java | 16 ++-- .../component/server/lang/MapCacheTest.java | 75 +++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/lang/MapCacheTest.java diff --git a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/lang/MapCache.java b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/lang/MapCache.java index 94b63d5d6d3cc..1f3d04842e889 100644 --- a/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/lang/MapCache.java +++ b/component-server-parent/component-server/src/main/java/org/talend/sdk/component/server/lang/MapCache.java @@ -24,18 +24,18 @@ @ApplicationScoped public class MapCache { - // simple - but enough - protection against too much entries in the cache - // this mainly targets ?query king of parameters + // simple - but enough - protection against too many entries in the cache + // this mainly targets ?query kind of parameters public void evictIfNeeded(final ConcurrentMap cache, final int maxSize) { - if (maxSize < 0) { + if (maxSize <= 0) { cache.clear(); return; } - while (cache.size() > maxSize) { - final Iterator> iterator = cache.entrySet().iterator(); - if (iterator.hasNext()) { - iterator.remove(); - } + final Iterator it = cache.keySet().iterator(); + int excess = cache.size() - maxSize; + while (excess-- > 0 && it.hasNext()) { + it.next(); + it.remove(); } } } diff --git a/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/lang/MapCacheTest.java b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/lang/MapCacheTest.java new file mode 100644 index 0000000000000..cd6e604636d80 --- /dev/null +++ b/component-server-parent/component-server/src/test/java/org/talend/sdk/component/server/lang/MapCacheTest.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2006-2026 Talend Inc. - www.talend.com + * + * Licensed 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.talend.sdk.component.server.lang; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class MapCacheTest { + + @ParameterizedTest + @MethodSource("emptyCacheTestCases") + void shouldEmptyCache(final ConcurrentMap cache, final int maxSize) { + new MapCache().evictIfNeeded(cache, maxSize); + assertTrue(cache.isEmpty()); + } + + public static Stream emptyCacheTestCases() { + return Stream.of( + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2")), 0), + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2")), -1), + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2", "3", "4")), 0) + ); + } + + @ParameterizedTest + @MethodSource("updatedCacheTestCases") + void shouldKeepTheExpectedNumberOfItems(final ConcurrentMap cache, final int maxSize) { + new MapCache().evictIfNeeded(cache, maxSize); + assertEquals(maxSize, cache.size()); + } + + public static Stream updatedCacheTestCases() { + return Stream.of( + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2", "3", "4", "5", "6")), 1), + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2", "3", "4", "5", "6")), 2) + ); + } + + @ParameterizedTest + @MethodSource("unmodifiedCacheTestCases") + void shouldNotUpdateTheCache(final ConcurrentMap cache, final int maxSize) { + int initialSize = cache.size(); + new MapCache().evictIfNeeded(cache, maxSize); + assertEquals(initialSize, cache.size()); + } + + public static Stream unmodifiedCacheTestCases() { + return Stream.of( + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2", "3", "4", "5", "6")), 3), + Arguments.of(new ConcurrentHashMap<>(Map.of("1", "2", "3", "4", "5", "6")), 4) + ); + } +}