Skip to content

fix: read the external resource cache under the event source monitor - #3524

Draft
csviri wants to merge 1 commit into
operator-framework:mainfrom
csviri:fix/external-cache-unsynchronized-reads
Draft

fix: read the external resource cache under the event source monitor#3524
csviri wants to merge 1 commit into
operator-framework:mainfrom
csviri:fix/external-cache-unsynchronized-reads

Conversation

@csviri

@csviri csviri commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

ExternalResourceCachingEventSource mutates its cache from synchronized
methods (handleResources, handleDelete,
handleRecentResourceCreate/Update), but the read paths were not
synchronized. The outer map is a ConcurrentHashMap; the nested
per-primary maps are plain HashMaps that handleDelete mutates in
place, so a reconciler thread reading them while a poll or informer
thread writes can observe a corrupted map or throw.

getSecondaryResources(ResourceID) additionally looked the primary up
twice:

var cachedValues = cache.get(primaryID);
if (cachedValues == null) { return Collections.emptySet(); }
else { return new HashSet<>(cache.get(primaryID).values()); }

If a concurrent handleDelete removes the entry between the two calls,
the second get returns null and this throws a NullPointerException.

Adds a cachedResourcesFor helper that snapshots the cached resources
while holding the monitor, and routes getSecondaryResources plus the
PerResourcePollingEventSource and CachingInboundEventSource overrides
(and checkAndRegisterTask) through it. The helper only copies, so the
potentially slow ResourceFetcher calls in those overrides still run
outside the lock and cannot block the informer or poll threads.

getCache() still returns a live view for backwards compatibility, but
now documents that iterating the nested maps requires synchronizing on the
event source.

Adds a test asserting getSecondaryResources returns a snapshot rather
than a live view.

Part of #3517

`ExternalResourceCachingEventSource` mutates its cache from `synchronized`
methods (`handleResources`, `handleDelete`,
`handleRecentResourceCreate/Update`), but the read paths were not
synchronized. The outer map is a `ConcurrentHashMap`; the nested
per-primary maps are plain `HashMap`s that `handleDelete` mutates in
place, so a reconciler thread reading them while a poll or informer
thread writes can observe a corrupted map or throw.

`getSecondaryResources(ResourceID)` additionally looked the primary up
twice:

    var cachedValues = cache.get(primaryID);
    if (cachedValues == null) { return Collections.emptySet(); }
    else { return new HashSet<>(cache.get(primaryID).values()); }

If a concurrent `handleDelete` removes the entry between the two calls,
the second `get` returns null and this throws a NullPointerException.

Adds a `cachedResourcesFor` helper that snapshots the cached resources
while holding the monitor, and routes `getSecondaryResources` plus the
`PerResourcePollingEventSource` and `CachingInboundEventSource` overrides
(and `checkAndRegisterTask`) through it. The helper only copies, so the
potentially slow `ResourceFetcher` calls in those overrides still run
outside the lock and cannot block the informer or poll threads.

`getCache()` still returns a live view for backwards compatibility, but
now documents that iterating the nested maps requires synchronizing on the
event source.

Adds a test asserting `getSecondaryResources` returns a snapshot rather
than a live view.
Copilot AI review requested due to automatic review settings July 30, 2026 09:04
@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 Jul 30, 2026

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

This PR fixes a concurrency correctness issue in ExternalResourceCachingEventSource by ensuring cache read paths take a synchronized snapshot of per-primary cached resources (the nested maps are HashMaps and are mutated under the event source monitor). This avoids races that could lead to corrupted reads or TOCTOU NullPointerExceptions.

Changes:

  • Added a cachedResourcesFor(ResourceID) helper that synchronizes on the event source and returns a snapshot copy of cached secondary resources.
  • Routed getSecondaryResources(ResourceID) and the relevant overrides in polling/inbound event sources through the snapshot helper.
  • Documented that getCache() returns a live view and that iterating nested maps requires synchronizing on the event source; added a regression test asserting snapshot behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java Introduces synchronized snapshot helper for cache reads and documents thread-safety expectations of getCache().
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/polling/PerResourcePollingEventSource.java Uses cachedResourcesFor to safely snapshot cached resources when registering tasks and when serving getSecondaryResources.
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/inbound/CachingInboundEventSource.java Uses cachedResourcesFor to safely snapshot cached resources in getSecondaryResources.
operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSourceTest.java Adds a test verifying getSecondaryResources returns a snapshot, not a live view.

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

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants