From 65d8c0f51a9e4dcf92a22cc09035ab7764ba3157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 8 Mar 2026 15:55:45 +0100 Subject: [PATCH 01/36] Obsolete resource handling for read-cache-after-write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/ManagedInformerEventSource.java | 2 +- .../informer/TemporaryResourceCache.java | 40 ++++++++++++++++++- .../informer/InformerEventSourceTest.java | 3 +- .../TemporaryPrimaryResourceCacheTest.java | 9 +++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 2fc67c4892..bb6c04cf85 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -153,7 +153,7 @@ public synchronized void start() { if (isRunning()) { return; } - temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions); + temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions, this); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); cache.addIndexers(indexers); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 43d9dc1fab..c01a804617 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -54,12 +54,16 @@ public class TemporaryResourceCache { private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); + public static final long DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL = 1000 * 60 * 3L; private final Map cache = new ConcurrentHashMap<>(); + private final Map activeUpdates = new HashMap<>(); private final boolean comparableResourceVersions; private String latestResourceVersion; - private final Map activeUpdates = new HashMap<>(); + private final long obsoleteResourceCheckInterval; + private volatile long lastObsoleteResourceCheck = System.currentTimeMillis(); + private ManagedInformerEventSource managedInformerEventSource; public enum EventHandling { DEFER, @@ -67,8 +71,22 @@ public enum EventHandling { NEW } - public TemporaryResourceCache(boolean comparableResourceVersions) { + public TemporaryResourceCache( + boolean comparableResourceVersions, + ManagedInformerEventSource managedInformerEventSource) { + this( + comparableResourceVersions, + DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL, + managedInformerEventSource); + } + + TemporaryResourceCache( + boolean comparableResourceVersions, + long obsoleteResourceCheckInterval, + ManagedInformerEventSource managedInformerEventSource) { this.comparableResourceVersions = comparableResourceVersions; + this.obsoleteResourceCheckInterval = obsoleteResourceCheckInterval; + this.managedInformerEventSource = managedInformerEventSource; } public synchronized void startEventFilteringModify(ResourceID resourceID) { @@ -148,6 +166,7 @@ private synchronized EventHandling onEvent( result = EventHandling.OBSOLETE; } } + checkObsoleteResources(); var ed = activeUpdates.get(resourceId); if (ed != null && result != EventHandling.OBSOLETE) { log.debug("Setting last event for id: {} delete: {}", resourceId, delete); @@ -208,6 +227,23 @@ public synchronized void putResource(T newResource) { } } + void checkObsoleteResources() { + if (System.currentTimeMillis() > lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { + lastObsoleteResourceCheck = System.currentTimeMillis(); + log.debug("Checking for obsolete resources."); + var iterator = cache.entrySet().iterator(); + while (iterator.hasNext()) { + var e = iterator.next(); + if (ReconcilerUtilsInternal.compareResourceVersions( + e.getValue().getMetadata().getResourceVersion(), latestResourceVersion) + < 0) iterator.remove(); + // todo propagate event + managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); + log.debug("Removing obsolete resource with ID: {}", e.getKey()); + } + } + } + public synchronized Optional getResourceFromCache(ResourceID resourceID) { return Optional.ofNullable(cache.get(resourceID)); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 12c85ee342..9cf5060fbf 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -376,7 +376,8 @@ private CountDownLatch sendForEventFilteringUpdate(Deployment deployment, int re } private void withRealTemporaryResourceCache() { - temporaryResourceCache = spy(new TemporaryResourceCache<>(true)); + temporaryResourceCache = + spy(new TemporaryResourceCache<>(true, mock(ManagedInformerEventSource.class))); informerEventSource.setTemporalResourceCache(temporaryResourceCache); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 592a552433..db2b7adb70 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; class TemporaryPrimaryResourceCacheTest { @@ -38,7 +39,8 @@ class TemporaryPrimaryResourceCacheTest { @BeforeEach void setup() { - temporaryResourceCache = new TemporaryResourceCache<>(true); + temporaryResourceCache = + new TemporaryResourceCache<>(true, mock(ManagedInformerEventSource.class)); } @Test @@ -114,7 +116,8 @@ void removesResourceFromCache() { @Test void nonComparableResourceVersionsDisables() { - this.temporaryResourceCache = new TemporaryResourceCache<>(false); + this.temporaryResourceCache = + new TemporaryResourceCache<>(false, mock(ManagedInformerEventSource.class)); this.temporaryResourceCache.putResource(testResource()); @@ -123,7 +126,7 @@ void nonComparableResourceVersionsDisables() { } @Test - void eventReceivedDuringFiltering() throws Exception { + void eventReceivedDuringFiltering() { var testResource = testResource(); temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); From da70c5f84366e2e8d9bf90bb7ea9d54cc4963fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 8 Mar 2026 16:19:59 +0100 Subject: [PATCH 02/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/source/informer/TemporaryResourceCache.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index c01a804617..094774256c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -63,7 +63,7 @@ public class TemporaryResourceCache { private final long obsoleteResourceCheckInterval; private volatile long lastObsoleteResourceCheck = System.currentTimeMillis(); - private ManagedInformerEventSource managedInformerEventSource; + private final ManagedInformerEventSource managedInformerEventSource; public enum EventHandling { DEFER, @@ -237,7 +237,6 @@ void checkObsoleteResources() { if (ReconcilerUtilsInternal.compareResourceVersions( e.getValue().getMetadata().getResourceVersion(), latestResourceVersion) < 0) iterator.remove(); - // todo propagate event managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); log.debug("Removing obsolete resource with ID: {}", e.getKey()); } From a390fe63cb8bcb325f8c7422082f5eb8318dbd98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 11:00:51 +0100 Subject: [PATCH 03/36] latest resource version by informers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/ManagedInformerEventSource.java | 6 ++- .../informer/TemporaryResourceCache.java | 40 ++++++++++++++++--- .../informer/InformerEventSourceTest.java | 2 +- .../TemporaryPrimaryResourceCacheTest.java | 4 +- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index bb6c04cf85..298cd4325c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -153,7 +153,11 @@ public synchronized void start() { if (isRunning()) { return; } - temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions, this); + var watchesOnlyOneNamespace = + configuration.getInformerConfig().watchCurrentNamespace() + || configuration.getInformerConfig().watchAllNamespaces(); + temporaryResourceCache = + new TemporaryResourceCache<>(comparableResourceVersions, !watchesOnlyOneNamespace, this); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); cache.addIndexers(indexers); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 094774256c..8749d3599d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -59,7 +59,8 @@ public class TemporaryResourceCache { private final Map cache = new ConcurrentHashMap<>(); private final Map activeUpdates = new HashMap<>(); private final boolean comparableResourceVersions; - private String latestResourceVersion; + private final ConcurrentHashMap perNamespaceLatestResourceVersion; + private volatile String latestResourceVersion; private final long obsoleteResourceCheckInterval; private volatile long lastObsoleteResourceCheck = System.currentTimeMillis(); @@ -73,20 +74,28 @@ public enum EventHandling { public TemporaryResourceCache( boolean comparableResourceVersions, + boolean wrapsMultipleInformers, ManagedInformerEventSource managedInformerEventSource) { this( comparableResourceVersions, + wrapsMultipleInformers, DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL, managedInformerEventSource); } TemporaryResourceCache( boolean comparableResourceVersions, + boolean wrapsMultipleInformers, long obsoleteResourceCheckInterval, ManagedInformerEventSource managedInformerEventSource) { this.comparableResourceVersions = comparableResourceVersions; this.obsoleteResourceCheckInterval = obsoleteResourceCheckInterval; this.managedInformerEventSource = managedInformerEventSource; + if (wrapsMultipleInformers || !comparableResourceVersions) { + perNamespaceLatestResourceVersion = new ConcurrentHashMap<>(); + } else { + perNamespaceLatestResourceVersion = null; + } } public synchronized void startEventFilteringModify(ResourceID resourceID) { @@ -145,8 +154,7 @@ private synchronized EventHandling onEvent( log.debug("Processing event"); } if (!unknownState) { - latestResourceVersion = resource.getMetadata().getResourceVersion(); - log.debug("Setting latest resource version to: {}", latestResourceVersion); + setLatestResourceVersion(resource); } var cached = cache.get(resourceId); EventHandling result = EventHandling.NEW; @@ -202,15 +210,16 @@ public synchronized void putResource(T newResource) { // // this also prevents resurrecting recently deleted entities for which the delete event // has already been processed - if (latestResourceVersion != null + var latestRV = getLatestResourceVersion(newResource.getMetadata().getNamespace()); + if (latestRV != null && ReconcilerUtilsInternal.compareResourceVersions( - latestResourceVersion, newResource.getMetadata().getResourceVersion()) + latestRV, newResource.getMetadata().getResourceVersion()) > 0) { log.debug( "Resource {}: resourceVersion {} is not later than latest {}", resourceId, newResource.getMetadata().getResourceVersion(), - latestResourceVersion); + latestRV); return; } @@ -246,4 +255,23 @@ void checkObsoleteResources() { public synchronized Optional getResourceFromCache(ResourceID resourceID) { return Optional.ofNullable(cache.get(resourceID)); } + + private void setLatestResourceVersion(T resource) { + if (perNamespaceLatestResourceVersion == null) { + latestResourceVersion = resource.getMetadata().getResourceVersion(); + log.debug("Setting latest resource version to: {}", latestResourceVersion); + } else { + perNamespaceLatestResourceVersion.put( + resource.getMetadata().getNamespace(), resource.getMetadata().getResourceVersion()); + log.debug("Setting latest resource version to: {} for namesoace", latestResourceVersion); + } + } + + public String getLatestResourceVersion(String namespace) { + if (perNamespaceLatestResourceVersion == null) { + return latestResourceVersion; + } else { + return perNamespaceLatestResourceVersion.get(namespace); + } + } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 9cf5060fbf..edbcb823d5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -377,7 +377,7 @@ private CountDownLatch sendForEventFilteringUpdate(Deployment deployment, int re private void withRealTemporaryResourceCache() { temporaryResourceCache = - spy(new TemporaryResourceCache<>(true, mock(ManagedInformerEventSource.class))); + spy(new TemporaryResourceCache<>(true, false, mock(ManagedInformerEventSource.class))); informerEventSource.setTemporalResourceCache(temporaryResourceCache); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index db2b7adb70..21f394e656 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -40,7 +40,7 @@ class TemporaryPrimaryResourceCacheTest { @BeforeEach void setup() { temporaryResourceCache = - new TemporaryResourceCache<>(true, mock(ManagedInformerEventSource.class)); + new TemporaryResourceCache<>(true, false, mock(ManagedInformerEventSource.class)); } @Test @@ -117,7 +117,7 @@ void removesResourceFromCache() { @Test void nonComparableResourceVersionsDisables() { this.temporaryResourceCache = - new TemporaryResourceCache<>(false, mock(ManagedInformerEventSource.class)); + new TemporaryResourceCache<>(false, false, mock(ManagedInformerEventSource.class)); this.temporaryResourceCache.putResource(testResource()); From 8ef46ad660cd339bccf31cd414dba461e4ed66ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 12:29:34 +0100 Subject: [PATCH 04/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../source/informer/ManagedInformerEventSource.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 298cd4325c..64ed085929 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -32,7 +32,6 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; @@ -193,15 +192,11 @@ public Optional get(ResourceID resourceID) { // point the resource would already be present in the informer cache, but we would // have missed it in both caches during this call. Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); - var res = cache.get(resourceID); - if (comparableResourceVersions - && resource.isPresent() - && res.filter( - r -> ReconcilerUtilsInternal.compareResourceVersions(r, resource.orElseThrow()) > 0) - .isEmpty()) { + if (comparableResourceVersions && resource.isPresent()) { log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); return resource; } + var res = cache.get(resourceID); log.debug( "Resource not found, or older, in temporary cache. Found in informer cache {}, for" + " Resource ID: {}", From 18ee1c80af853ee6bf4f56855d794c160571d8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 12:41:28 +0100 Subject: [PATCH 05/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/ManagedInformerEventSource.java | 15 ++++++--------- .../source/informer/TemporaryResourceCache.java | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 64ed085929..1b1d6f669d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -186,15 +186,12 @@ public void handleRecentResourceCreate(ResourceID resourceID, R resource) { @Override public Optional get(ResourceID resourceID) { - // The order of these two lookups matters. If we queried the informer cache first, - // a race condition could occur: we might not find the resource there yet, then - // process an informer event that evicts the temporary resource cache entry. At that - // point the resource would already be present in the informer cache, but we would - // have missed it in both caches during this call. - Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); - if (comparableResourceVersions && resource.isPresent()) { - log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); - return resource; + if (comparableResourceVersions) { + Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); + if (resource.isPresent()) { + log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); + return resource; + } } var res = cache.get(resourceID); log.debug( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 8749d3599d..9ff37adda9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -263,7 +263,7 @@ private void setLatestResourceVersion(T resource) { } else { perNamespaceLatestResourceVersion.put( resource.getMetadata().getNamespace(), resource.getMetadata().getResourceVersion()); - log.debug("Setting latest resource version to: {} for namesoace", latestResourceVersion); + log.debug("Setting latest resource version to: {} for namespace", latestResourceVersion); } } From de8267da0d6fc14e7da2586487889948e3bb2790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 12:44:13 +0100 Subject: [PATCH 06/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/ManagedInformerEventSource.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 1b1d6f669d..298cd4325c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -32,6 +32,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.ReconcilerUtilsInternal; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.Informable; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; @@ -186,14 +187,21 @@ public void handleRecentResourceCreate(ResourceID resourceID, R resource) { @Override public Optional get(ResourceID resourceID) { - if (comparableResourceVersions) { - Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); - if (resource.isPresent()) { - log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); - return resource; - } - } + // The order of these two lookups matters. If we queried the informer cache first, + // a race condition could occur: we might not find the resource there yet, then + // process an informer event that evicts the temporary resource cache entry. At that + // point the resource would already be present in the informer cache, but we would + // have missed it in both caches during this call. + Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); var res = cache.get(resourceID); + if (comparableResourceVersions + && resource.isPresent() + && res.filter( + r -> ReconcilerUtilsInternal.compareResourceVersions(r, resource.orElseThrow()) > 0) + .isEmpty()) { + log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); + return resource; + } log.debug( "Resource not found, or older, in temporary cache. Found in informer cache {}, for" + " Resource ID: {}", From a5631c58a0c7eccd51e4fb4340ac9ae50b8c60a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 13:32:42 +0100 Subject: [PATCH 07/36] latest resync version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../source/informer/InformerManager.java | 7 +++ .../source/informer/InformerWrapper.java | 4 ++ .../informer/ManagedInformerEventSource.java | 6 +-- .../informer/TemporaryResourceCache.java | 39 +++------------ .../informer/InformerEventSourceTest.java | 8 +++- .../TemporaryPrimaryResourceCacheTest.java | 48 ++++++++++++------- 6 files changed, 54 insertions(+), 58 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 42e06c9d9a..711f7f6f92 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -235,6 +235,13 @@ private Optional> getSource(String namespace) { return Optional.ofNullable(sources.get(namespace)); } + String lastSyncResourceVersion(String namespace) { + return getSource(namespace) + .map(InformerWrapper::getInformer) + .orElseThrow() + .lastSyncResourceVersion(); + } + @Override public void addIndexers(Map>> indexers) { this.indexers.putAll(indexers); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 2f57f879b8..a07f97407f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -236,4 +236,8 @@ public Status getStatus() { public String getTargetNamespace() { return namespaceIdentifier; } + + public SharedIndexInformer getInformer() { + return informer; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 298cd4325c..bb6c04cf85 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -153,11 +153,7 @@ public synchronized void start() { if (isRunning()) { return; } - var watchesOnlyOneNamespace = - configuration.getInformerConfig().watchCurrentNamespace() - || configuration.getInformerConfig().watchAllNamespaces(); - temporaryResourceCache = - new TemporaryResourceCache<>(comparableResourceVersions, !watchesOnlyOneNamespace, this); + temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions, this); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); cache.addIndexers(indexers); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 9ff37adda9..b54462031a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -59,8 +59,6 @@ public class TemporaryResourceCache { private final Map cache = new ConcurrentHashMap<>(); private final Map activeUpdates = new HashMap<>(); private final boolean comparableResourceVersions; - private final ConcurrentHashMap perNamespaceLatestResourceVersion; - private volatile String latestResourceVersion; private final long obsoleteResourceCheckInterval; private volatile long lastObsoleteResourceCheck = System.currentTimeMillis(); @@ -74,28 +72,20 @@ public enum EventHandling { public TemporaryResourceCache( boolean comparableResourceVersions, - boolean wrapsMultipleInformers, ManagedInformerEventSource managedInformerEventSource) { this( comparableResourceVersions, - wrapsMultipleInformers, DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL, managedInformerEventSource); } TemporaryResourceCache( boolean comparableResourceVersions, - boolean wrapsMultipleInformers, long obsoleteResourceCheckInterval, ManagedInformerEventSource managedInformerEventSource) { this.comparableResourceVersions = comparableResourceVersions; this.obsoleteResourceCheckInterval = obsoleteResourceCheckInterval; this.managedInformerEventSource = managedInformerEventSource; - if (wrapsMultipleInformers || !comparableResourceVersions) { - perNamespaceLatestResourceVersion = new ConcurrentHashMap<>(); - } else { - perNamespaceLatestResourceVersion = null; - } } public synchronized void startEventFilteringModify(ResourceID resourceID) { @@ -153,9 +143,6 @@ private synchronized EventHandling onEvent( if (log.isDebugEnabled()) { log.debug("Processing event"); } - if (!unknownState) { - setLatestResourceVersion(resource); - } var cached = cache.get(resourceId); EventHandling result = EventHandling.NEW; if (cached != null) { @@ -236,6 +223,10 @@ public synchronized void putResource(T newResource) { } } + private String getLatestResourceVersion(String namespace) { + return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); + } + void checkObsoleteResources() { if (System.currentTimeMillis() > lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { lastObsoleteResourceCheck = System.currentTimeMillis(); @@ -244,7 +235,8 @@ void checkObsoleteResources() { while (iterator.hasNext()) { var e = iterator.next(); if (ReconcilerUtilsInternal.compareResourceVersions( - e.getValue().getMetadata().getResourceVersion(), latestResourceVersion) + e.getValue().getMetadata().getResourceVersion(), + getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) < 0) iterator.remove(); managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); log.debug("Removing obsolete resource with ID: {}", e.getKey()); @@ -255,23 +247,4 @@ void checkObsoleteResources() { public synchronized Optional getResourceFromCache(ResourceID resourceID) { return Optional.ofNullable(cache.get(resourceID)); } - - private void setLatestResourceVersion(T resource) { - if (perNamespaceLatestResourceVersion == null) { - latestResourceVersion = resource.getMetadata().getResourceVersion(); - log.debug("Setting latest resource version to: {}", latestResourceVersion); - } else { - perNamespaceLatestResourceVersion.put( - resource.getMetadata().getNamespace(), resource.getMetadata().getResourceVersion()); - log.debug("Setting latest resource version to: {} for namespace", latestResourceVersion); - } - } - - public String getLatestResourceVersion(String namespace) { - if (perNamespaceLatestResourceVersion == null) { - return latestResourceVersion; - } else { - return perNamespaceLatestResourceVersion.get(namespace); - } - } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index edbcb823d5..de09b18794 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -376,8 +376,12 @@ private CountDownLatch sendForEventFilteringUpdate(Deployment deployment, int re } private void withRealTemporaryResourceCache() { - temporaryResourceCache = - spy(new TemporaryResourceCache<>(true, false, mock(ManagedInformerEventSource.class))); + var mes = mock(ManagedInformerEventSource.class); + var mim = mock(InformerManager.class); + when(mes.manager()).thenReturn(mim); + when(mim.lastSyncResourceVersion(any())).thenReturn("1"); + + temporaryResourceCache = spy(new TemporaryResourceCache<>(true, mes)); informerEventSource.setTemporalResourceCache(temporaryResourceCache); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 21f394e656..cde415482a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -29,18 +29,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; private TemporaryResourceCache temporaryResourceCache; + private volatile String latestSyncVersion; @BeforeEach void setup() { - temporaryResourceCache = - new TemporaryResourceCache<>(true, false, mock(ManagedInformerEventSource.class)); + var mes = mock(ManagedInformerEventSource.class); + var mim = mock(InformerManager.class); + when(mes.manager()).thenReturn(mim); + when(mim.lastSyncResourceVersion(any())).then(a -> latestSyncVersion); + + temporaryResourceCache = new TemporaryResourceCache<>(true, mes); } @Test @@ -49,7 +56,7 @@ void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() var prevTestResource = testResource(); prevTestResource.getMetadata().setResourceVersion("1"); - temporaryResourceCache.putResource(testResource); + putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); @@ -63,8 +70,8 @@ void updateNotAddsTheResourceIntoCacheIfLaterVersionKnown() { ResourceAction.ADDED, testResource.toBuilder().editMetadata().withResourceVersion("3").endMetadata().build(), null); - - temporaryResourceCache.putResource(testResource); + latestSyncVersion = "3"; + putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isNotPresent(); @@ -74,7 +81,7 @@ void updateNotAddsTheResourceIntoCacheIfLaterVersionKnown() { void addOperationAddsTheResourceIfInformerCacheStillEmpty() { var testResource = testResource(); - temporaryResourceCache.putResource(testResource); + putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); @@ -84,9 +91,9 @@ void addOperationAddsTheResourceIfInformerCacheStillEmpty() { void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { var testResource = testResource(); - temporaryResourceCache.putResource(testResource); + putResource(testResource); - temporaryResourceCache.putResource( + putResource( new ConfigMapBuilder(testResource) .editMetadata() .withResourceVersion("1") @@ -109,7 +116,7 @@ void removesResourceFromCache() { .endMetadata() .build(), null); - + latestSyncVersion = "3"; assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isNotPresent(); } @@ -117,7 +124,7 @@ void removesResourceFromCache() { @Test void nonComparableResourceVersionsDisables() { this.temporaryResourceCache = - new TemporaryResourceCache<>(false, false, mock(ManagedInformerEventSource.class)); + new TemporaryResourceCache<>(false, mock(ManagedInformerEventSource.class)); this.temporaryResourceCache.putResource(testResource()); @@ -131,7 +138,7 @@ void eventReceivedDuringFiltering() { temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); - temporaryResourceCache.putResource(testResource); + putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); @@ -153,7 +160,7 @@ void newerEventDuringFiltering() { temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); - temporaryResourceCache.putResource(testResource); + putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); @@ -177,7 +184,7 @@ void eventAfterFiltering() { temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); - temporaryResourceCache.putResource(testResource); + putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); @@ -204,7 +211,7 @@ void putBeforeEvent() { var nextResource = testResource(); nextResource.getMetadata().setResourceVersion("3"); - temporaryResourceCache.putResource(nextResource); + putResource(nextResource); // the result is obsolete result = temporaryResourceCache.onAddOrUpdateEvent(ResourceAction.UPDATED, nextResource, null); @@ -225,7 +232,7 @@ void putBeforeEventWithEventFiltering() { var resourceId = ResourceID.fromResource(testResource); temporaryResourceCache.startEventFilteringModify(resourceId); - temporaryResourceCache.putResource(nextResource); + putResource(nextResource); temporaryResourceCache.doneEventFilterModify(resourceId, "3"); // the result is obsolete @@ -252,7 +259,7 @@ void putAfterEventWithEventFilteringNoPost() { ResourceAction.UPDATED, nextResource, testResource); // the result is deferred assertThat(result).isEqualTo(EventHandling.DEFER); - temporaryResourceCache.putResource(nextResource); + putResource(nextResource); var postEvent = temporaryResourceCache.doneEventFilterModify(resourceId, "3"); // there is no post event because the done call claimed responsibility for rv 3 @@ -291,15 +298,20 @@ void rapidDeletion() { .endMetadata() .build(), false); - temporaryResourceCache.putResource(testResource); + latestSyncVersion = "3"; + putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isEmpty(); } + private void putResource(ConfigMap testResource) { + temporaryResourceCache.putResource(testResource); + } + private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); - temporaryResourceCache.putResource(testResource); + putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); return testResource; From c6897115b46bf032bbc66373a3dc4041bf923a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 13:57:41 +0100 Subject: [PATCH 08/36] scheduled cleanup of obsolete resources, improved get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/ExecutorServiceManager.java | 7 ++++--- .../informer/ManagedInformerEventSource.java | 16 ++++++++++++---- .../source/informer/TemporaryResourceCache.java | 15 +++++++++++++-- .../source/informer/InformerEventSourceTest.java | 4 +++- .../TemporaryPrimaryResourceCacheTest.java | 8 +++++--- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index bb07c5a6cb..f0f5b39450 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -23,6 +23,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -39,7 +40,7 @@ public class ExecutorServiceManager { private static final Logger log = LoggerFactory.getLogger(ExecutorServiceManager.class); private ExecutorService executor; private ExecutorService workflowExecutor; - private ExecutorService cachingExecutorService; + private ScheduledExecutorService cachingExecutorService; private boolean started; private ConfigurationService configurationService; @@ -122,14 +123,14 @@ private synchronized void lazyInitWorkflowExecutorService() { } } - public ExecutorService cachingExecutorService() { + public ScheduledExecutorService cachingExecutorService() { return cachingExecutorService; } public void start(ConfigurationService configurationService) { if (!started) { this.configurationService = configurationService; // used to lazy init workflow executor - this.cachingExecutorService = Executors.newCachedThreadPool(); + this.cachingExecutorService = Executors.newScheduledThreadPool(0); this.executor = new InstrumentedExecutorService(configurationService.getExecutorService()); started = true; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index bb6c04cf85..082c485dd7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -153,7 +153,14 @@ public synchronized void start() { if (isRunning()) { return; } - temporaryResourceCache = new TemporaryResourceCache<>(comparableResourceVersions, this); + temporaryResourceCache = + new TemporaryResourceCache<>( + comparableResourceVersions, + controllerConfiguration + .getConfigurationService() + .getExecutorServiceManager() + .cachingExecutorService(), + this); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); cache.addIndexers(indexers); @@ -192,9 +199,10 @@ public Optional get(ResourceID resourceID) { var res = cache.get(resourceID); if (comparableResourceVersions && resource.isPresent() - && res.filter( - r -> ReconcilerUtilsInternal.compareResourceVersions(r, resource.orElseThrow()) > 0) - .isEmpty()) { + && ReconcilerUtilsInternal.compareResourceVersions( + resource.get().getMetadata().getResourceVersion(), + manager().lastSyncResourceVersion(resource.get().getMetadata().getNamespace())) + > 0) { log.debug("Latest resource found in temporary cache for Resource ID: {}", resourceID); return resource; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index b54462031a..964509287a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -19,6 +19,8 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,20 +74,30 @@ public enum EventHandling { public TemporaryResourceCache( boolean comparableResourceVersions, + ScheduledExecutorService obsoleteCheckExecutor, ManagedInformerEventSource managedInformerEventSource) { this( comparableResourceVersions, DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL, + obsoleteCheckExecutor, managedInformerEventSource); } TemporaryResourceCache( boolean comparableResourceVersions, long obsoleteResourceCheckInterval, + ScheduledExecutorService obsoleteCheckExecutor, ManagedInformerEventSource managedInformerEventSource) { this.comparableResourceVersions = comparableResourceVersions; this.obsoleteResourceCheckInterval = obsoleteResourceCheckInterval; this.managedInformerEventSource = managedInformerEventSource; + if (comparableResourceVersions) { + obsoleteCheckExecutor.scheduleWithFixedDelay( + this::checkObsoleteResources, + obsoleteResourceCheckInterval, + obsoleteResourceCheckInterval, + TimeUnit.MILLISECONDS); + } } public synchronized void startEventFilteringModify(ResourceID resourceID) { @@ -161,7 +173,6 @@ private synchronized EventHandling onEvent( result = EventHandling.OBSOLETE; } } - checkObsoleteResources(); var ed = activeUpdates.get(resourceId); if (ed != null && result != EventHandling.OBSOLETE) { log.debug("Setting last event for id: {} delete: {}", resourceId, delete); @@ -227,7 +238,7 @@ private String getLatestResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } - void checkObsoleteResources() { + private void checkObsoleteResources() { if (System.currentTimeMillis() > lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { lastObsoleteResourceCheck = System.currentTimeMillis(); log.debug("Checking for obsolete resources."); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index de09b18794..ec0d67a616 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -381,7 +382,8 @@ private void withRealTemporaryResourceCache() { when(mes.manager()).thenReturn(mim); when(mim.lastSyncResourceVersion(any())).thenReturn("1"); - temporaryResourceCache = spy(new TemporaryResourceCache<>(true, mes)); + temporaryResourceCache = + spy(new TemporaryResourceCache<>(true, mock(ScheduledExecutorService.class), mes)); informerEventSource.setTemporalResourceCache(temporaryResourceCache); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index cde415482a..813eb08a82 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -16,6 +16,7 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,8 +47,8 @@ void setup() { var mim = mock(InformerManager.class); when(mes.manager()).thenReturn(mim); when(mim.lastSyncResourceVersion(any())).then(a -> latestSyncVersion); - - temporaryResourceCache = new TemporaryResourceCache<>(true, mes); + temporaryResourceCache = + new TemporaryResourceCache<>(true, mock(ScheduledExecutorService.class), mes); } @Test @@ -124,7 +125,8 @@ void removesResourceFromCache() { @Test void nonComparableResourceVersionsDisables() { this.temporaryResourceCache = - new TemporaryResourceCache<>(false, mock(ManagedInformerEventSource.class)); + new TemporaryResourceCache<>( + false, mock(ScheduledExecutorService.class), mock(ManagedInformerEventSource.class)); this.temporaryResourceCache.putResource(testResource()); From 5d7de15d4ae750207fb36d2e2b9898782382d507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 14:38:36 +0100 Subject: [PATCH 09/36] obsolete handling unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/ManagedInformerEventSource.java | 5 -- .../TemporaryPrimaryResourceCacheTest.java | 69 ++++++++++++++----- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 082c485dd7..7486a10929 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -190,11 +190,6 @@ public void handleRecentResourceCreate(ResourceID resourceID, R resource) { @Override public Optional get(ResourceID resourceID) { - // The order of these two lookups matters. If we queried the informer cache first, - // a race condition could occur: we might not find the resource there yet, then - // process an informer event that evicts the temporary resource cache entry. At that - // point the resource would already be present in the informer cache, but we would - // have missed it in both caches during this call. Optional resource = temporaryResourceCache.getResourceFromCache(resourceID); var res = cache.get(resourceID); if (comparableResourceVersions diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 813eb08a82..8a1e963120 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -15,7 +15,9 @@ */ package io.javaoperatorsdk.operator.processing.event.source.informer; +import java.time.Duration; import java.util.Map; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import org.junit.jupiter.api.BeforeEach; @@ -29,6 +31,7 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.TemporaryResourceCache.EventHandling; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -37,18 +40,23 @@ class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; + public static final int OBSOLETE_RESOURCE_CHECK_INTERVAL = 100; private TemporaryResourceCache temporaryResourceCache; private volatile String latestSyncVersion; + private ManagedInformerEventSource managedInformerEventSource = + mock(ManagedInformerEventSource.class); @BeforeEach void setup() { - var mes = mock(ManagedInformerEventSource.class); + latestSyncVersion = "1"; + managedInformerEventSource = mock(ManagedInformerEventSource.class); var mim = mock(InformerManager.class); - when(mes.manager()).thenReturn(mim); + when(managedInformerEventSource.manager()).thenReturn(mim); when(mim.lastSyncResourceVersion(any())).then(a -> latestSyncVersion); temporaryResourceCache = - new TemporaryResourceCache<>(true, mock(ScheduledExecutorService.class), mes); + new TemporaryResourceCache<>( + true, mock(ScheduledExecutorService.class), managedInformerEventSource); } @Test @@ -57,7 +65,7 @@ void updateAddsTheResourceIntoCacheIfTheInformerHasThePreviousResourceVersion() var prevTestResource = testResource(); prevTestResource.getMetadata().setResourceVersion("1"); - putResource(testResource); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); @@ -72,7 +80,7 @@ void updateNotAddsTheResourceIntoCacheIfLaterVersionKnown() { testResource.toBuilder().editMetadata().withResourceVersion("3").endMetadata().build(), null); latestSyncVersion = "3"; - putResource(testResource); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isNotPresent(); @@ -82,7 +90,7 @@ void updateNotAddsTheResourceIntoCacheIfLaterVersionKnown() { void addOperationAddsTheResourceIfInformerCacheStillEmpty() { var testResource = testResource(); - putResource(testResource); + temporaryResourceCache.putResource(testResource); var cached = temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource)); assertThat(cached).isPresent(); @@ -92,9 +100,9 @@ void addOperationAddsTheResourceIfInformerCacheStillEmpty() { void addOperationNotAddsTheResourceIfInformerCacheNotEmpty() { var testResource = testResource(); - putResource(testResource); + temporaryResourceCache.putResource(testResource); - putResource( + temporaryResourceCache.putResource( new ConfigMapBuilder(testResource) .editMetadata() .withResourceVersion("1") @@ -140,7 +148,7 @@ void eventReceivedDuringFiltering() { temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); - putResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); @@ -162,7 +170,7 @@ void newerEventDuringFiltering() { temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); - putResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); @@ -186,7 +194,7 @@ void eventAfterFiltering() { temporaryResourceCache.startEventFilteringModify(ResourceID.fromResource(testResource)); - putResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); @@ -213,7 +221,7 @@ void putBeforeEvent() { var nextResource = testResource(); nextResource.getMetadata().setResourceVersion("3"); - putResource(nextResource); + temporaryResourceCache.putResource(nextResource); // the result is obsolete result = temporaryResourceCache.onAddOrUpdateEvent(ResourceAction.UPDATED, nextResource, null); @@ -228,16 +236,18 @@ void putBeforeEventWithEventFiltering() { var result = temporaryResourceCache.onAddOrUpdateEvent(ResourceAction.ADDED, testResource, null); assertThat(result).isEqualTo(EventHandling.NEW); + latestSyncVersion = RESOURCE_VERSION; var nextResource = testResource(); nextResource.getMetadata().setResourceVersion("3"); var resourceId = ResourceID.fromResource(testResource); temporaryResourceCache.startEventFilteringModify(resourceId); - putResource(nextResource); + temporaryResourceCache.putResource(nextResource); temporaryResourceCache.doneEventFilterModify(resourceId, "3"); // the result is obsolete + latestSyncVersion = "3"; result = temporaryResourceCache.onAddOrUpdateEvent(ResourceAction.UPDATED, nextResource, null); assertThat(result).isEqualTo(EventHandling.OBSOLETE); } @@ -261,7 +271,7 @@ void putAfterEventWithEventFilteringNoPost() { ResourceAction.UPDATED, nextResource, testResource); // the result is deferred assertThat(result).isEqualTo(EventHandling.DEFER); - putResource(nextResource); + temporaryResourceCache.putResource(testResource); var postEvent = temporaryResourceCache.doneEventFilterModify(resourceId, "3"); // there is no post event because the done call claimed responsibility for rv 3 @@ -301,19 +311,42 @@ void rapidDeletion() { .build(), false); latestSyncVersion = "3"; - putResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isEmpty(); } - private void putResource(ConfigMap testResource) { - temporaryResourceCache.putResource(testResource); + @Test + void removalOfObsoleteResources() { + this.temporaryResourceCache = + new TemporaryResourceCache<>( + true, + OBSOLETE_RESOURCE_CHECK_INTERVAL, + Executors.newScheduledThreadPool(1), + managedInformerEventSource); + var tr = testResource(); + this.temporaryResourceCache.putResource(tr); + + await() + .pollDelay(Duration.ofMillis(2 * OBSOLETE_RESOURCE_CHECK_INTERVAL)) + .untilAsserted( + () -> + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))) + .isPresent()); + + latestSyncVersion = "3"; + + await() + .untilAsserted( + () -> + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))) + .isEmpty()); } private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); - putResource(testResource); + temporaryResourceCache.putResource(testResource); assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(testResource))) .isPresent(); return testResource; From 9b49b9f9c9707a6dd645be7979fb7c502fb645cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 14:49:27 +0100 Subject: [PATCH 10/36] additional unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/TemporaryResourceCache.java | 10 ++-- .../TemporaryPrimaryResourceCacheTest.java | 56 +++++++++++++++++-- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 964509287a..216396d4e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -239,7 +239,7 @@ private String getLatestResourceVersion(String namespace) { } private void checkObsoleteResources() { - if (System.currentTimeMillis() > lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { + if (System.currentTimeMillis() >= lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { lastObsoleteResourceCheck = System.currentTimeMillis(); log.debug("Checking for obsolete resources."); var iterator = cache.entrySet().iterator(); @@ -248,9 +248,11 @@ private void checkObsoleteResources() { if (ReconcilerUtilsInternal.compareResourceVersions( e.getValue().getMetadata().getResourceVersion(), getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) - < 0) iterator.remove(); - managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); - log.debug("Removing obsolete resource with ID: {}", e.getKey()); + < 0) { + iterator.remove(); + managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); + log.debug("Removing obsolete resource with ID: {}", e.getKey()); + } } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 8a1e963120..426870ae0e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -319,12 +319,8 @@ void rapidDeletion() { @Test void removalOfObsoleteResources() { - this.temporaryResourceCache = - new TemporaryResourceCache<>( - true, - OBSOLETE_RESOURCE_CHECK_INTERVAL, - Executors.newScheduledThreadPool(1), - managedInformerEventSource); + withTemporaryResourceCacheForObsoleteHandling(); + var tr = testResource(); this.temporaryResourceCache.putResource(tr); @@ -344,6 +340,54 @@ void removalOfObsoleteResources() { .isEmpty()); } + @Test + void checksObsoleteOnlyWithCertainDelay() { + withTemporaryResourceCacheForObsoleteHandling(); + this.temporaryResourceCache.putResource(testResource()); + latestSyncVersion = "3"; + await() + .pollDelay(Duration.ofMillis(OBSOLETE_RESOURCE_CHECK_INTERVAL / 5)) + .untilAsserted( + () -> + assertThat( + temporaryResourceCache.getResourceFromCache( + ResourceID.fromResource(testResource()))) + .isPresent()); + + await() + .untilAsserted( + () -> + assertThat( + temporaryResourceCache.getResourceFromCache( + ResourceID.fromResource(testResource()))) + .isEmpty()); + } + + @Test + void obsoleteResourceIsNotRemovedIfLatestSyncVersionIsOlder() { + withTemporaryResourceCacheForObsoleteHandling(); + this.temporaryResourceCache.putResource(testResource()); + latestSyncVersion = "1"; + + await() + .pollDelay(Duration.ofMillis(OBSOLETE_RESOURCE_CHECK_INTERVAL * 2)) + .untilAsserted( + () -> + assertThat( + temporaryResourceCache.getResourceFromCache( + ResourceID.fromResource(testResource()))) + .isPresent()); + } + + private void withTemporaryResourceCacheForObsoleteHandling() { + this.temporaryResourceCache = + new TemporaryResourceCache<>( + true, + OBSOLETE_RESOURCE_CHECK_INTERVAL, + Executors.newScheduledThreadPool(1), + managedInformerEventSource); + } + private ConfigMap propagateTestResourceToCache() { var testResource = testResource(); temporaryResourceCache.putResource(testResource); From 720b8caa258e2c7b06aebe367c1aa37d0f88cf3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 15:44:17 +0100 Subject: [PATCH 11/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/informer/Informer.java | 11 ++++++++ .../informer/InformerConfiguration.java | 22 +++++++++++++-- .../InformerEventSourceConfiguration.java | 28 ++++++++++++++++--- .../operator/api/reconciler/Constants.java | 5 ++++ .../informer/ManagedInformerEventSource.java | 1 + .../informer/TemporaryResourceCache.java | 19 +++++-------- .../controller/ControllerEventSourceTest.java | 6 +++- .../informer/InformerEventSourceTest.java | 2 +- .../TemporaryPrimaryResourceCacheTest.java | 5 ++-- 9 files changed, 77 insertions(+), 22 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index e6655641a2..cd7c88b1e1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -27,9 +27,11 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; +import io.javaoperatorsdk.operator.processing.event.source.informer.TemporaryResourceCache; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_LONG_VALUE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET; @@ -139,4 +141,13 @@ * @since 5.3.0 */ boolean comparableResourceVersions() default DEFAULT_COMPARABLE_RESOURCE_VERSION; + + /** + * For read-cache-after-write consistency there are some corner cases where we need to check the + * caches see {@link TemporaryResourceCache#checkObsoleteResources()} periodically. This is the + * period in milliseconds. Applicable only if {@link #comparableResourceVersions()}} is true. + * + * @since 5.3.0 + */ + long obsoleteResourceCacheCheckInterval() default DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index f6caa4fe4d..e7bf04d4c2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -15,6 +15,7 @@ */ package io.javaoperatorsdk.operator.api.config.informer; +import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -54,6 +55,7 @@ public class InformerConfiguration { private Long informerListLimit; private FieldSelector fieldSelector; private boolean comparableResourceVersions; + private Duration obsoleteResourceCacheCheckInterval; protected InformerConfiguration( Class resourceClass, @@ -68,7 +70,8 @@ protected InformerConfiguration( ItemStore itemStore, Long informerListLimit, FieldSelector fieldSelector, - boolean comparableResourceVersions) { + boolean comparableResourceVersions, + Duration obsoleteResourceCacheCheckInterval) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -82,6 +85,7 @@ protected InformerConfiguration( this.informerListLimit = informerListLimit; this.fieldSelector = fieldSelector; this.comparableResourceVersions = comparableResourceVersions; + this.obsoleteResourceCacheCheckInterval = obsoleteResourceCacheCheckInterval; } private InformerConfiguration(Class resourceClass) { @@ -117,7 +121,8 @@ public static InformerConfiguration.Builder builder( original.itemStore, original.informerListLimit, original.fieldSelector, - original.comparableResourceVersions) + original.comparableResourceVersions, + original.obsoleteResourceCacheCheckInterval) .builder; } @@ -296,6 +301,10 @@ public boolean isComparableResourceVersions() { return comparableResourceVersions; } + public Duration getObsoleteResourceCacheCheckInterval() { + return obsoleteResourceCacheCheckInterval; + } + @SuppressWarnings("UnusedReturnValue") public class Builder { @@ -368,6 +377,8 @@ public InformerConfiguration.Builder initFromAnnotation( .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated())) .toList())); withComparableResourceVersions(informerConfig.comparableResourceVersions()); + withObsoleteResourceCacheCheckInterval( + Duration.ofMillis(informerConfig.obsoleteResourceCacheCheckInterval())); } return this; } @@ -473,5 +484,12 @@ public Builder withComparableResourceVersions(boolean comparableResourceVersions InformerConfiguration.this.comparableResourceVersions = comparableResourceVersions; return this; } + + public Builder withObsoleteResourceCacheCheckInterval( + Duration obsoleteResourceCacheCheckInterval) { + InformerConfiguration.this.obsoleteResourceCacheCheckInterval = + obsoleteResourceCacheCheckInterval; + return this; + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index 69903e805f..53db0a39e2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -15,6 +15,7 @@ */ package io.javaoperatorsdk.operator.api.config.informer; +import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -34,6 +35,7 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; import static io.javaoperatorsdk.operator.api.reconciler.Constants.SAME_AS_CONTROLLER_NAMESPACES_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE_SET; @@ -90,6 +92,10 @@ default Optional getKubernetesClient() { return Optional.empty(); } + boolean comparableResourceVersion(); + + Duration getObsoleteResourceCacheCheckInterval(); + class DefaultInformerEventSourceConfiguration implements InformerEventSourceConfiguration { private final PrimaryToSecondaryMapper primaryToSecondaryMapper; @@ -98,6 +104,7 @@ class DefaultInformerEventSourceConfiguration private final InformerConfiguration informerConfig; private final KubernetesClient kubernetesClient; private final boolean comparableResourceVersion; + private final Duration obsoleteResourceCacheCheckInterval; protected DefaultInformerEventSourceConfiguration( GroupVersionKind groupVersionKind, @@ -105,13 +112,15 @@ protected DefaultInformerEventSourceConfiguration( SecondaryToPrimaryMapper secondaryToPrimaryMapper, InformerConfiguration informerConfig, KubernetesClient kubernetesClient, - boolean comparableResourceVersion) { + boolean comparableResourceVersion, + Duration obsoleteResourceCacheCheckInterval) { this.informerConfig = Objects.requireNonNull(informerConfig); this.groupVersionKind = groupVersionKind; this.primaryToSecondaryMapper = primaryToSecondaryMapper; this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; this.kubernetesClient = kubernetesClient; this.comparableResourceVersion = comparableResourceVersion; + this.obsoleteResourceCacheCheckInterval = obsoleteResourceCacheCheckInterval; } @Override @@ -144,6 +153,11 @@ public Optional getKubernetesClient() { public boolean comparableResourceVersion() { return this.comparableResourceVersion; } + + @Override + public Duration getObsoleteResourceCacheCheckInterval() { + return obsoleteResourceCacheCheckInterval; + } } @SuppressWarnings({"unused", "UnusedReturnValue"}) @@ -158,6 +172,7 @@ class Builder { private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private KubernetesClient kubernetesClient; private boolean comparableResourceVersion = DEFAULT_COMPARABLE_RESOURCE_VERSION; + private Duration obsoleteResourceCacheCheckInterval = DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; private Builder(Class resourceClass, Class primaryResourceClass) { this(resourceClass, primaryResourceClass, null); @@ -300,6 +315,12 @@ public Builder withComparableResourceVersion(boolean comparableResourceVersio return this; } + public Builder withObsoleteResourceCacheCheckInterval( + Duration obsoleteResourceCacheCheckInterval) { + this.obsoleteResourceCacheCheckInterval = obsoleteResourceCacheCheckInterval; + return this; + } + public void updateFrom(InformerConfiguration informerConfig) { if (informerConfig != null) { final var informerConfigName = informerConfig.getName(); @@ -340,9 +361,8 @@ public InformerEventSourceConfiguration build() { false)), config.build(), kubernetesClient, - comparableResourceVersion); + comparableResourceVersion, + obsoleteResourceCacheCheckInterval); } } - - boolean comparableResourceVersion(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index 7330a407c1..0cefebd78f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -15,6 +15,7 @@ */ package io.javaoperatorsdk.operator.api.reconciler; +import java.time.Duration; import java.util.Collections; import java.util.Set; @@ -43,5 +44,9 @@ public final class Constants { public static final boolean DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES = true; public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSION = true; + public static final long DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS = 3 * 60 * 1000; + public static final Duration DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL = + Duration.ofMillis(DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS); + private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index 7486a10929..c6f07a0188 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -156,6 +156,7 @@ public synchronized void start() { temporaryResourceCache = new TemporaryResourceCache<>( comparableResourceVersions, + configuration.getInformerConfig().getObsoleteResourceCacheCheckInterval().toMillis(), controllerConfiguration .getConfigurationService() .getExecutorServiceManager() diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 216396d4e2..d7be8b5240 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -56,7 +56,6 @@ public class TemporaryResourceCache { private static final Logger log = LoggerFactory.getLogger(TemporaryResourceCache.class); - public static final long DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL = 1000 * 60 * 3L; private final Map cache = new ConcurrentHashMap<>(); private final Map activeUpdates = new HashMap<>(); @@ -73,17 +72,6 @@ public enum EventHandling { } public TemporaryResourceCache( - boolean comparableResourceVersions, - ScheduledExecutorService obsoleteCheckExecutor, - ManagedInformerEventSource managedInformerEventSource) { - this( - comparableResourceVersions, - DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL, - obsoleteCheckExecutor, - managedInformerEventSource); - } - - TemporaryResourceCache( boolean comparableResourceVersions, long obsoleteResourceCheckInterval, ScheduledExecutorService obsoleteCheckExecutor, @@ -238,6 +226,13 @@ private String getLatestResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } + /** + * There are (probably) extremely rare circumstances, when we can miss a delete event related to a + * resources: when we create a resource that is deleted right after by third party and the related + * informer have a disconnected watch and this watch needs to do a re-list when connected again. + * In this case neither the ADD nor DELETE event will be propagated to the informer, but we + * explicitly add resources to this cache. Those are cleaned up by this check. + */ private void checkObsoleteResources() { if (System.currentTimeMillis() >= lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { lastObsoleteResourceCheck = System.currentTimeMillis(); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index df450b29a6..523034e100 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -30,6 +30,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ResolvedControllerConfiguration; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.processing.Controller; @@ -60,8 +61,11 @@ class ControllerEventSourceTest @BeforeEach public void setup() { - when(controllerConfig.getConfigurationService()).thenReturn(new BaseConfigurationService()); + var ic = mock(InformerConfiguration.class); + when(controllerConfig.getInformerConfig()).thenReturn(ic); + when(ic.getObsoleteResourceCacheCheckInterval()) + .thenReturn(Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL); setUpSource(new ControllerEventSource<>(testController), true, controllerConfig); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index ec0d67a616..79031ce4f3 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -383,7 +383,7 @@ private void withRealTemporaryResourceCache() { when(mim.lastSyncResourceVersion(any())).thenReturn("1"); temporaryResourceCache = - spy(new TemporaryResourceCache<>(true, mock(ScheduledExecutorService.class), mes)); + spy(new TemporaryResourceCache<>(true, 0, mock(ScheduledExecutorService.class), mes)); informerEventSource.setTemporalResourceCache(temporaryResourceCache); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 426870ae0e..ad9a204957 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@SuppressWarnings({"unchecked", "rawtypes"}) class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; @@ -56,7 +57,7 @@ void setup() { when(mim.lastSyncResourceVersion(any())).then(a -> latestSyncVersion); temporaryResourceCache = new TemporaryResourceCache<>( - true, mock(ScheduledExecutorService.class), managedInformerEventSource); + true, 0, mock(ScheduledExecutorService.class), managedInformerEventSource); } @Test @@ -134,7 +135,7 @@ void removesResourceFromCache() { void nonComparableResourceVersionsDisables() { this.temporaryResourceCache = new TemporaryResourceCache<>( - false, mock(ScheduledExecutorService.class), mock(ManagedInformerEventSource.class)); + false, 0, mock(ScheduledExecutorService.class), mock(ManagedInformerEventSource.class)); this.temporaryResourceCache.putResource(testResource()); From 5421153faefce4a2eb773420c2ed8cc48f23bdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 16:17:27 +0100 Subject: [PATCH 12/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/source/informer/InformerEventSourceTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 79031ce4f3..ca158c416d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -37,6 +37,7 @@ import io.javaoperatorsdk.operator.api.config.InformerStoppedHandler; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.processing.event.EventHandler; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.processing.event.source.EventFilterTestUtils; @@ -87,6 +88,8 @@ void setup() { when(informerEventSourceConfiguration.getInformerConfig()).thenReturn(informerConfig); when(informerConfig.getEffectiveNamespaces(any())).thenReturn(DEFAULT_NAMESPACES_SET); when(informerEventSourceConfiguration.getResourceClass()).thenReturn(Deployment.class); + when(informerConfig.getObsoleteResourceCacheCheckInterval()) + .thenReturn(Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL); informerEventSource = spy( new InformerEventSource<>(informerEventSourceConfiguration, clientMock) { @@ -383,7 +386,7 @@ private void withRealTemporaryResourceCache() { when(mim.lastSyncResourceVersion(any())).thenReturn("1"); temporaryResourceCache = - spy(new TemporaryResourceCache<>(true, 0, mock(ScheduledExecutorService.class), mes)); + spy(new TemporaryResourceCache<>(true, 100, mock(ScheduledExecutorService.class), mes)); informerEventSource.setTemporalResourceCache(temporaryResourceCache); } From 68861abdd55daba45ebde3b46d5494bf9de6c744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 17:48:49 +0100 Subject: [PATCH 13/36] test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/source/controller/ControllerEventSourceTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index 523034e100..15ad5d02f0 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -329,6 +329,8 @@ public TestConfiguration( .withOnUpdateFilter(onUpdateFilter) .withGenericFilter(genericFilter) .withComparableResourceVersions(true) + .withObsoleteResourceCacheCheckInterval( + Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL) .buildForController(), false); } From 6d0bfac687d467bc0d547a6c38c75fcedc0d71a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 18:03:38 +0100 Subject: [PATCH 14/36] logger config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- sample-operators/leader-election/src/test/resources/log4j2.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-operators/leader-election/src/test/resources/log4j2.xml b/sample-operators/leader-election/src/test/resources/log4j2.xml index 8b1c5ca270..2979258355 100644 --- a/sample-operators/leader-election/src/test/resources/log4j2.xml +++ b/sample-operators/leader-election/src/test/resources/log4j2.xml @@ -19,7 +19,7 @@ - + From ffd050f4d7c2fda4a271389cd2a3d1773e7e97c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 18:11:18 +0100 Subject: [PATCH 15/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/source/informer/TemporaryResourceCache.java | 1 + 1 file changed, 1 insertion(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index d7be8b5240..e1d296508b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -245,6 +245,7 @@ private void checkObsoleteResources() { getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) < 0) { iterator.remove(); + // todo test delete event propagation? managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); log.debug("Removing obsolete resource with ID: {}", e.getKey()); } From 386e280c5003edafbf74f91e39539c85d6fff6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 9 Mar 2026 20:05:57 +0100 Subject: [PATCH 16/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/source/informer/TemporaryResourceCache.java | 1 + 1 file changed, 1 insertion(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index e1d296508b..38ccc5e79b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -226,6 +226,7 @@ private String getLatestResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } + // todo obsolete is the good word? /** * There are (probably) extremely rare circumstances, when we can miss a delete event related to a * resources: when we create a resource that is deleted right after by third party and the related From 706d162fafd917cecacd901dfb689a21523258a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:03:42 +0100 Subject: [PATCH 17/36] hardedning obsolete cleanup conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../source/informer/TemporaryResourceCache.java | 13 ++++++++----- .../informer/TemporaryPrimaryResourceCacheTest.java | 6 ++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 38ccc5e79b..9e7ee38d79 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -226,7 +226,6 @@ private String getLatestResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } - // todo obsolete is the good word? /** * There are (probably) extremely rare circumstances, when we can miss a delete event related to a * resources: when we create a resource that is deleted right after by third party and the related @@ -242,11 +241,15 @@ private void checkObsoleteResources() { while (iterator.hasNext()) { var e = iterator.next(); if (ReconcilerUtilsInternal.compareResourceVersions( - e.getValue().getMetadata().getResourceVersion(), - getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) - < 0) { + e.getValue().getMetadata().getResourceVersion(), + getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) + < 0 + // making sure we have the situation where resource is missing from the cache + && managedInformerEventSource + .manager() + .get(ResourceID.fromResource(e.getValue())) + .isEmpty()) { iterator.remove(); - // todo test delete event propagation? managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); log.debug("Removing obsolete resource with ID: {}", e.getKey()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index ad9a204957..19b913bf04 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -34,7 +34,11 @@ import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked", "rawtypes"}) @@ -339,6 +343,8 @@ void removalOfObsoleteResources() { () -> assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))) .isEmpty()); + verify(managedInformerEventSource, times(1)) + .handleEvent(eq(ResourceAction.DELETED), eq(tr), isNull(), eq(true)); } @Test From 9e271141964b68467ffeeb88fc6b8083d8cf4359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:04:15 +0100 Subject: [PATCH 18/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/source/informer/TemporaryResourceCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 9e7ee38d79..7116f5c37a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -227,7 +227,7 @@ private String getLatestResourceVersion(String namespace) { } /** - * There are (probably) extremely rare circumstances, when we can miss a delete event related to a + * There are (probably extremely rare) circumstances, when we can miss a delete event related to a * resources: when we create a resource that is deleted right after by third party and the related * informer have a disconnected watch and this watch needs to do a re-list when connected again. * In this case neither the ADD nor DELETE event will be propagated to the informer, but we From 71a7e109263dad851062374a2778e8193d5a9ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:07:03 +0100 Subject: [PATCH 19/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- sample-operators/leader-election/pom.xml | 5 ----- .../metrics-processing/src/main/resources/log4j2.xml | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sample-operators/leader-election/pom.xml b/sample-operators/leader-election/pom.xml index 8194b433fc..4f896485d1 100644 --- a/sample-operators/leader-election/pom.xml +++ b/sample-operators/leader-election/pom.xml @@ -57,11 +57,6 @@ log4j-core compile - - org.takes - takes - 1.25.0 - org.awaitility awaitility diff --git a/sample-operators/metrics-processing/src/main/resources/log4j2.xml b/sample-operators/metrics-processing/src/main/resources/log4j2.xml index 2979258355..593f120e0b 100644 --- a/sample-operators/metrics-processing/src/main/resources/log4j2.xml +++ b/sample-operators/metrics-processing/src/main/resources/log4j2.xml @@ -23,7 +23,10 @@ - + + + + From 771049dd97808fa96589e297ea066296887eb60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:07:22 +0100 Subject: [PATCH 20/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../leader-election/src/test/resources/log4j2.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sample-operators/leader-election/src/test/resources/log4j2.xml b/sample-operators/leader-election/src/test/resources/log4j2.xml index 2979258355..593f120e0b 100644 --- a/sample-operators/leader-election/src/test/resources/log4j2.xml +++ b/sample-operators/leader-election/src/test/resources/log4j2.xml @@ -23,7 +23,10 @@ - + + + + From 66b96651e586840a9101b0033999213d02a1c105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:16:00 +0100 Subject: [PATCH 21/36] remove interval doublecheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/TemporaryResourceCache.java | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 7116f5c37a..c5802c3cce 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -62,7 +62,6 @@ public class TemporaryResourceCache { private final boolean comparableResourceVersions; private final long obsoleteResourceCheckInterval; - private volatile long lastObsoleteResourceCheck = System.currentTimeMillis(); private final ManagedInformerEventSource managedInformerEventSource; public enum EventHandling { @@ -234,25 +233,22 @@ private String getLatestResourceVersion(String namespace) { * explicitly add resources to this cache. Those are cleaned up by this check. */ private void checkObsoleteResources() { - if (System.currentTimeMillis() >= lastObsoleteResourceCheck + obsoleteResourceCheckInterval) { - lastObsoleteResourceCheck = System.currentTimeMillis(); - log.debug("Checking for obsolete resources."); - var iterator = cache.entrySet().iterator(); - while (iterator.hasNext()) { - var e = iterator.next(); - if (ReconcilerUtilsInternal.compareResourceVersions( - e.getValue().getMetadata().getResourceVersion(), - getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) - < 0 - // making sure we have the situation where resource is missing from the cache - && managedInformerEventSource - .manager() - .get(ResourceID.fromResource(e.getValue())) - .isEmpty()) { - iterator.remove(); - managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); - log.debug("Removing obsolete resource with ID: {}", e.getKey()); - } + log.debug("Checking for obsolete resources."); + var iterator = cache.entrySet().iterator(); + while (iterator.hasNext()) { + var e = iterator.next(); + if (ReconcilerUtilsInternal.compareResourceVersions( + e.getValue().getMetadata().getResourceVersion(), + getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) + < 0 + // making sure we have the situation where resource is missing from the cache + && managedInformerEventSource + .manager() + .get(ResourceID.fromResource(e.getValue())) + .isEmpty()) { + iterator.remove(); + managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); + log.debug("Removing obsolete resource with ID: {}", e.getKey()); } } } From f834fb6223bd83164af689895a7d7d00c8932e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:41:02 +0100 Subject: [PATCH 22/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/source/informer/InformerWrapper.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index a07f97407f..2f57f879b8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -236,8 +236,4 @@ public Status getStatus() { public String getTargetNamespace() { return namespaceIdentifier; } - - public SharedIndexInformer getInformer() { - return informer; - } } From d7d484e49590b3533345eb9c4ed7a1cac0001197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 09:45:12 +0100 Subject: [PATCH 23/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/source/informer/InformerWrapper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index 2f57f879b8..a07f97407f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -236,4 +236,8 @@ public Status getStatus() { public String getTargetNamespace() { return namespaceIdentifier; } + + public SharedIndexInformer getInformer() { + return informer; + } } From aefcbc6a613083569d7af9e9d437a076ea2b5c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 13:12:11 +0100 Subject: [PATCH 24/36] explicit scheduled executor service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/ExecutorServiceManager.java | 12 +++++++++--- .../informer/ManagedInformerEventSource.java | 2 +- test.sh | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 test.sh diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java index f0f5b39450..a66ed19abd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ExecutorServiceManager.java @@ -40,7 +40,8 @@ public class ExecutorServiceManager { private static final Logger log = LoggerFactory.getLogger(ExecutorServiceManager.class); private ExecutorService executor; private ExecutorService workflowExecutor; - private ScheduledExecutorService cachingExecutorService; + private ExecutorService cachingExecutorService; + private ScheduledExecutorService scheduledExecutorService; private boolean started; private ConfigurationService configurationService; @@ -123,14 +124,19 @@ private synchronized void lazyInitWorkflowExecutorService() { } } - public ScheduledExecutorService cachingExecutorService() { + public ExecutorService cachingExecutorService() { return cachingExecutorService; } + public ScheduledExecutorService scheduledExecutorService() { + return scheduledExecutorService; + } + public void start(ConfigurationService configurationService) { if (!started) { this.configurationService = configurationService; // used to lazy init workflow executor - this.cachingExecutorService = Executors.newScheduledThreadPool(0); + this.cachingExecutorService = Executors.newCachedThreadPool(); + this.scheduledExecutorService = Executors.newScheduledThreadPool(0); this.executor = new InstrumentedExecutorService(configurationService.getExecutorService()); started = true; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index c6f07a0188..f49ebb8485 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -160,7 +160,7 @@ public synchronized void start() { controllerConfiguration .getConfigurationService() .getExecutorServiceManager() - .cachingExecutorService(), + .scheduledExecutorService(), this); this.cache = new InformerManager<>(client, configuration, this); cache.setControllerConfiguration(controllerConfiguration); diff --git a/test.sh b/test.sh new file mode 100644 index 0000000000..22e0c25669 --- /dev/null +++ b/test.sh @@ -0,0 +1,17 @@ +# +# Copyright Java Operator SDK Authors +# +# 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. +# + +mci -Dlicense.skip jib:dockerBuild && kind load docker-image leader-election-operator:latest \ No newline at end of file From 46cd7eeb98de6c895e8257903c8c14f0f48863c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 14:05:21 +0100 Subject: [PATCH 25/36] cleanup configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/InformerConfiguration.java | 19 +++++++++-- .../InformerEventSourceConfiguration.java | 34 +++---------------- .../controller/ControllerEventSource.java | 6 +--- .../source/informer/InformerEventSource.java | 19 ++--------- .../informer/ManagedInformerEventSource.java | 6 ++-- 5 files changed, 28 insertions(+), 56 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index e7bf04d4c2..db9c34f155 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -54,7 +54,7 @@ public class InformerConfiguration { private ItemStore itemStore; private Long informerListLimit; private FieldSelector fieldSelector; - private boolean comparableResourceVersions; + private Boolean comparableResourceVersions; private Duration obsoleteResourceCacheCheckInterval; protected InformerConfiguration( @@ -70,7 +70,7 @@ protected InformerConfiguration( ItemStore itemStore, Long informerListLimit, FieldSelector fieldSelector, - boolean comparableResourceVersions, + Boolean comparableResourceVersions, Duration obsoleteResourceCacheCheckInterval) { this(resourceClass); this.name = name; @@ -319,6 +319,13 @@ public InformerConfiguration buildForController() { } // to avoid potential NPE followControllerNamespaceChanges = false; + if (comparableResourceVersions == null) { + comparableResourceVersions = DEFAULT_COMPARABLE_RESOURCE_VERSION; + } + + if (obsoleteResourceCacheCheckInterval == null) { + obsoleteResourceCacheCheckInterval = DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; + } return InformerConfiguration.this; } @@ -330,6 +337,14 @@ public InformerConfiguration build() { if (followControllerNamespaceChanges == null) { followControllerNamespaceChanges = DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES; } + if (comparableResourceVersions == null) { + comparableResourceVersions = DEFAULT_COMPARABLE_RESOURCE_VERSION; + } + + if (obsoleteResourceCacheCheckInterval == null) { + obsoleteResourceCacheCheckInterval = DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; + } + return InformerConfiguration.this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index 53db0a39e2..666c4d3613 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -34,8 +34,6 @@ import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter; import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; import static io.javaoperatorsdk.operator.api.reconciler.Constants.SAME_AS_CONTROLLER_NAMESPACES_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE_SET; @@ -92,10 +90,6 @@ default Optional getKubernetesClient() { return Optional.empty(); } - boolean comparableResourceVersion(); - - Duration getObsoleteResourceCacheCheckInterval(); - class DefaultInformerEventSourceConfiguration implements InformerEventSourceConfiguration { private final PrimaryToSecondaryMapper primaryToSecondaryMapper; @@ -103,24 +97,18 @@ class DefaultInformerEventSourceConfiguration private final GroupVersionKind groupVersionKind; private final InformerConfiguration informerConfig; private final KubernetesClient kubernetesClient; - private final boolean comparableResourceVersion; - private final Duration obsoleteResourceCacheCheckInterval; protected DefaultInformerEventSourceConfiguration( GroupVersionKind groupVersionKind, PrimaryToSecondaryMapper primaryToSecondaryMapper, SecondaryToPrimaryMapper secondaryToPrimaryMapper, InformerConfiguration informerConfig, - KubernetesClient kubernetesClient, - boolean comparableResourceVersion, - Duration obsoleteResourceCacheCheckInterval) { + KubernetesClient kubernetesClient) { this.informerConfig = Objects.requireNonNull(informerConfig); this.groupVersionKind = groupVersionKind; this.primaryToSecondaryMapper = primaryToSecondaryMapper; this.secondaryToPrimaryMapper = secondaryToPrimaryMapper; this.kubernetesClient = kubernetesClient; - this.comparableResourceVersion = comparableResourceVersion; - this.obsoleteResourceCacheCheckInterval = obsoleteResourceCacheCheckInterval; } @Override @@ -148,16 +136,6 @@ public Optional getGroupVersionKind() { public Optional getKubernetesClient() { return Optional.ofNullable(kubernetesClient); } - - @Override - public boolean comparableResourceVersion() { - return this.comparableResourceVersion; - } - - @Override - public Duration getObsoleteResourceCacheCheckInterval() { - return obsoleteResourceCacheCheckInterval; - } } @SuppressWarnings({"unused", "UnusedReturnValue"}) @@ -171,8 +149,6 @@ class Builder { private PrimaryToSecondaryMapper primaryToSecondaryMapper; private SecondaryToPrimaryMapper secondaryToPrimaryMapper; private KubernetesClient kubernetesClient; - private boolean comparableResourceVersion = DEFAULT_COMPARABLE_RESOURCE_VERSION; - private Duration obsoleteResourceCacheCheckInterval = DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; private Builder(Class resourceClass, Class primaryResourceClass) { this(resourceClass, primaryResourceClass, null); @@ -311,13 +287,13 @@ public Builder withFieldSelector(FieldSelector fieldSelector) { } public Builder withComparableResourceVersion(boolean comparableResourceVersion) { - this.comparableResourceVersion = comparableResourceVersion; + config.withComparableResourceVersions(comparableResourceVersion); return this; } public Builder withObsoleteResourceCacheCheckInterval( Duration obsoleteResourceCacheCheckInterval) { - this.obsoleteResourceCacheCheckInterval = obsoleteResourceCacheCheckInterval; + config.withObsoleteResourceCacheCheckInterval(obsoleteResourceCacheCheckInterval); return this; } @@ -360,9 +336,7 @@ public InformerEventSourceConfiguration build() { HasMetadata.getKind(primaryResourceClass), false)), config.build(), - kubernetesClient, - comparableResourceVersion, - obsoleteResourceCacheCheckInterval); + kubernetesClient); } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java index e0682d5808..07d59e039a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java @@ -48,11 +48,7 @@ public class ControllerEventSource @SuppressWarnings({"unchecked", "rawtypes"}) public ControllerEventSource(Controller controller) { - super( - NAME, - controller.getCRClient(), - controller.getConfiguration(), - controller.getConfiguration().getInformerConfig().isComparableResourceVersions()); + super(NAME, controller.getCRClient(), controller.getConfiguration()); this.controller = controller; final var config = controller.getConfiguration(); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index fcec8ae68b..70d83e640e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -35,8 +35,6 @@ import io.javaoperatorsdk.operator.processing.event.source.ResourceAction; import io.javaoperatorsdk.operator.processing.event.source.informer.TemporaryResourceCache.EventHandling; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; - /** * Wraps informer(s) so they are connected to the eventing system of the framework. Note that since * this is built on top of Fabric8 client Informers, it also supports caching resources using @@ -58,29 +56,18 @@ public class InformerEventSource public InformerEventSource( InformerEventSourceConfiguration configuration, EventSourceContext

context) { - this( - configuration, - configuration.getKubernetesClient().orElse(context.getClient()), - configuration.comparableResourceVersion()); - } - - InformerEventSource(InformerEventSourceConfiguration configuration, KubernetesClient client) { - this(configuration, client, DEFAULT_COMPARABLE_RESOURCE_VERSION); + this(configuration, configuration.getKubernetesClient().orElse(context.getClient())); } @SuppressWarnings({"unchecked", "rawtypes"}) - private InformerEventSource( - InformerEventSourceConfiguration configuration, - KubernetesClient client, - boolean comparableResourceVersions) { + InformerEventSource(InformerEventSourceConfiguration configuration, KubernetesClient client) { super( configuration.name(), configuration .getGroupVersionKind() .map(gvk -> client.genericKubernetesResources(gvk.apiVersion(), gvk.getKind())) .orElseGet(() -> (MixedOperation) client.resources(configuration.getResourceClass())), - configuration, - comparableResourceVersions); + configuration); // If there is a primary to secondary mapper there is no need for primary to secondary index. primaryToSecondaryMapper = configuration.getPrimaryToSecondaryMapper(); if (useSecondaryToPrimaryIndex()) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index f49ebb8485..a181d004cb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -67,10 +67,10 @@ public abstract class ManagedInformerEventSource< protected TemporaryResourceCache temporaryResourceCache; protected MixedOperation client; - protected ManagedInformerEventSource( - String name, MixedOperation client, C configuration, boolean comparableResourceVersions) { + protected ManagedInformerEventSource(String name, MixedOperation client, C configuration) { super(configuration.getResourceClass(), name); - this.comparableResourceVersions = comparableResourceVersions; + this.comparableResourceVersions = + configuration.getInformerConfig().isComparableResourceVersions(); this.client = client; this.configuration = configuration; } From 3069759e58e77c987e37f42f24bda30a750f600f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 14:11:12 +0100 Subject: [PATCH 26/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- test.sh | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 test.sh diff --git a/test.sh b/test.sh deleted file mode 100644 index 22e0c25669..0000000000 --- a/test.sh +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright Java Operator SDK Authors -# -# 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. -# - -mci -Dlicense.skip jib:dockerBuild && kind load docker-image leader-election-operator:latest \ No newline at end of file From 1415048cda62ce9f45c5989be4b9705099f6981a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 14:28:44 +0100 Subject: [PATCH 27/36] naming: ghost resources instead obsolete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/informer/Informer.java | 8 ++--- .../informer/InformerConfiguration.java | 30 +++++++++---------- .../InformerEventSourceConfiguration.java | 6 ++-- .../operator/api/reconciler/Constants.java | 6 ++-- .../informer/ManagedInformerEventSource.java | 2 +- .../informer/TemporaryResourceCache.java | 20 ++++++------- .../controller/ControllerEventSourceTest.java | 7 ++--- .../informer/InformerEventSourceTest.java | 4 +-- .../TemporaryPrimaryResourceCacheTest.java | 26 ++++++++-------- 9 files changed, 51 insertions(+), 58 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index cd7c88b1e1..1679ffd899 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -31,7 +31,7 @@ import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION; import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES; -import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL_MILLIS; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_LONG_VALUE_SET; import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET; @@ -144,10 +144,10 @@ /** * For read-cache-after-write consistency there are some corner cases where we need to check the - * caches see {@link TemporaryResourceCache#checkObsoleteResources()} periodically. This is the - * period in milliseconds. Applicable only if {@link #comparableResourceVersions()}} is true. + * caches see {@link TemporaryResourceCache} periodically. This is the period in milliseconds. + * Applicable only if {@link #comparableResourceVersions()}} is true. * * @since 5.3.0 */ - long obsoleteResourceCacheCheckInterval() default DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS; + long ghostResourceCacheCheckInterval() default DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL_MILLIS; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java index db9c34f155..a3637b6929 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java @@ -55,7 +55,7 @@ public class InformerConfiguration { private Long informerListLimit; private FieldSelector fieldSelector; private Boolean comparableResourceVersions; - private Duration obsoleteResourceCacheCheckInterval; + private Duration ghostResourceCacheCheckInterval; protected InformerConfiguration( Class resourceClass, @@ -71,7 +71,7 @@ protected InformerConfiguration( Long informerListLimit, FieldSelector fieldSelector, Boolean comparableResourceVersions, - Duration obsoleteResourceCacheCheckInterval) { + Duration ghostResourceCacheCheckInterval) { this(resourceClass); this.name = name; this.namespaces = namespaces; @@ -85,7 +85,7 @@ protected InformerConfiguration( this.informerListLimit = informerListLimit; this.fieldSelector = fieldSelector; this.comparableResourceVersions = comparableResourceVersions; - this.obsoleteResourceCacheCheckInterval = obsoleteResourceCacheCheckInterval; + this.ghostResourceCacheCheckInterval = ghostResourceCacheCheckInterval; } private InformerConfiguration(Class resourceClass) { @@ -122,7 +122,7 @@ public static InformerConfiguration.Builder builder( original.informerListLimit, original.fieldSelector, original.comparableResourceVersions, - original.obsoleteResourceCacheCheckInterval) + original.ghostResourceCacheCheckInterval) .builder; } @@ -301,8 +301,8 @@ public boolean isComparableResourceVersions() { return comparableResourceVersions; } - public Duration getObsoleteResourceCacheCheckInterval() { - return obsoleteResourceCacheCheckInterval; + public Duration getGhostResourceCacheCheckInterval() { + return ghostResourceCacheCheckInterval; } @SuppressWarnings("UnusedReturnValue") @@ -323,8 +323,8 @@ public InformerConfiguration buildForController() { comparableResourceVersions = DEFAULT_COMPARABLE_RESOURCE_VERSION; } - if (obsoleteResourceCacheCheckInterval == null) { - obsoleteResourceCacheCheckInterval = DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; + if (ghostResourceCacheCheckInterval == null) { + ghostResourceCacheCheckInterval = DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL; } return InformerConfiguration.this; } @@ -341,8 +341,8 @@ public InformerConfiguration build() { comparableResourceVersions = DEFAULT_COMPARABLE_RESOURCE_VERSION; } - if (obsoleteResourceCacheCheckInterval == null) { - obsoleteResourceCacheCheckInterval = DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL; + if (ghostResourceCacheCheckInterval == null) { + ghostResourceCacheCheckInterval = DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL; } return InformerConfiguration.this; @@ -392,8 +392,8 @@ public InformerConfiguration.Builder initFromAnnotation( .map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated())) .toList())); withComparableResourceVersions(informerConfig.comparableResourceVersions()); - withObsoleteResourceCacheCheckInterval( - Duration.ofMillis(informerConfig.obsoleteResourceCacheCheckInterval())); + withGhostResourceCacheCheckInterval( + Duration.ofMillis(informerConfig.ghostResourceCacheCheckInterval())); } return this; } @@ -500,10 +500,8 @@ public Builder withComparableResourceVersions(boolean comparableResourceVersions return this; } - public Builder withObsoleteResourceCacheCheckInterval( - Duration obsoleteResourceCacheCheckInterval) { - InformerConfiguration.this.obsoleteResourceCacheCheckInterval = - obsoleteResourceCacheCheckInterval; + public Builder withGhostResourceCacheCheckInterval(Duration ghostResourceCacheCheckInterval) { + InformerConfiguration.this.ghostResourceCacheCheckInterval = ghostResourceCacheCheckInterval; return this; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java index 666c4d3613..31851044d9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java @@ -291,9 +291,9 @@ public Builder withComparableResourceVersion(boolean comparableResourceVersio return this; } - public Builder withObsoleteResourceCacheCheckInterval( - Duration obsoleteResourceCacheCheckInterval) { - config.withObsoleteResourceCacheCheckInterval(obsoleteResourceCacheCheckInterval); + public Builder withGhostResourceCacheCheckInterval( + Duration ghostResourceCacheCheckInterval) { + config.withGhostResourceCacheCheckInterval(ghostResourceCacheCheckInterval); return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java index 0cefebd78f..5d5f7a70cb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java @@ -44,9 +44,9 @@ public final class Constants { public static final boolean DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES = true; public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSION = true; - public static final long DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS = 3 * 60 * 1000; - public static final Duration DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL = - Duration.ofMillis(DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL_MILLIS); + public static final long DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL_MILLIS = 3 * 60 * 1000; + public static final Duration DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL = + Duration.ofMillis(DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL_MILLIS); private Constants() {} } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index a181d004cb..978deda333 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -156,7 +156,7 @@ public synchronized void start() { temporaryResourceCache = new TemporaryResourceCache<>( comparableResourceVersions, - configuration.getInformerConfig().getObsoleteResourceCacheCheckInterval().toMillis(), + configuration.getInformerConfig().getGhostResourceCacheCheckInterval().toMillis(), controllerConfiguration .getConfigurationService() .getExecutorServiceManager() diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index c5802c3cce..5a5abed9c5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -61,7 +61,6 @@ public class TemporaryResourceCache { private final Map activeUpdates = new HashMap<>(); private final boolean comparableResourceVersions; - private final long obsoleteResourceCheckInterval; private final ManagedInformerEventSource managedInformerEventSource; public enum EventHandling { @@ -72,17 +71,16 @@ public enum EventHandling { public TemporaryResourceCache( boolean comparableResourceVersions, - long obsoleteResourceCheckInterval, - ScheduledExecutorService obsoleteCheckExecutor, + long ghostResourceCheckInterval, + ScheduledExecutorService ghostCheckExecutor, ManagedInformerEventSource managedInformerEventSource) { this.comparableResourceVersions = comparableResourceVersions; - this.obsoleteResourceCheckInterval = obsoleteResourceCheckInterval; this.managedInformerEventSource = managedInformerEventSource; if (comparableResourceVersions) { - obsoleteCheckExecutor.scheduleWithFixedDelay( - this::checkObsoleteResources, - obsoleteResourceCheckInterval, - obsoleteResourceCheckInterval, + ghostCheckExecutor.scheduleWithFixedDelay( + this::checkGhostResources, + ghostResourceCheckInterval, + ghostResourceCheckInterval, TimeUnit.MILLISECONDS); } } @@ -232,8 +230,8 @@ private String getLatestResourceVersion(String namespace) { * In this case neither the ADD nor DELETE event will be propagated to the informer, but we * explicitly add resources to this cache. Those are cleaned up by this check. */ - private void checkObsoleteResources() { - log.debug("Checking for obsolete resources."); + private void checkGhostResources() { + log.debug("Checking for ghost resources."); var iterator = cache.entrySet().iterator(); while (iterator.hasNext()) { var e = iterator.next(); @@ -248,7 +246,7 @@ private void checkObsoleteResources() { .isEmpty()) { iterator.remove(); managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true); - log.debug("Removing obsolete resource with ID: {}", e.getKey()); + log.debug("Removing ghost resource with ID: {}", e.getKey()); } } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java index 15ad5d02f0..820768d6a2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSourceTest.java @@ -64,8 +64,8 @@ public void setup() { when(controllerConfig.getConfigurationService()).thenReturn(new BaseConfigurationService()); var ic = mock(InformerConfiguration.class); when(controllerConfig.getInformerConfig()).thenReturn(ic); - when(ic.getObsoleteResourceCacheCheckInterval()) - .thenReturn(Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL); + when(ic.getGhostResourceCacheCheckInterval()) + .thenReturn(Constants.DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL); setUpSource(new ControllerEventSource<>(testController), true, controllerConfig); } @@ -329,8 +329,7 @@ public TestConfiguration( .withOnUpdateFilter(onUpdateFilter) .withGenericFilter(genericFilter) .withComparableResourceVersions(true) - .withObsoleteResourceCacheCheckInterval( - Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL) + .withGhostResourceCacheCheckInterval(Constants.DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL) .buildForController(), false); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index ca158c416d..1984d67fd8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -88,8 +88,8 @@ void setup() { when(informerEventSourceConfiguration.getInformerConfig()).thenReturn(informerConfig); when(informerConfig.getEffectiveNamespaces(any())).thenReturn(DEFAULT_NAMESPACES_SET); when(informerEventSourceConfiguration.getResourceClass()).thenReturn(Deployment.class); - when(informerConfig.getObsoleteResourceCacheCheckInterval()) - .thenReturn(Constants.DEFAULT_OBSOLETE_RESOURCE_CHECK_INTERVAL); + when(informerConfig.getGhostResourceCacheCheckInterval()) + .thenReturn(Constants.DEFAULT_GHOST_RESOURCE_CHECK_INTERVAL); informerEventSource = spy( new InformerEventSource<>(informerEventSourceConfiguration, clientMock) { diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 19b913bf04..5f18cbc44d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -45,7 +45,7 @@ class TemporaryPrimaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; - public static final int OBSOLETE_RESOURCE_CHECK_INTERVAL = 100; + public static final int GHOST_RESOURCE_CHECK_INTERVAL = 100; private TemporaryResourceCache temporaryResourceCache; private volatile String latestSyncVersion; @@ -228,7 +228,6 @@ void putBeforeEvent() { nextResource.getMetadata().setResourceVersion("3"); temporaryResourceCache.putResource(nextResource); - // the result is obsolete result = temporaryResourceCache.onAddOrUpdateEvent(ResourceAction.UPDATED, nextResource, null); assertThat(result).isEqualTo(EventHandling.OBSOLETE); } @@ -251,7 +250,6 @@ void putBeforeEventWithEventFiltering() { temporaryResourceCache.putResource(nextResource); temporaryResourceCache.doneEventFilterModify(resourceId, "3"); - // the result is obsolete latestSyncVersion = "3"; result = temporaryResourceCache.onAddOrUpdateEvent(ResourceAction.UPDATED, nextResource, null); assertThat(result).isEqualTo(EventHandling.OBSOLETE); @@ -323,14 +321,14 @@ void rapidDeletion() { } @Test - void removalOfObsoleteResources() { - withTemporaryResourceCacheForObsoleteHandling(); + void removalOfGhostResources() { + withTemporaryResourceCacheForGhostHandling(); var tr = testResource(); this.temporaryResourceCache.putResource(tr); await() - .pollDelay(Duration.ofMillis(2 * OBSOLETE_RESOURCE_CHECK_INTERVAL)) + .pollDelay(Duration.ofMillis(2 * GHOST_RESOURCE_CHECK_INTERVAL)) .untilAsserted( () -> assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))) @@ -348,12 +346,12 @@ void removalOfObsoleteResources() { } @Test - void checksObsoleteOnlyWithCertainDelay() { - withTemporaryResourceCacheForObsoleteHandling(); + void checksGhostOnlyWithCertainDelay() { + withTemporaryResourceCacheForGhostHandling(); this.temporaryResourceCache.putResource(testResource()); latestSyncVersion = "3"; await() - .pollDelay(Duration.ofMillis(OBSOLETE_RESOURCE_CHECK_INTERVAL / 5)) + .pollDelay(Duration.ofMillis(GHOST_RESOURCE_CHECK_INTERVAL / 5)) .untilAsserted( () -> assertThat( @@ -371,13 +369,13 @@ void checksObsoleteOnlyWithCertainDelay() { } @Test - void obsoleteResourceIsNotRemovedIfLatestSyncVersionIsOlder() { - withTemporaryResourceCacheForObsoleteHandling(); + void ghostResourceIsNotRemovedIfLatestSyncVersionIsOlder() { + withTemporaryResourceCacheForGhostHandling(); this.temporaryResourceCache.putResource(testResource()); latestSyncVersion = "1"; await() - .pollDelay(Duration.ofMillis(OBSOLETE_RESOURCE_CHECK_INTERVAL * 2)) + .pollDelay(Duration.ofMillis(GHOST_RESOURCE_CHECK_INTERVAL * 2)) .untilAsserted( () -> assertThat( @@ -386,11 +384,11 @@ void obsoleteResourceIsNotRemovedIfLatestSyncVersionIsOlder() { .isPresent()); } - private void withTemporaryResourceCacheForObsoleteHandling() { + private void withTemporaryResourceCacheForGhostHandling() { this.temporaryResourceCache = new TemporaryResourceCache<>( true, - OBSOLETE_RESOURCE_CHECK_INTERVAL, + GHOST_RESOURCE_CHECK_INTERVAL, Executors.newScheduledThreadPool(1), managedInformerEventSource); } From 67ecebdf2e2c43be245c9c9291b9913306d6135f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 14:33:19 +0100 Subject: [PATCH 28/36] cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/informer/Informer.java | 2 +- .../processing/event/source/informer/InformerManager.java | 5 +---- .../processing/event/source/informer/InformerWrapper.java | 4 ---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java index 1679ffd899..c8da5c0c7c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java @@ -145,7 +145,7 @@ /** * For read-cache-after-write consistency there are some corner cases where we need to check the * caches see {@link TemporaryResourceCache} periodically. This is the period in milliseconds. - * Applicable only if {@link #comparableResourceVersions()}} is true. + * Applicable only if {@link #comparableResourceVersions()} is true. * * @since 5.3.0 */ diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 711f7f6f92..0512f514e0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -236,10 +236,7 @@ private Optional> getSource(String namespace) { } String lastSyncResourceVersion(String namespace) { - return getSource(namespace) - .map(InformerWrapper::getInformer) - .orElseThrow() - .lastSyncResourceVersion(); + return getSource(namespace).orElseThrow().getLastSyncResourceVersion(); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java index a07f97407f..2f57f879b8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java @@ -236,8 +236,4 @@ public Status getStatus() { public String getTargetNamespace() { return namespaceIdentifier; } - - public SharedIndexInformer getInformer() { - return informer; - } } From ec967133b3ca04a049d1cc556da7f6d020b7f469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 14:40:55 +0100 Subject: [PATCH 29/36] test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../source/informer/TemporaryPrimaryResourceCacheTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index 5f18cbc44d..dcc873b3a5 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -274,7 +274,7 @@ void putAfterEventWithEventFilteringNoPost() { ResourceAction.UPDATED, nextResource, testResource); // the result is deferred assertThat(result).isEqualTo(EventHandling.DEFER); - temporaryResourceCache.putResource(testResource); + temporaryResourceCache.putResource(nextResource); var postEvent = temporaryResourceCache.doneEventFilterModify(resourceId, "3"); // there is no post event because the done call claimed responsibility for rv 3 From 6060af0d94fa5c5017938ebd248c26b8b2236273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 14:44:06 +0100 Subject: [PATCH 30/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../source/informer/TemporaryResourceCache.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 5a5abed9c5..bf17b30127 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -193,7 +193,7 @@ public synchronized void putResource(T newResource) { // // this also prevents resurrecting recently deleted entities for which the delete event // has already been processed - var latestRV = getLatestResourceVersion(newResource.getMetadata().getNamespace()); + var latestRV = getLastSyncResourceVersion(newResource.getMetadata().getNamespace()); if (latestRV != null && ReconcilerUtilsInternal.compareResourceVersions( latestRV, newResource.getMetadata().getResourceVersion()) @@ -219,7 +219,7 @@ public synchronized void putResource(T newResource) { } } - private String getLatestResourceVersion(String namespace) { + private String getLastSyncResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } @@ -235,10 +235,12 @@ private void checkGhostResources() { var iterator = cache.entrySet().iterator(); while (iterator.hasNext()) { var e = iterator.next(); - if (ReconcilerUtilsInternal.compareResourceVersions( - e.getValue().getMetadata().getResourceVersion(), - getLatestResourceVersion(e.getValue().getMetadata().getNamespace())) - < 0 + var latestResourceVersion = + getLastSyncResourceVersion(e.getValue().getMetadata().getNamespace()); + if ((latestResourceVersion == null + || ReconcilerUtilsInternal.compareResourceVersions( + e.getValue().getMetadata().getResourceVersion(), latestResourceVersion) + < 0) // making sure we have the situation where resource is missing from the cache && managedInformerEventSource .manager() From dd0e326a004ac9bba833108189fbf697c8403150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 15:14:38 +0100 Subject: [PATCH 31/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/source/informer/TemporaryResourceCache.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index bf17b30127..74e618c07f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -235,12 +235,10 @@ private void checkGhostResources() { var iterator = cache.entrySet().iterator(); while (iterator.hasNext()) { var e = iterator.next(); - var latestResourceVersion = - getLastSyncResourceVersion(e.getValue().getMetadata().getNamespace()); - if ((latestResourceVersion == null - || ReconcilerUtilsInternal.compareResourceVersions( - e.getValue().getMetadata().getResourceVersion(), latestResourceVersion) - < 0) + if ((ReconcilerUtilsInternal.compareResourceVersions( + e.getValue().getMetadata().getResourceVersion(), + getLastSyncResourceVersion(e.getValue().getMetadata().getNamespace())) + < 0) // making sure we have the situation where resource is missing from the cache && managedInformerEventSource .manager() From 3f22f3ed864e5f48e588263ffcbf067a1e51cc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 18:04:07 +0100 Subject: [PATCH 32/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../source/informer/InformerManager.java | 12 +++++++++ .../informer/TemporaryResourceCache.java | 25 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java index 0512f514e0..6632ce631e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java @@ -230,6 +230,18 @@ private boolean isWatchingAllNamespaces() { return sources.containsKey(WATCH_ALL_NAMESPACES); } + public boolean isWatchingNamespace(String namespace) { + // for cluster scoped resources we can assume + // that we watch the whole cluster + if (namespace == null) { + return true; + } + if (isWatchingAllNamespaces()) { + return true; + } + return sources.containsKey(namespace); + } + private Optional> getSource(String namespace) { namespace = isWatchingAllNamespaces() || namespace == null ? WATCH_ALL_NAMESPACES : namespace; return Optional.ofNullable(sources.get(namespace)); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 74e618c07f..eba2c926a7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -188,6 +188,14 @@ public synchronized void putResource(T newResource) { return; } + // todo unit test + // this can happen when we dynamically change the NS + if (!managedInformerEventSource + .manager() + .isWatchingNamespace(newResource.getMetadata().getNamespace())) { + return; + } + // check against the latestResourceVersion processed by the TemporaryResourceCache // If the resource is older, then we can safely ignore. // @@ -223,6 +231,7 @@ private String getLastSyncResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } + // todo tests with combination of event processing /** * There are (probably extremely rare) circumstances, when we can miss a delete event related to a * resources: when we create a resource that is deleted right after by third party and the related @@ -235,9 +244,21 @@ private void checkGhostResources() { var iterator = cache.entrySet().iterator(); while (iterator.hasNext()) { var e = iterator.next(); + + var ns = e.getValue().getMetadata().getNamespace(); + // todo unit tests + // this can happen if followed namespaces are changed dynamically + if (!managedInformerEventSource.manager().isWatchingNamespace(ns)) { + log.debug( + "Removing resource: {} from cache as part of ghost cleanup. Namespace is not followed" + + " anymore: {}", + e.getKey(), + ns); + iterator.remove(); + continue; + } if ((ReconcilerUtilsInternal.compareResourceVersions( - e.getValue().getMetadata().getResourceVersion(), - getLastSyncResourceVersion(e.getValue().getMetadata().getNamespace())) + e.getValue().getMetadata().getResourceVersion(), getLastSyncResourceVersion(ns)) < 0) // making sure we have the situation where resource is missing from the cache && managedInformerEventSource From 3586c2d73f8faed4e00874438682aa5d423adc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 10 Mar 2026 18:33:02 +0100 Subject: [PATCH 33/36] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../event/source/informer/TemporaryResourceCache.java | 9 +++++---- .../informer/TemporaryPrimaryResourceCacheTest.java | 1 + .../ConfigMapDependentResource.java | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index eba2c926a7..5fde082ec1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -188,11 +188,12 @@ public synchronized void putResource(T newResource) { return; } + var ns = newResource.getMetadata().getNamespace(); // todo unit test // this can happen when we dynamically change the NS - if (!managedInformerEventSource - .manager() - .isWatchingNamespace(newResource.getMetadata().getNamespace())) { + if (!managedInformerEventSource.manager().isWatchingNamespace(ns)) { + log.debug( + "Skipping caching of resource: {} since namespace is now watched: {}", resourceId, ns); return; } @@ -201,7 +202,7 @@ public synchronized void putResource(T newResource) { // // this also prevents resurrecting recently deleted entities for which the delete event // has already been processed - var latestRV = getLastSyncResourceVersion(newResource.getMetadata().getNamespace()); + var latestRV = getLastSyncResourceVersion(ns); if (latestRV != null && ReconcilerUtilsInternal.compareResourceVersions( latestRV, newResource.getMetadata().getResourceVersion()) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java index dcc873b3a5..28936d1a6c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java @@ -58,6 +58,7 @@ void setup() { managedInformerEventSource = mock(ManagedInformerEventSource.class); var mim = mock(InformerManager.class); when(managedInformerEventSource.manager()).thenReturn(mim); + when(mim.isWatchingNamespace(any())).thenReturn(true); when(mim.lastSyncResourceVersion(any())).then(a -> latestSyncVersion); temporaryResourceCache = new TemporaryResourceCache<>( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java index 1feb5b5ecd..4f3921d509 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/dependentdifferentnamespace/ConfigMapDependentResource.java @@ -19,9 +19,13 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.javaoperatorsdk.operator.api.config.informer.Informer; +import io.javaoperatorsdk.operator.api.reconciler.Constants; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +@KubernetesDependent(informer = @Informer(namespaces = Constants.WATCH_ALL_NAMESPACES)) public class ConfigMapDependentResource extends CRUDNoGCKubernetesDependentResource< ConfigMap, DependentDifferentNamespaceCustomResource> { From b507acb60479ab65532b0ed2cb7592c014f213f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 11 Mar 2026 07:53:13 +0100 Subject: [PATCH 34/36] test class naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- ...ryResourceCacheTest.java => TemporaryResourceCacheTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/{TemporaryPrimaryResourceCacheTest.java => TemporaryResourceCacheTest.java} (99%) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java rename to operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java index 28936d1a6c..cfcef77ed2 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryPrimaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java @@ -42,7 +42,7 @@ import static org.mockito.Mockito.when; @SuppressWarnings({"unchecked", "rawtypes"}) -class TemporaryPrimaryResourceCacheTest { +class TemporaryResourceCacheTest { public static final String RESOURCE_VERSION = "2"; public static final int GHOST_RESOURCE_CHECK_INTERVAL = 100; From 0685672e5a24de5ae7153f893da0cd90dad1f305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 11 Mar 2026 08:07:22 +0100 Subject: [PATCH 35/36] additional unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/TemporaryResourceCache.java | 4 +-- .../informer/TemporaryResourceCacheTest.java | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index 5fde082ec1..c3bbeb9d53 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -189,8 +189,7 @@ public synchronized void putResource(T newResource) { } var ns = newResource.getMetadata().getNamespace(); - // todo unit test - // this can happen when we dynamically change the NS + // this can happen when we dynamically change the followed namespace list if (!managedInformerEventSource.manager().isWatchingNamespace(ns)) { log.debug( "Skipping caching of resource: {} since namespace is now watched: {}", resourceId, ns); @@ -247,7 +246,6 @@ private void checkGhostResources() { var e = iterator.next(); var ns = e.getValue().getMetadata().getNamespace(); - // todo unit tests // this can happen if followed namespaces are changed dynamically if (!managedInformerEventSource.manager().isWatchingNamespace(ns)) { log.debug( diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java index cfcef77ed2..f50063ccde 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCacheTest.java @@ -385,6 +385,42 @@ void ghostResourceIsNotRemovedIfLatestSyncVersionIsOlder() { .isPresent()); } + @Test + void ghostRemovalRemovesResourcesOnNotFollowedNamespaces() { + withTemporaryResourceCacheForGhostHandling(); + + var tr = testResource(); + temporaryResourceCache.putResource(tr); + + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))) + .isPresent(); + + // simulate namespace no longer being watched + var mim = managedInformerEventSource.manager(); + when(mim.isWatchingNamespace(tr.getMetadata().getNamespace())).thenReturn(false); + + await() + .untilAsserted( + () -> + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))) + .isEmpty()); + + // no delete event should be fired for resources removed due to namespace change + verify(managedInformerEventSource, times(0)) + .handleEvent(any(), any(), any(), any(Boolean.class)); + } + + @Test + void doNotCacheResourceOnPutIfNamespaceIsNotFollowedAnymore() { + var mim = managedInformerEventSource.manager(); + when(mim.isWatchingNamespace("default")).thenReturn(false); + + var tr = testResource(); + temporaryResourceCache.putResource(tr); + + assertThat(temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(tr))).isEmpty(); + } + private void withTemporaryResourceCacheForGhostHandling() { this.temporaryResourceCache = new TemporaryResourceCache<>( From f850a8300163febb3aed427add5763972f0e5e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 11 Mar 2026 08:23:18 +0100 Subject: [PATCH 36/36] ghost clear concurrency tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../informer/TemporaryResourceCache.java | 1 - .../informer/InformerEventSourceTest.java | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java index c3bbeb9d53..1044720d59 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/TemporaryResourceCache.java @@ -231,7 +231,6 @@ private String getLastSyncResourceVersion(String namespace) { return managedInformerEventSource.manager().lastSyncResourceVersion(namespace); } - // todo tests with combination of event processing /** * There are (probably extremely rare) circumstances, when we can miss a delete event related to a * resources: when we create a resource that is deleted right after by third party and the related diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 1984d67fd8..e60ac02280 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import org.junit.jupiter.api.BeforeEach; @@ -339,6 +340,116 @@ void multipleCachingFilteringUpdates_variant4() { assertNoEventProduced(); } + @Test + void ghostCheckRemovesCachedResourceDuringFilteringUpdate() { + var mes = mock(ManagedInformerEventSource.class); + var mim = mock(InformerManager.class); + when(mes.manager()).thenReturn(mim); + when(mim.isWatchingNamespace(any())).thenReturn(true); + when(mim.lastSyncResourceVersion(any())).thenReturn("1"); + when(mim.get(any())).thenReturn(Optional.empty()); + + var ghostCheckExecutor = Executors.newScheduledThreadPool(1); + temporaryResourceCache = spy(new TemporaryResourceCache<>(true, 50, ghostCheckExecutor, mes)); + informerEventSource.setTemporalResourceCache(temporaryResourceCache); + + // put resource in cache and start a filtering update + var deployment = deploymentWithResourceVersion(2); + temporaryResourceCache.putResource(deployment); + var resourceId = ResourceID.fromResource(deployment); + temporaryResourceCache.startEventFilteringModify(resourceId); + + // advance sync version so ghost check considers the cached resource outdated + when(mim.lastSyncResourceVersion(any())).thenReturn("3"); + + // ghost check should remove the cached resource + await() + .untilAsserted( + () -> assertThat(temporaryResourceCache.getResourceFromCache(resourceId)).isEmpty()); + + // complete the filtering update - the resource should not reappear + temporaryResourceCache.doneEventFilterModify(resourceId, "2"); + assertThat(temporaryResourceCache.getResourceFromCache(resourceId)).isEmpty(); + + ghostCheckExecutor.shutdownNow(); + } + + @Test + void ghostCheckRunsConcurrentlyWithPutResource() { + var mes = mock(ManagedInformerEventSource.class); + var mim = mock(InformerManager.class); + when(mes.manager()).thenReturn(mim); + when(mim.isWatchingNamespace(any())).thenReturn(true); + when(mim.lastSyncResourceVersion(any())).thenReturn("1"); + when(mim.get(any())).thenReturn(Optional.empty()); + + var ghostCheckExecutor = Executors.newScheduledThreadPool(1); + temporaryResourceCache = spy(new TemporaryResourceCache<>(true, 50, ghostCheckExecutor, mes)); + informerEventSource.setTemporalResourceCache(temporaryResourceCache); + + // put a resource that will become a ghost + var deployment = deploymentWithResourceVersion(2); + temporaryResourceCache.putResource(deployment); + + // advance sync version so ghost check removes it + when(mim.lastSyncResourceVersion(any())).thenReturn("3"); + + await() + .untilAsserted( + () -> + assertThat( + temporaryResourceCache.getResourceFromCache( + ResourceID.fromResource(deployment))) + .isEmpty()); + + // now put a newer resource - should succeed even after ghost removal + var newerDeployment = deploymentWithResourceVersion(4); + temporaryResourceCache.putResource(newerDeployment); + assertThat( + temporaryResourceCache.getResourceFromCache(ResourceID.fromResource(newerDeployment))) + .isPresent(); + + ghostCheckExecutor.shutdownNow(); + } + + @Test + void filteringUpdateAndGhostCheckWithNamespaceChange() { + var mes = mock(ManagedInformerEventSource.class); + var mim = mock(InformerManager.class); + when(mes.manager()).thenReturn(mim); + when(mim.isWatchingNamespace(any())).thenReturn(true); + when(mim.lastSyncResourceVersion(any())).thenReturn("1"); + when(mim.get(any())).thenReturn(Optional.empty()); + + var ghostCheckExecutor = Executors.newScheduledThreadPool(1); + temporaryResourceCache = spy(new TemporaryResourceCache<>(true, 50, ghostCheckExecutor, mes)); + informerEventSource.setTemporalResourceCache(temporaryResourceCache); + + // start filtering update and put resource + var deployment = deploymentWithResourceVersion(2); + var resourceId = ResourceID.fromResource(deployment); + temporaryResourceCache.startEventFilteringModify(resourceId); + temporaryResourceCache.putResource(deployment); + + // namespace becomes unwatched - ghost check should clean up + when(mim.isWatchingNamespace(any())).thenReturn(false); + + await() + .untilAsserted( + () -> assertThat(temporaryResourceCache.getResourceFromCache(resourceId)).isEmpty()); + + // complete the filtering update + var doneResult = temporaryResourceCache.doneEventFilterModify(resourceId, "2"); + // resource was already cleaned by ghost check, so no deferred event + assertThat(doneResult).isEmpty(); + + // put should be rejected since namespace is no longer watched + temporaryResourceCache.putResource(deploymentWithResourceVersion(3)); + assertThat(temporaryResourceCache.getResourceFromCache(resourceId)).isEmpty(); + + ghostCheckExecutor.shutdownNow(); + } + private void assertNoEventProduced() { await() .pollDelay(Duration.ofMillis(50))