From 5dfc2e8caeea6c77c20c52d0730d59cf4927ec54 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:46:56 +0000 Subject: [PATCH 1/2] Initial plan From 21b4d37723f78694459df752c50ca3399cadeaa7 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:50:58 +0000 Subject: [PATCH 2/2] Fix ConfigMap scheduled refresh recovery after transient failures --- .../KubernetesFromConfigMapProcessor.java | 10 ++++++- .../KubernetesFromConfigMapTest.java | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapProcessor.java b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapProcessor.java index 5312cd04d4..899999a7d0 100644 --- a/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapProcessor.java +++ b/spring/src/main/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapProcessor.java @@ -81,7 +81,15 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw fullyRefreshCache(configMapGetter, fromConfigMapAnnotation, configMapDataCache); configMapKeyRefresher.scheduleAtFixedRate( () -> { - fullyRefreshCache(configMapGetter, fromConfigMapAnnotation, configMapDataCache); + try { + fullyRefreshCache(configMapGetter, fromConfigMapAnnotation, configMapDataCache); + } catch (RuntimeException e) { + log.warn( + "Failed refreshing ConfigMap {}/{}", + fromConfigMapAnnotation.namespace(), + fromConfigMapAnnotation.name(), + e); + } }, manifestsProperties.getRefreshInterval().getSeconds(), manifestsProperties.getRefreshInterval().getSeconds(), diff --git a/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java b/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java index ec3c6bd770..4525805e51 100644 --- a/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java +++ b/spring/src/test/java/io/kubernetes/client/spring/extended/manifests/KubernetesFromConfigMapTest.java @@ -21,6 +21,7 @@ import io.kubernetes.client.spring.extended.manifests.configmaps.ConfigMapGetter; import java.time.Duration; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -93,6 +94,25 @@ void valueUpdate() { .until(() -> "bar2".equals(myBean.dynamicData.get("foo"))); } + @Test + void refreshRecoversAfterReadFailure() { + assertThat(myBean.dynamicData).containsEntry("foo", "bar1"); + int failedReads = mockAtomicConfigMapGetter.failedReads.get(); + mockAtomicConfigMapGetter.readFailure.set(new IllegalStateException("Transient API failure")); + try { + await().until(() -> mockAtomicConfigMapGetter.failedReads.get() > failedReads); + assertThat(myBean.dynamicData).containsExactlyEntriesOf(Map.of("foo", "bar1")); + } finally { + mockAtomicConfigMapGetter.readFailure.set(null); + } + + mockAtomicConfigMapGetter.configMapAtomicReference.set( + new V1ConfigMap().putDataItem("foo", "recovered")); + await() + .timeout(manifestsProperties.getRefreshInterval().multipliedBy(2)) + .until(() -> "recovered".equals(myBean.dynamicData.get("foo"))); + } + @Test void keyUpdate() { assertThat(manifestsProperties.getRefreshInterval()).isEqualTo(Duration.ofSeconds(1)); @@ -119,9 +139,16 @@ public V1ConfigMap get(String namespace, String name) { static class MockAtomicConfigMapGetter implements ConfigMapGetter { private final AtomicReference configMapAtomicReference = new AtomicReference<>(); + private final AtomicReference readFailure = new AtomicReference<>(); + private final AtomicInteger failedReads = new AtomicInteger(); @Override public V1ConfigMap get(String namespace, String name) { + RuntimeException failure = readFailure.get(); + if (failure != null) { + failedReads.incrementAndGet(); + throw failure; + } return configMapAtomicReference.get(); } }