cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public R get(K key) {
+ return cache.getIfPresent(key);
+ }
+
+ @Override
+ public R remove(K key) {
+ var value = cache.getIfPresent(key);
+ cache.invalidate(key);
+ return value;
+ }
+
+ @Override
+ public void put(K key, R object) {
+ cache.put(key, object);
+ }
+}
diff --git a/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java b/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java
new file mode 100644
index 0000000000..a58d58bd2a
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java
@@ -0,0 +1,55 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache;
+
+import java.time.Duration;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.client.KubernetesClient;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+/**
+ * A factory for Caffeine-backed
+ * {@link BoundedItemStore}. The implementation uses a {@link CaffeineBoundedCache} to store
+ * resources and progressively evict them if they haven't been used in a while. The idea about
+ * CaffeinBoundedItemStore-s is that, caffeine will cache the resources which were recently used,
+ * and will evict resource, which are not used for a while. This is ideal for startup performance
+ * and efficiency when all resources should be cached to avoid undue load on the API server. This is
+ * why setting a maximal cache size is not practical and the approach of evicting least recently
+ * used resources was chosen. However, depending on controller implementations and domains, it could
+ * happen that some / many of these resources are then seldom or even reconciled anymore. In that
+ * situation, large amounts of memory might be consumed to cache resources that are never used
+ * again.
+ *
+ * Note that if a resource is reconciled and is not present anymore in cache, it will transparently
+ * be fetched again from the API server. Similarly, since associated secondary resources are usually
+ * reconciled too, they might need to be fetched and populated to the cache, and will remain there
+ * for some time, for subsequent reconciliations.
+ */
+public class CaffeineBoundedItemStores {
+
+ private CaffeineBoundedItemStores() {}
+
+ /**
+ * @param client Kubernetes Client
+ * @param rClass resource class
+ * @param accessExpireDuration the duration after resources is evicted from cache if not accessed.
+ * @return the ItemStore implementation
+ * @param resource type
+ */
+ @SuppressWarnings("unused")
+ public static BoundedItemStore boundedItemStore(
+ KubernetesClient client, Class rClass,
+ Duration accessExpireDuration) {
+ Cache cache = Caffeine.newBuilder()
+ .expireAfterAccess(accessExpireDuration)
+ .build();
+ return boundedItemStore(client, rClass, cache);
+ }
+
+ public static BoundedItemStore boundedItemStore(
+ KubernetesClient client, Class rClass, Cache cache) {
+ return new BoundedItemStore<>(new CaffeineBoundedCache<>(cache), rClass, client);
+ }
+
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java
new file mode 100644
index 0000000000..21adf81cc0
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/BoundedCacheTestBase.java
@@ -0,0 +1,95 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache;
+
+import java.time.Duration;
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestSpec;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestStatus;
+
+import static io.javaoperatorsdk.operator.processing.event.source.cache.sample.AbstractTestReconciler.DATA_KEY;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+public abstract class BoundedCacheTestBase> {
+
+ private static final Logger log = LoggerFactory.getLogger(BoundedCacheTestBase.class);
+
+ public static final int NUMBER_OF_RESOURCE_TO_TEST = 3;
+ public static final String RESOURCE_NAME_PREFIX = "test-";
+ public static final String INITIAL_DATA_PREFIX = "data-";
+ public static final String UPDATED_PREFIX = "updatedPrefix";
+
+ @Test
+ void reconciliationWorksWithLimitedCache() {
+ createTestResources();
+
+ assertConfigMapData(INITIAL_DATA_PREFIX);
+
+ updateTestResources();
+
+ assertConfigMapData(UPDATED_PREFIX);
+
+ deleteTestResources();
+
+ assertConfigMapsDeleted();
+ }
+
+ private void assertConfigMapsDeleted() {
+ await().atMost(Duration.ofSeconds(30))
+ .untilAsserted(() -> IntStream.range(0, NUMBER_OF_RESOURCE_TO_TEST).forEach(i -> {
+ var cm = extension().get(ConfigMap.class, RESOURCE_NAME_PREFIX + i);
+ assertThat(cm).isNull();
+ }));
+ }
+
+ private void deleteTestResources() {
+ IntStream.range(0, NUMBER_OF_RESOURCE_TO_TEST).forEach(i -> {
+ var cm = extension().get(customResourceClass(), RESOURCE_NAME_PREFIX + i);
+ var deleted = extension().delete(cm);
+ if (!deleted) {
+ log.warn("Custom resource might not be deleted: {}", cm);
+ }
+ });
+ }
+
+ private void updateTestResources() {
+ IntStream.range(0, NUMBER_OF_RESOURCE_TO_TEST).forEach(i -> {
+ var cm = extension().get(ConfigMap.class, RESOURCE_NAME_PREFIX + i);
+ cm.getData().put(DATA_KEY, UPDATED_PREFIX + i);
+ extension().replace(cm);
+ });
+ }
+
+ void assertConfigMapData(String dataPrefix) {
+ await().untilAsserted(() -> IntStream.range(0, NUMBER_OF_RESOURCE_TO_TEST)
+ .forEach(i -> assertConfigMap(i, dataPrefix)));
+ }
+
+ private void assertConfigMap(int i, String prefix) {
+ var cm = extension().get(ConfigMap.class, RESOURCE_NAME_PREFIX + i);
+ assertThat(cm).isNotNull();
+ assertThat(cm.getData().get(DATA_KEY)).isEqualTo(prefix + i);
+ }
+
+ private void createTestResources() {
+ IntStream.range(0, NUMBER_OF_RESOURCE_TO_TEST).forEach(i -> {
+ extension().create(createTestResource(i));
+ });
+ }
+
+ abstract P createTestResource(int index);
+
+ abstract Class
customResourceClass();
+
+ abstract LocallyRunOperatorExtension extension();
+
+
+
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedCacheClusterScopeIT.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedCacheClusterScopeIT.java
new file mode 100644
index 0000000000..252b20f4a4
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedCacheClusterScopeIT.java
@@ -0,0 +1,52 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.clusterscope.BoundedCacheClusterScopeTestCustomResource;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.clusterscope.BoundedCacheClusterScopeTestReconciler;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestSpec;
+
+import static io.javaoperatorsdk.operator.processing.event.source.cache.sample.AbstractTestReconciler.boundedItemStore;
+
+public class CaffeineBoundedCacheClusterScopeIT
+ extends BoundedCacheTestBase {
+
+ @RegisterExtension
+ LocallyRunOperatorExtension extension =
+ LocallyRunOperatorExtension.builder()
+ .withReconciler(new BoundedCacheClusterScopeTestReconciler(), o -> {
+ o.withItemStore(boundedItemStore(
+ new KubernetesClientBuilder().build(),
+ BoundedCacheClusterScopeTestCustomResource.class,
+ Duration.ofMinutes(1),
+ 1));
+ })
+ .build();
+
+ @Override
+ BoundedCacheClusterScopeTestCustomResource createTestResource(int index) {
+ var res = new BoundedCacheClusterScopeTestCustomResource();
+ res.setMetadata(new ObjectMetaBuilder()
+ .withName(RESOURCE_NAME_PREFIX + index)
+ .build());
+ res.setSpec(new BoundedCacheTestSpec());
+ res.getSpec().setData(INITIAL_DATA_PREFIX + index);
+ res.getSpec().setTargetNamespace(extension.getNamespace());
+ return res;
+ }
+
+ @Override
+ Class customResourceClass() {
+ return BoundedCacheClusterScopeTestCustomResource.class;
+ }
+
+ @Override
+ LocallyRunOperatorExtension extension() {
+ return extension;
+ }
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedCacheNamespacedIT.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedCacheNamespacedIT.java
new file mode 100644
index 0000000000..ae7f8f5873
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedCacheNamespacedIT.java
@@ -0,0 +1,50 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestCustomResource;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestReconciler;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestSpec;
+
+import static io.javaoperatorsdk.operator.processing.event.source.cache.sample.AbstractTestReconciler.boundedItemStore;
+
+class CaffeineBoundedCacheNamespacedIT
+ extends BoundedCacheTestBase {
+
+ @RegisterExtension
+ LocallyRunOperatorExtension extension =
+ LocallyRunOperatorExtension.builder().withReconciler(new BoundedCacheTestReconciler(), o -> {
+ o.withItemStore(boundedItemStore(
+ new KubernetesClientBuilder().build(), BoundedCacheTestCustomResource.class,
+ Duration.ofMinutes(1),
+ 1));
+ })
+ .build();
+
+ BoundedCacheTestCustomResource createTestResource(int index) {
+ var res = new BoundedCacheTestCustomResource();
+ res.setMetadata(new ObjectMetaBuilder()
+ .withName(RESOURCE_NAME_PREFIX + index)
+ .build());
+ res.setSpec(new BoundedCacheTestSpec());
+ res.getSpec().setData(INITIAL_DATA_PREFIX + index);
+ res.getSpec().setTargetNamespace(extension.getNamespace());
+ return res;
+ }
+
+ @Override
+ Class customResourceClass() {
+ return BoundedCacheTestCustomResource.class;
+ }
+
+ @Override
+ LocallyRunOperatorExtension extension() {
+ return extension;
+ }
+
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/AbstractTestReconciler.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/AbstractTestReconciler.java
new file mode 100644
index 0000000000..835fcef91a
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/AbstractTestReconciler.java
@@ -0,0 +1,117 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample;
+
+import java.time.Duration;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.fabric8.kubernetes.api.model.*;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.*;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.junit.KubernetesClientAware;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.cache.BoundedItemStore;
+import io.javaoperatorsdk.operator.processing.event.source.cache.CaffeineBoundedItemStores;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.clusterscope.BoundedCacheClusterScopeTestReconciler;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestSpec;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestStatus;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+public abstract class AbstractTestReconciler>
+ implements KubernetesClientAware, Reconciler
,
+ EventSourceInitializer
{
+
+ private static final Logger log =
+ LoggerFactory.getLogger(BoundedCacheClusterScopeTestReconciler.class);
+
+ public static final String DATA_KEY = "dataKey";
+
+ protected KubernetesClient client;
+
+ @Override
+ public UpdateControl
reconcile(
+ P resource,
+ Context
context) {
+ var maybeConfigMap = context.getSecondaryResource(ConfigMap.class);
+ maybeConfigMap.ifPresentOrElse(
+ cm -> updateConfigMapIfNeeded(cm, resource),
+ () -> createConfigMap(resource));
+ ensureStatus(resource);
+ log.info("Reconciled: {}", resource.getMetadata().getName());
+ return UpdateControl.patchStatus(resource);
+ }
+
+ protected void updateConfigMapIfNeeded(ConfigMap cm, P resource) {
+ var data = cm.getData().get(DATA_KEY);
+ if (data == null || data.equals(resource.getSpec().getData())) {
+ cm.setData(Map.of(DATA_KEY, resource.getSpec().getData()));
+ client.configMaps().resource(cm).replace();
+ }
+ }
+
+ protected void createConfigMap(P resource) {
+ var cm = new ConfigMapBuilder()
+ .withMetadata(new ObjectMetaBuilder()
+ .withName(resource.getMetadata().getName())
+ .withNamespace(resource.getSpec().getTargetNamespace())
+ .build())
+ .withData(Map.of(DATA_KEY, resource.getSpec().getData()))
+ .build();
+ cm.addOwnerReference(resource);
+ client.configMaps().resource(cm).create();
+ }
+
+ @Override
+ public KubernetesClient getKubernetesClient() {
+ return client;
+ }
+
+ @Override
+ public void setKubernetesClient(KubernetesClient kubernetesClient) {
+ this.client = kubernetesClient;
+ }
+
+ @Override
+ public Map prepareEventSources(
+ EventSourceContext context) {
+
+ var boundedItemStore =
+ boundedItemStore(new KubernetesClientBuilder().build(),
+ ConfigMap.class, Duration.ofMinutes(1), 1); // setting max size for testing purposes
+
+ var es = new InformerEventSource<>(InformerConfiguration.from(ConfigMap.class, context)
+ .withItemStore(boundedItemStore)
+ .withSecondaryToPrimaryMapper(
+ Mappers.fromOwnerReference(this instanceof BoundedCacheClusterScopeTestReconciler))
+ .build(), context);
+
+ return EventSourceInitializer.nameEventSources(es);
+ }
+
+ private void ensureStatus(P resource) {
+ if (resource.getStatus() == null) {
+ resource.setStatus(new BoundedCacheTestStatus());
+ }
+ }
+
+ public static BoundedItemStore boundedItemStore(
+ KubernetesClient client, Class rClass,
+ Duration accessExpireDuration,
+ // max size is only for testing purposes
+ long cacheMaxSize) {
+ Cache cache = Caffeine.newBuilder()
+ .expireAfterAccess(accessExpireDuration)
+ .maximumSize(cacheMaxSize)
+ .build();
+ return CaffeineBoundedItemStores.boundedItemStore(client, rClass, cache);
+ }
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/clusterscope/BoundedCacheClusterScopeTestCustomResource.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/clusterscope/BoundedCacheClusterScopeTestCustomResource.java
new file mode 100644
index 0000000000..a77416715e
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/clusterscope/BoundedCacheClusterScopeTestCustomResource.java
@@ -0,0 +1,15 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample.clusterscope;
+
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestSpec;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope.BoundedCacheTestStatus;
+
+@Group("sample.javaoperatorsdk")
+@Version("v1")
+@ShortNames("bccs")
+public class BoundedCacheClusterScopeTestCustomResource
+ extends CustomResource {
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/clusterscope/BoundedCacheClusterScopeTestReconciler.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/clusterscope/BoundedCacheClusterScopeTestReconciler.java
new file mode 100644
index 0000000000..a154659164
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/clusterscope/BoundedCacheClusterScopeTestReconciler.java
@@ -0,0 +1,10 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample.clusterscope;
+
+import io.javaoperatorsdk.operator.api.reconciler.*;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.AbstractTestReconciler;
+
+@ControllerConfiguration
+public class BoundedCacheClusterScopeTestReconciler extends
+ AbstractTestReconciler {
+
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestCustomResource.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestCustomResource.java
new file mode 100644
index 0000000000..a5e37917ba
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestCustomResource.java
@@ -0,0 +1,14 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("sample.javaoperatorsdk")
+@Version("v1")
+@ShortNames("bct")
+public class BoundedCacheTestCustomResource
+ extends CustomResource implements Namespaced {
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestReconciler.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestReconciler.java
new file mode 100644
index 0000000000..211877b361
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestReconciler.java
@@ -0,0 +1,10 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope;
+
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.processing.event.source.cache.sample.AbstractTestReconciler;
+
+@ControllerConfiguration
+public class BoundedCacheTestReconciler
+ extends AbstractTestReconciler {
+
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestSpec.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestSpec.java
new file mode 100644
index 0000000000..63e5876267
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestSpec.java
@@ -0,0 +1,25 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope;
+
+public class BoundedCacheTestSpec {
+
+ private String data;
+ private String targetNamespace;
+
+ public String getData() {
+ return data;
+ }
+
+ public BoundedCacheTestSpec setData(String data) {
+ this.data = data;
+ return this;
+ }
+
+ public String getTargetNamespace() {
+ return targetNamespace;
+ }
+
+ public BoundedCacheTestSpec setTargetNamespace(String targetNamespace) {
+ this.targetNamespace = targetNamespace;
+ return this;
+ }
+}
diff --git a/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestStatus.java b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestStatus.java
new file mode 100644
index 0000000000..03a311529e
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/java/io/javaoperatorsdk/operator/processing/event/source/cache/sample/namespacescope/BoundedCacheTestStatus.java
@@ -0,0 +1,6 @@
+package io.javaoperatorsdk.operator.processing.event.source.cache.sample.namespacescope;
+
+import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;
+
+public class BoundedCacheTestStatus extends ObservedGenerationAwareStatus {
+}
diff --git a/caffeine-bounded-cache-support/src/test/resources/log4j2.xml b/caffeine-bounded-cache-support/src/test/resources/log4j2.xml
new file mode 100644
index 0000000000..f23cf772dd
--- /dev/null
+++ b/caffeine-bounded-cache-support/src/test/resources/log4j2.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/documentation/features.md b/docs/documentation/features.md
index ac4f8dfc69..9a62271e6c 100644
--- a/docs/documentation/features.md
+++ b/docs/documentation/features.md
@@ -739,9 +739,9 @@ to add the following dependencies to your project:
```xml
- io.fabric8
- crd-generator-apt
- provided
+ io.fabric8
+ crd-generator-apt
+ provided
```
@@ -756,3 +756,109 @@ with a `mycrs` plural form will result in 2 files:
**NOTE:**
> Quarkus users using the `quarkus-operator-sdk` extension do not need to add any extra dependency
> to get their CRD generated as this is handled by the extension itself.
+
+## Metrics
+
+JOSDK provides built-in support for metrics reporting on what is happening with your reconcilers in the form of
+the `Metrics` interface which can be implemented to connect to your metrics provider of choice, JOSDK calling the
+methods as it goes about reconciling resources. By default, a no-operation implementation is provided thus providing a
+no-cost sane default. A [micrometer](https://micrometer.io)-based implementation is also provided.
+
+You can use a different implementation by overriding the default one provided by the default `ConfigurationService`, as
+follows:
+
+```java
+Metrics metrics= …;
+ConfigurationServiceProvider.overrideCurrent(overrider->overrider.withMetrics(metrics));
+```
+
+### Micrometer implementation
+
+The micrometer implementation is typically created using one of the provided factory methods which, depending on which
+is used, will return either a ready to use instance or a builder allowing users to customized how the implementation
+behaves, in particular when it comes to the granularity of collected metrics. It is, for example, possible to collect
+metrics on a per-resource basis via tags that are associated with meters. This is the default, historical behavior but
+this will change in a future version of JOSDK because this dramatically increases the cardinality of metrics, which
+could lead to performance issues.
+
+To create a `MicrometerMetrics` implementation that behaves how it has historically behaved, you can just create an
+instance via:
+
+```java
+MeterRegistry registry= …;
+Metrics metrics=new MicrometerMetrics(registry)
+```
+
+Note, however, that this constructor is deprecated and we encourage you to use the factory methods instead, which either
+return a fully pre-configured instance or a builder object that will allow you to configure more easily how the instance
+will behave. You can, for example, configure whether or not the implementation should collect metrics on a per-resource
+basis, whether or not associated meters should be removed when a resource is deleted and how the clean-up is performed.
+See the relevant classes documentation for more details.
+
+For example, the following will create a `MicrometerMetrics` instance configured to collect metrics on a per-resource
+basis, deleting the associated meters after 5 seconds when a resource is deleted, using up to 2 threads to do so.
+
+```java
+MicrometerMetrics.newPerResourceCollectingMicrometerMetricsBuilder(registry)
+ .withCleanUpDelayInSeconds(5)
+ .withCleaningThreadNumber(2)
+ .build()
+```
+
+The micrometer implementation records the following metrics:
+
+| Meter name | Type | Tag names | Description |
+|-----------------------------------------------------------|----------------|-----------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
+| operator.sdk.reconciliations.executions. | gauge | group, version, kind | Number of executions of the named reconciler |
+| operator.sdk.reconciliations.queue.size. | gauge | group, version, kind | How many resources are queued to get reconciled by named reconciler |
+| operator.sdk.