Skip to content

Informer pools#3325

Open
csviri wants to merge 9 commits into
nextfrom
informer-pool
Open

Informer pools#3325
csviri wants to merge 9 commits into
nextfrom
informer-pool

Conversation

@csviri

@csviri csviri commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

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 own SharedIndexInformer. 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

InformerPool abstraction (processing/event/source/informer/pool)

  • InformerPool — interface for getting/releasing informers, keyed by an InformerClassifier, plus reference counting via numberOfInformersForResource.
  • InformerClassifier — identifies "equivalent" informer requests: API server URL, label/field/shard selectors, namespace, resource type (or GVK for generic resources), and item store. informerListLimit and 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 underlying SharedIndexInformer from a classifier (selectors, field selectors, item store, list limit, exception/stopped handlers) and starting it with the configured cacheSyncTimeout.
  • 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 same ConfigurationService share the pool. AbstractConfigurationService caches a DefaultInformerPool lazily (synchronized to avoid creating more than one instance under concurrent first access).
  • ConfigurationServiceOverrider.withInformerPool(InformerPool) — lets users opt into AlwaysNewInformerPool (or a custom implementation) to disable/customize sharing.

Integration

  • InformerManager and InformerWrapper were reworked to go through the pool instead of creating/starting/stopping the SharedIndexInformer directly:
    • InformerManager now builds an InformerClassifier per namespace and calls informerPool.getInformer(...) / releaseInformer(...) instead of constructing the informer from the Fabric8 client itself.
    • Starting/stopping is now delegated to the pool: getInformer only registers/reference-counts, InformerPool#start blocks (idempotently) until cache sync, and releaseInformer decrements 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.
    • InformerWrapper no longer owns lifecycle (start/stop removed); it now just exposes the informer/classifier and cache-facing behavior.
  • ControllerEventSource and ManagedInformerEventSource updated to go through the same pooled path.
  • Dynamic registration/deregistration of event sources correctly reuses/releases pooled informers, replaying existing cache state to newly added handlers when attaching to an already-running shared informer.

Docs

Added a new "Sharing Informers Between Controllers (Informer Pool)" section to docs/content/en/docs/documentation/eventing.md describing sharing semantics, what counts toward informer identity, and how to select/override the pooling strategy.

Tests

  • Unit tests for AlwaysNewInformerPool, DefaultInformerPool, and InformerClassifier (equality/identity semantics).
  • New integration tests under informerpool:
    • basic — two reconcilers sharing an informer for the same secondary resource type, parameterized to also run against AlwaysNewInformerPool for 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.
  • Existing tests (ControllerTest, EventSourceManagerTest, ReconciliationDispatcherTest, ControllerEventSourceTest, InformerEventSourceTest, MockKubernetesClient, StandaloneDependentResourceIT, WorkflowMultipleActivationIT) updated for the new construction/wiring paths.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Apr 29, 2026
@csviri
csviri changed the base branch from main to next April 29, 2026 13:26
@csviri csviri linked an issue Apr 29, 2026 that may be closed by this pull request
5 tasks
@csviri
csviri marked this pull request as ready for review April 29, 2026 13:28
Copilot AI review requested due to automatic review settings April 29, 2026 13:28
@openshift-ci
openshift-ci Bot requested review from metacosm and xstefank April 29, 2026 13:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 EventSourcePool interface and AbstractEventSourcePool base type.
  • Added InformerClassifier record to key pooled informers by selector/namespace/resource type.
  • Added initial (currently incomplete) InformerPool implementation and a minor whitespace cleanup in InformerManager.

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.

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 30, 2026
@csviri
csviri marked this pull request as draft May 4, 2026 12:59
@csviri
csviri force-pushed the next branch 2 times, most recently from d5157bd to 0297680 Compare May 11, 2026 11:49
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label May 17, 2026
@csviri
csviri force-pushed the next branch 3 times, most recently from e45bf4c to 1e91a47 Compare July 2, 2026 06:51
Base automatically changed from next to main July 2, 2026 12:55
@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 4, 2026
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 6, 2026
@csviri
csviri requested a review from Copilot July 22, 2026 14:04
@csviri
csviri marked this pull request as ready for review July 22, 2026 14:04
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 22, 2026
@csviri
csviri force-pushed the informer-pool branch 2 times, most recently from 2244abf to fd58492 Compare July 22, 2026 14:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 */

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 null from ConfigurationService.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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a ConcurrentHashMap. If two threads request the same (controllerName,name,classifier) at the same time, both can pass containsKey and one will overwrite the other, leaking an informer instance. Use putIfAbsent (or compute) 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() returning null can lead to a hard-to-diagnose NullPointerException if 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 explicit UnsupportedOperationException.

Comment on lines 107 to 109
private String informerInfo() {
return "InformerWrapper [" + versionedFullResourceName() + "]";
return "InformerWrapper [informerInfo" + informer.getApiTypeClass().getSimpleName() + "]";
}
csviri and others added 9 commits July 22, 2026 21:36
…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>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>

# Conflicts:
#	operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/pool/DefaultInformerPool.java
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Informer Pools

2 participants