Informer pools#3325
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces initial scaffolding for “event source pooling” (starting with informers) under processing.event.source.pool, likely to enable sharing/reuse of informers across components/controllers.
Changes:
- Added
EventSourcePoolinterface andAbstractEventSourcePoolbase type. - Added
InformerClassifierrecord to key pooled informers by selector/namespace/resource type. - Added initial (currently incomplete)
InformerPoolimplementation and a minor whitespace cleanup inInformerManager.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/pool/InformerPool.java |
Adds a new pool for SharedIndexInformer<?> instances (currently stubbed/incomplete). |
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/pool/InformerClassifier.java |
Adds a classifier record intended as the cache key for pooled informers. |
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/pool/EventSourcePool.java |
Introduces a generic pool interface for event sources. |
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/pool/AbstractEventSourcePool.java |
Adds a base class placeholder for pool implementations. |
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerManager.java |
Removes an extraneous whitespace line in createEventSource. |
d5157bd to
0297680
Compare
e45bf4c to
1e91a47
Compare
2244abf to
fd58492
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 54 out of 54 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AbstractInformerPool.java:72
- createInformer() always calls withLabelSelector(...) and withShardSelector(...) in the initial namespace scoping branch, even when the classifier selectors are null. This can lead to null being passed to the Fabric8 DSL and is also redundant with the subsequent null-guarded selector application block.
FilterWatchListDeletable filteredClient;
if (WATCH_ALL_NAMESPACES.equals(classifier.namespaceIdentifier())) {
filteredClient =
clientWithResource
.inAnyNamespace()
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/EventSource.java:137
- Typo in Javadoc: "Strop" should be "Stop".
/** Strop the event source */
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 52 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java:1
- Returning
nullfromConfigurationService.informerPool()violates the new contract implied by the interface (and can cause confusing NPEs if code paths change). Even in tests, return a real pool instance (e.g., the default pool) or a minimal stub that satisfies the interface and is safe to call.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 52 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AlwaysNewInformerPool.java:45
- The
containsKey(...)+put(...)sequence is not atomic on aConcurrentHashMap. If two threads request the same (controllerName,name,classifier) at the same time, both can passcontainsKeyand one will overwrite the other, leaking an informer instance. UseputIfAbsent(orcompute) to make registration atomic, and stop the newly created informer if it loses the race.
var key = new ClassifierWithName(controllerName, name, classifier);
if (informers.containsKey(key)) {
throw new OperatorException(
operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java:136
informerPool()returningnullcan lead to a hard-to-diagnoseNullPointerExceptionif this test (or the default methods it calls) ever ends up touching the pool. If the pool is intentionally unsupported here, it’s safer to throw an explicitUnsupportedOperationException.
| private String informerInfo() { | ||
| return "InformerWrapper [" + versionedFullResourceName() + "]"; | ||
| return "InformerWrapper [informerInfo" + informer.getApiTypeClass().getSimpleName() + "]"; | ||
| } |
…ources
Introduce an informer pool so that InformerEventSources watching the same
resource type with an equivalent configuration share a single underlying
fabric8 SharedIndexInformer instead of creating one per event source. This
reduces memory usage and the number of watch connections opened against the API
server when many controllers watch the same (secondary) resource type.
- Add an InformerPool abstraction with two strategies:
- DefaultInformerPool (default): reference-counted sharing of informers keyed
by an InformerClassifier (API server URL, resource type / GVK, namespace,
label/field/shard selectors, item store). The informer is created on first
use and stopped only when the last user releases it. informerListLimit is
intentionally not part of the identity; indexers are added independently.
- AlwaysNewInformerPool: never shares (one informer per event source), to opt
out of pooling and keep the previous behavior.
- Resolve the pool via ConfigurationService.informerPool() (an abstract method,
cached as a per-ConfigurationService singleton in AbstractConfigurationService)
and expose ConfigurationServiceOverrider.withInformerPool(...) to select the
strategy.
- Route InformerManager / ManagedInformerEventSource through the pool. Dynamic
event source registration reuses an already-running informer and relies on the
informer replaying its cache to the newly added handler for initial state.
- Add group/version/kind and field-selector support to the classifier.
- Tests: unit tests for InformerClassifier equality and pool reference counting;
integration tests for basic sharing, dynamic registration and de-registration,
each run against both pool strategies.
- Add documentation for informer pooling.
The feature is marked @experimental: the runtime behavior is production-ready,
but the configuration API may still change in a non-backwards-compatible way.
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 52 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AlwaysNewInformerPool.java:58
- The containsKey(...) check followed by informers.put(...) is not atomic. Under concurrent calls, two threads can both pass the containsKey check, create two informers, and one put will overwrite the other entry (leaking the overwritten informer) before the exception path is hit. Use an atomic putIfAbsent/compute and, if you still throw on duplicates, ensure any newly created informer is stopped before throwing to avoid leaking watch threads/connections.
var informer = createInformer(classifier, client);
informers.put(key, informer);
return informer;
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AbstractInformerPool.java:140
- The thread name format string looks accidentally duplicated ("InformerInfo[informerInfo...") which makes debugging output harder to read/grep. Consider simplifying to a single, consistent prefix (e.g., "InformerInfo[] ").
thread.setName(
"InformerInfo[" + informer.getApiTypeClass().getSimpleName() + "] " + thread.getId());
final var resourceName = informer.getApiTypeClass().getSimpleName();
// idempotent: if the informer was already started (e.g. by the pool when it was
// created/reused), this just returns the existing start future without restarting it
var start = informer.start();
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java:109
- This toString/informerInfo label contains an extra "informerInfo" token ("InformerWrapper [informerInfo]") which looks like an unintentional duplication and makes logs less clear. Consider emitting just the type name (and optionally namespace) in the wrapper label.
private String informerInfo() {
return "InformerWrapper [informerInfo" + informer.getApiTypeClass().getSimpleName() + "]";
}
operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java:136
- Returning null from ConfigurationService#informerPool() violates the new contract and will cause a NullPointerException if this helper ConfigurationService is ever used beyond the current narrow purpose. Prefer failing fast with an UnsupportedOperationException (or returning a real pool) so future refactors don't introduce a hard-to-diagnose NPE.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 52 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerWrapper.java:109
- informerInfo() currently returns "InformerWrapper [informerInfo]", which looks like a leftover debug label and makes toString()/logs confusing when diagnosing informer issues. It should format consistently without the stray "informerInfo" token.
private String informerInfo() {
return "InformerWrapper [informerInfo" + informer.getApiTypeClass().getSimpleName() + "]";
}
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/AlwaysNewInformerPool.java:47
- getInformer() uses informers.containsKey(key) followed by informers.put(key, informer). With a ConcurrentHashMap this check-then-act sequence is not atomic: two concurrent callers can both observe the key as absent, create two informers, and one put will overwrite the other (leaking an informer) without throwing the intended OperatorException. Use putIfAbsent (or compute) to make the registration atomic and stop the newly-created informer on collision.
var key = new ClassifierWithName(controllerName, name, classifier);
if (informers.containsKey(key)) {
throw new OperatorException(
"Informer already registered for controller: "
+ controllerName
operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/standalonedependent/StandaloneDependentResourceIT.java:136
- This test-only ConfigurationService implementation returns null from informerPool(). Even if currently “never accessed”, it’s brittle and can turn into an unexpected NPE if the cloner implementation (or any other default method used here) starts consulting informerPool(). Since the test already has a running LocallyRunOperatorExtension, it can avoid the ad-hoc ConfigurationService entirely and reuse the operator’s real ConfigurationService/resource cloner instead.
Summary
Introduces an informer pool so that informers backing
InformerEventSources (and the internal controller event source) can be shared across controllers instead of each controller/event source always creating its ownSharedIndexInformer. This reduces the number of watch connections opened against the API server and memory overhead in operators where multiple controllers (or multiple dynamically-registered event sources) watch the same secondary resource type (e.g.ConfigMap,Secret).This is marked
@Experimental: the pooling behavior itself is production ready, but the configuration API may still evolve in a non-backwards-compatible way.What's new
InformerPoolabstraction (processing/event/source/informer/pool)InformerPool— interface for getting/releasing informers, keyed by anInformerClassifier, plus reference counting vianumberOfInformersForResource.InformerClassifier— identifies "equivalent" informer requests: API server URL, label/field/shard selectors, namespace, resource type (or GVK for generic resources), and item store.informerListLimitand indexers are intentionally excluded from identity (a warning is logged if an existing informer is reused with a different list limit; indexers can be added independently to a shared informer as long as names don't collide).AbstractInformerPool— shared logic for building the underlyingSharedIndexInformerfrom a classifier (selectors, field selectors, item store, list limit, exception/stopped handlers) and starting it with the configuredcacheSyncTimeout.DefaultInformerPool(new default) — reference-counted, sharing pool: creates an informer on first request for a given classifier and reuses it for subsequent requests; only stops it once the last user releases it.AlwaysNewInformerPool— opt-out pool that creates a dedicated informer per event source, preserving pre-pooling behavior.Configuration
ConfigurationService.informerPool()— new (non-default) method; implementations must return the same instance on every call so controllers using the sameConfigurationServiceshare the pool.AbstractConfigurationServicecaches aDefaultInformerPoollazily (synchronized to avoid creating more than one instance under concurrent first access).ConfigurationServiceOverrider.withInformerPool(InformerPool)— lets users opt intoAlwaysNewInformerPool(or a custom implementation) to disable/customize sharing.Integration
InformerManagerandInformerWrapperwere reworked to go through the pool instead of creating/starting/stopping theSharedIndexInformerdirectly:InformerManagernow builds anInformerClassifierper namespace and callsinformerPool.getInformer(...)/releaseInformer(...)instead of constructing the informer from the Fabric8 client itself.getInformeronly registers/reference-counts,InformerPool#startblocks (idempotently) until cache sync, andreleaseInformerdecrements the reference count, stopping the informer only when it drops to zero. The event handler is always removed from the returned informer, even when it stays running for other users.InformerWrapperno longer owns lifecycle (start/stopremoved); it now just exposes the informer/classifier and cache-facing behavior.ControllerEventSourceandManagedInformerEventSourceupdated to go through the same pooled path.Docs
Added a new "Sharing Informers Between Controllers (Informer Pool)" section to
docs/content/en/docs/documentation/eventing.mddescribing sharing semantics, what counts toward informer identity, and how to select/override the pooling strategy.Tests
AlwaysNewInformerPool,DefaultInformerPool, andInformerClassifier(equality/identity semantics).informerpool:basic— two reconcilers sharing an informer for the same secondary resource type, parameterized to also run againstAlwaysNewInformerPoolfor regression coverage.deregister— verifies informer lifecycle when an event source is dynamically deregistered while shared.dynamic— verifies dynamically-registered/static shared informers, including replay of existing cache state to a newly attached handler.ControllerTest,EventSourceManagerTest,ReconciliationDispatcherTest,ControllerEventSourceTest,InformerEventSourceTest,MockKubernetesClient,StandaloneDependentResourceIT,WorkflowMultipleActivationIT) updated for the new construction/wiring paths.