Skip to content

Commit 82d7945

Browse files
Mohit Guptaclaude
andcommitted
improve: generic cache-aware secondary resource lookup for dependent resources
Introduces a cache-aware O(1) secondary resource lookup path at the AbstractEventSourceHolderDependentResource level, generalising the optimisation that was previously only available in a Kubernetes-specific branch (optimize-kube-dependent). ## Problem AbstractDependentResource.getSecondaryResource() calls Context.getSecondaryResources(resourceType()), which loads all secondary resources into a Set<R> and then delegates to selectTargetSecondaryResource() to filter down to the single expected resource. For KubernetesDependentResource this additionally calls getOrComputeDesired() inside selectTargetSecondaryResource() just to derive the target name/namespace — even though InformerEventSource already provides an O(1) cache.get(ResourceID) API. For AbstractExternalDependentResource, selectTargetSecondaryResource() calls desired(primary, context) equally wastefully for a mere lookup. ## Solution AbstractEventSourceHolderDependentResource: overrides getSecondaryResource() to check whether the event source implements Cache<R>. When it does, and when targetSecondaryResourceID() returns a non-null ResourceID, the framework calls cache.get(resourceID) directly — a single O(1) hash-map lookup. Adds targetSecondaryResourceID() returning null by default (fully backward compatible: existing subclasses see no change). KubernetesDependentResource: the existing targetSecondaryResourceID() override naturally wires into the new generic parent mechanism. selectTargetSecondaryResource() is retained as a fallback for shared/non-informer event source scenarios. Both methods re-documented to clarify the preferred fast path. AbstractExternalDependentResource: overrides getSecondaryResource() to call ExternalResourceCachingEventSource.getSecondaryResource(primaryID) directly, bypassing desired() computation in selectTargetSecondaryResource(). All changes are strictly additive/override-with-fallback — no API removed, no existing subclass needs to change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 04a694e commit 82d7945

3 files changed

Lines changed: 107 additions & 5 deletions

File tree

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller;
2828
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
2929
import io.javaoperatorsdk.operator.processing.event.ResourceID;
30+
import io.javaoperatorsdk.operator.processing.event.source.Cache;
3031
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
3132

3233
@Ignore
@@ -123,6 +124,60 @@ public Optional<T> eventSource() {
123124
return Optional.ofNullable(eventSource);
124125
}
125126

