Skip to content

Commit e6ad29f

Browse files
committed
wip
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 3c9e5b3 commit e6ad29f

6 files changed

Lines changed: 81 additions & 9 deletions

File tree

docs/content/en/docs/documentation/eventing.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,57 @@ for [primary resources](https://github.com/operator-framework/java-operator-sdk/
346346

347347
See
348348
also [CaffeineBoundedItemStores](https://github.com/operator-framework/java-operator-sdk/blob/main/caffeine-bounded-cache-support/src/main/java/io/javaoperatorsdk/operator/processing/event/source/cache/CaffeineBoundedItemStores.java)
349-
for more details.
349+
for more details.
350+
351+
### Sharing Informers Between Controllers (Informer Pool)
352+
353+
{{% alert title="Experimental" color="warning" %}}
354+
Informer pooling is marked `@Experimental`: the feature itself is production ready, but its
355+
configuration API may still change in a non-backwards-compatible way.
356+
{{% /alert %}}
357+
358+
By default JOSDK maintains an *informer pool* so that informers are **shared** across controllers
359+
and event sources. When several `InformerEventSource`s (whether belonging to different controllers,
360+
or dynamically registered at runtime) watch the same resource type with an equivalent configuration,
361+
they are all backed by a single underlying `SharedIndexInformer` instead of one informer each. This
362+
reduces memory usage and the number of watch connections opened against the API server — which
363+
matters in operators where many controllers watch the same secondary resource type (for example
364+
`ConfigMap` or `Secret`).
365+
366+
Two event sources share an informer when their effective informer configuration matches on all of:
367+
368+
- the target cluster (API server URL, including the remote client used for
369+
[multi-cluster](#informereventsource-multi-cluster-support) event sources),
370+
- the resource type (or the group/version/kind for generic resources),
371+
- the watched namespace,
372+
- the label, field and shard selectors,
373+
- the configured [item store](#bounded-caches-for-informers).
374+
375+
The `informerListLimit` is intentionally *not* part of this identity: if two otherwise-equivalent
376+
event sources request a different list limit, the existing informer is reused (a warning is logged
377+
and the first-configured limit is kept). Indexers are also not part of the identity, since they can
378+
be added to a shared informer independently; just make sure indexer names don't collide.
379+
380+
The pool is reference-counted: the shared informer is created on first use and only stopped once the
381+
last event source using it is de-registered (or its controller stops). Dynamically registering an
382+
event source for a resource that is already backed by a running informer reuses that informer, and
383+
the initial state already in its cache is replayed to the newly added handler.
384+
385+
#### Selecting the pooling strategy
386+
387+
The strategy is provided by the
388+
[`InformerPool`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/InformerPool.java)
389+
configured on the `ConfigurationService`. Two implementations are available:
390+
391+
- [`DefaultInformerPool`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/DefaultInformerPool.java)
392+
(the default): shares informers as described above.
393+
- [`AlwaysNewInformerPool`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AlwaysNewInformerPool.java):
394+
never shares informers, creating a dedicated informer for every event source. Use this to opt out
395+
of pooling and restore the pre-pooling behavior.
396+
397+
You can override the strategy through the `ConfigurationService`:
398+
399+
```java
400+
Operator operator = new Operator(overrider ->
401+
overrider.withInformerPool(new AlwaysNewInformerPool()));
402+
```

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.fabric8.kubernetes.client.KubernetesClient;
2525
import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
2626
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
27+
import io.javaoperatorsdk.operator.processing.event.source.informer.pool.DefaultInformerPool;
2728
import io.javaoperatorsdk.operator.processing.event.source.informer.pool.InformerPool;
2829

2930
/**
@@ -199,7 +200,7 @@ public synchronized InformerPool informerPool() {
199200
// can therefore share the underlying informers; synchronized so concurrent first-access from
200201
// multiple controllers cannot create (and share out) more than one pool instance
201202
if (informerPool == null) {
202-
informerPool = ConfigurationService.super.informerPool();
203+
informerPool = new DefaultInformerPool(this);
203204
}
204205
return informerPool;
205206
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
4444
import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory;
4545
import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource;
46-
import io.javaoperatorsdk.operator.processing.event.source.informer.pool.DefaultInformerPool;
4746
import io.javaoperatorsdk.operator.processing.event.source.informer.pool.InformerPool;
4847

4948
/** An interface from which to retrieve configuration information. */
@@ -479,8 +478,20 @@ default boolean cloneSecondaryResourcesWhenGettingFromCache() {
479478
return false;
480479
}
481480

482-
default InformerPool informerPool() {
483-
// todo this is probably wrong here
484-
return new DefaultInformerPool(this);
485-
}
481+
/**
482+
* The {@link InformerPool} used to create and (when using the default, sharing pool) share the
483+
* informers backing the event sources of all controllers managed by this {@code
484+
* ConfigurationService}.
485+
*
486+
* <p><strong>Implementations must return the same instance on every call.</strong> The pool is
487+
* effectively a per-{@code ConfigurationService} singleton: controllers share informers only if
488+
* they resolve the same pool, and reference counting / informer shutdown are only correct if
489+
* {@code getInformer} and {@code releaseInformer} operate on that same instance. This is
490+
* intentionally not a {@code default} method, since a {@code default} could not cache the result
491+
* and would hand out a fresh (unshared) pool on each call; {@link AbstractConfigurationService}
492+
* provides a cached implementation backed by the default sharing pool.
493+
*
494+
* @return the informer pool for this configuration service
495+
*/
496+
InformerPool informerPool();
486497
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AlwaysNewInformerPool.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
@SuppressWarnings({"unchecked", "rawtypes"})
3030
public class AlwaysNewInformerPool extends AbstractInformerPool {
31-
// todo integration tests
31+
3232
private static final Logger log = LoggerFactory.getLogger(AlwaysNewInformerPool.class);
3333

3434
private final Map<ClassifierWithName, SharedIndexInformer> informers = new ConcurrentHashMap();

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/InformerClassifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public boolean equals(Object o) {
5252
&& Objects.equals(resourceClass, that.resourceClass)
5353
&& Objects.equals(groupVersionKind, that.groupVersionKind)
5454
&& Objects.equals(fieldSelector, that.fieldSelector)
55-
&& Objects.equals(itemStore, that.itemStore);
55+
&& Objects.equals(itemSto re, that.itemStore);
5656
}
5757

5858
@Override

operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.javaoperatorsdk.operator.api.config.*;
2929
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
3030
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
31+
import io.javaoperatorsdk.operator.processing.event.source.informer.pool.InformerPool;
3132

3233
import static org.assertj.core.api.Assertions.assertThat;
3334
import static org.awaitility.Awaitility.await;
@@ -127,6 +128,12 @@ public Set<String> getKnownReconcilerNames() {
127128
public Version getVersion() {
128129
return null;
129130
}
131+
132+
// only used here to obtain the resource cloner, so the pool is never accessed
133+
@Override
134+
public InformerPool informerPool() {
135+
return null;
136+
}
130137
}.getResourceCloner();
131138
}
132139
}

0 commit comments

Comments
 (0)