127+
/**
128+
* Returns the secondary resource for the given primary, using an optimized O(1) cache lookup when
129+
* the associated event source implements {@link Cache} and {@link
130+
* #targetSecondaryResourceID(HasMetadata, Context)} is overridden to return a non-null {@link
131+
* ResourceID}.
132+
*
133+
* <p>When the event source does not implement {@link Cache} or the target {@link ResourceID} is
134+
* not known, the method falls back to the default context-based lookup via {@link
135+
* AbstractDependentResource#getSecondaryResource(HasMetadata, Context)}, which loads all
136+
* secondary resources of the matching type and delegates to {@link
137+
* AbstractDependentResource#selectTargetSecondaryResource(java.util.Set, HasMetadata, Context)}.
138+
*
139+
* @param primary the primary resource
140+
* @param context the reconciliation context
141+
* @return the secondary resource if found, or {@link Optional#empty()}
142+
*/
143+
@Override
144+
public Optional<R> getSecondaryResource(P primary, Context<P> context) {
145+
var es = eventSource();
146+
if (es.isPresent() && es.get() instanceof Cache<?>) {
147+
@SuppressWarnings("unchecked")
148+
Cache<R> cache = (Cache<R>) es.get();
149+
ResourceID targetID = targetSecondaryResourceID(primary, context);
150+
if (targetID != null) {
151+
return cache.get(targetID);
152+
}
153+
}
154+
return super.getSecondaryResource(primary, context);
155+
}
156+
157+
/**
158+
* Returns the {@link ResourceID} of the expected secondary resource to enable a direct O(1) cache
159+
* lookup, bypassing the costlier {@link
160+
* io.javaoperatorsdk.operator.api.reconciler.Context#getSecondaryResources} path.
161+
*
162+
* <p>This hook is called by {@link #getSecondaryResource(HasMetadata, Context)} only when the
163+
* associated event source implements {@link Cache}. When this method returns a non-null value,
164+
* the framework performs {@code cache.get(resourceID)} directly instead of loading all secondary
165+
* resources and filtering through them.
166+
*
167+
* <p>Implementors are strongly encouraged to return a cheap, statically-derivable {@link
168+
* ResourceID} (e.g. based on the primary's name or a fixed naming convention) to avoid computing
169+
* the full desired state during secondary resource lookup. See {@code
170+
* KubernetesDependentResource#targetSecondaryResourceID} for a concrete example.
171+
*
172+
* @param primary the primary resource associated with the secondary resource being looked up
173+
* @param context the current reconciliation context
174+
* @return the {@link ResourceID} identifying the expected secondary resource, or {@code null} if
175+
* not known (falls back to the default lookup path)
176+
*/
177+
protected ResourceID targetSecondaryResourceID(P primary, Context<P> context) {
178+
return null;
179+
}
180+
126181
protected void onCreated(P primary, R created, Context<P> context) {
127182
if (isCacheFillerEventSource) {
128183
recentOperationCacheFiller()

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
2727
import io.javaoperatorsdk.operator.processing.event.ResourceID;
2828
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
29+
import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource;
2930
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
3031

3132
public abstract class AbstractExternalDependentResource<
@@ -103,6 +104,33 @@ protected void handleExplicitStateCreation(P primary, R created, Context<P> cont
103104
}
104105
}
105106

107+
/**
108+
* Returns the secondary resource for the given primary, using a direct cache lookup on the
109+
* underlying {@link
110+
* io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource} when
111+
* available. This avoids the default path which calls {@link
112+
* io.javaoperatorsdk.operator.api.reconciler.Context#getSecondaryResources} and then delegates to
113+
* {@link #selectTargetSecondaryResource(java.util.Set, HasMetadata, Context)}, which in turn
114+
* computes the full desired state unnecessarily for a mere lookup.
115+
*
116+
* <p>Falls back to the standard path when the event source is not cache-aware (e.g. for custom or
117+
* shared event source implementations).
118+
*
119+
* @param primary the primary resource
120+
* @param context the reconciliation context
121+
* @return the secondary resource if found, or {@link Optional#empty()}
122+
*/
123+
@Override
124+
public Optional<R> getSecondaryResource(P primary, Context<P> context) {
125+
var es = eventSource();
126+
if (es.isPresent() && es.get() instanceof ExternalResourceCachingEventSource<?, ?, ?>) {
127+
@SuppressWarnings("unchecked")
128+
var extCache = (ExternalResourceCachingEventSource<R, P, ?>) es.get();
129+
return extCache.getSecondaryResource(ResourceID.fromResource(primary));
130+
}
131+
return super.getSecondaryResource(primary, context);
132+
}
133+
106134
@Override
107135
public Matcher.Result<R> match(R resource, P primary, Context<P> context) {
108136
var desired = getOrComputeDesired(context);

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,18 @@ protected void addSecondaryToPrimaryMapperAnnotations(
247247
annotations.put(typeKey, GroupVersionKind.gvkFor(primary.getClass()).toGVKString());
248248
}
249249

250+
/**
251+
* Provides a fallback secondary resource selection when the cache-aware lookup path (via {@link
252+
* #targetSecondaryResourceID(HasMetadata, Context)}) is not taken. This occurs when the event
253+
* source does not implement {@link io.javaoperatorsdk.operator.processing.event.source.Cache} or
254+
* when a shared event source is used.
255+
*
256+
* <p>In the common case where the event source is an {@link
257+
* io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}, this method
258+
* is <em>not</em> called because {@link #targetSecondaryResourceID(HasMetadata, Context)} is
259+
* implemented and {@code InformerEventSource} implements {@code Cache}. Prefer overriding {@link
260+
* #targetSecondaryResourceID(HasMetadata, Context)} for performance-critical code paths.
261+
*/
250262
@Override
251263
protected Optional<R> selectTargetSecondaryResource(
252264
Set<R> secondaryResources, P primary, Context<P> context) {
@@ -262,13 +274,20 @@ protected Optional<R> selectTargetSecondaryResource(
262274
}
263275

264276
/**
265-
* Override this method in order to optimize and not compute the desired when selecting the target
266-
* secondary resource. Simply, a static ResourceID can be returned.
277+
* Returns the {@link ResourceID} of the target secondary resource, driving the O(1) cache lookup
278+
* in the parent class when the event source implements {@link
279+
* io.javaoperatorsdk.operator.processing.event.source.Cache}.
267280
*
268-
* @param primary resource
269-
* @param context of current reconciliation
270-
* @return id of the target managed resource
281+
* <p>Override this method to provide a cheap, statically-derivable {@link ResourceID} (e.g.
282+
* derived from the primary resource's name or a fixed naming convention) and thereby avoid
283+
* computing the full desired state during secondary resource lookup. The base implementation
284+
* falls back to {@link #getOrComputeDesired(Context)}, which is correct but may be costlier.
285+
*
286+
* @param primary the primary resource
287+
* @param context the current reconciliation context
288+
* @return the {@link ResourceID} of the managed secondary resource
271289
*/
290+
@Override
272291
protected ResourceID targetSecondaryResourceID(P primary, Context<P> context) {
273292
return ResourceID.fromResource(getOrComputeDesired(context));
274293
}

0 commit comments

Comments
 (0)