Skip to content

Commit e8088fd

Browse files
committed
improve: related resource reference change (#3425)
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent cba2337 commit e8088fd

20 files changed

Lines changed: 694 additions & 89 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ rare corner cases. Returning an empty set means that the mapper considered the s
139139
resource event as irrelevant and the SDK will thus not trigger a reconciliation of the primary
140140
resource in that situation.
141141

142+
On an update event, the SDK calls `toPrimaryResourceIDs` for **both the old and the new version**
143+
of the secondary resource. This way it can reconcile not only the primaries that the secondary
144+
currently maps to, but also those it previously mapped to and no longer does. So when a reference
145+
changes — including when only a subset of the referenced primaries changes — both the newly
146+
referenced and the dropped primaries are reconciled, and a dropped primary can revert to its
147+
default state. Because the mapper can be invoked for an older version of a resource, keep your
148+
implementation a pure function of the resource passed to it.
149+
142150
Adding a `SecondaryToPrimaryMapper` is typically sufficient when there is a one-to-many relationship
143151
between primary and secondary resources. The secondary resources can be mapped to its primary
144152
owner, and this is enough information to also get these secondary resources from the `Context`

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/SecondaryToPrimaryMapper.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@
2626
*/
2727
@FunctionalInterface
2828
public interface SecondaryToPrimaryMapper<R> {
29+
2930
/**
30-
* @param resource - secondary
31-
* @return set of primary resource IDs
31+
* Maps a secondary resource to the set of primary resources that should be reconciled in
32+
* response.
33+
*
34+
* @param resource the secondary resource for which an event was received
35+
* @return set of primary resource IDs to enqueue for reconciliation; an empty set means the event
36+
* is irrelevant and no reconciliation is triggered. On update events, this method is invoked
37+
* for both the old and the new versions of the resource.
3238
*/
3339
Set<ResourceID> toPrimaryResourceIDs(R resource);
3440
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerEventSource.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ public synchronized void start() {
8686

8787
@Override
8888
protected synchronized void handleEvent(
89-
ResourceAction action, T resource, T oldResource, Boolean deletedFinalStateUnknown) {
89+
ResourceAction action,
90+
T resource,
91+
T oldResource,
92+
Boolean deletedFinalStateUnknown,
93+
// not relevant for controller event source
94+
Set<ResourceID> relatedPrimaryIDs) {
9095
try {
9196
if (log.isDebugEnabled()) {
9297
log.debug("Event received with action: {}", action);
@@ -162,7 +167,8 @@ private void handleEvent(ExtendedResourceEvent r) {
162167
r.getAction(),
163168
(T) r.getResource().orElseThrow(),
164169
(T) r.getPreviousResource().orElse(null),
165-
r.isLastStateUnknown());
170+
r.isLastStateUnknown(),
171+
null);
166172
}
167173

168174
@Override

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,42 @@ public DefaultPrimaryToSecondaryIndex(SecondaryToPrimaryMapper<R> secondaryToPri
3232
}
3333

3434
@Override
35-
public synchronized void onAddOrUpdate(R resource) {
35+
public synchronized Set<ResourceID> onAddOrUpdate(R resource, R oldResource) {
36+
3637
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
38+
39+
var secondaryId = ResourceID.fromResource(resource);
40+
3741
primaryResources.forEach(
3842
primaryResource -> {
3943
var resourceSet =
4044
index.computeIfAbsent(primaryResource, pr -> ConcurrentHashMap.newKeySet());
41-
resourceSet.add(ResourceID.fromResource(resource));
45+
resourceSet.add(secondaryId);
4246
});
47+
48+
if (oldResource != null) {
49+
var obsoletePrimaries =
50+
new HashSet<>(secondaryToPrimaryMapper.toPrimaryResourceIDs(oldResource));
51+
if (!primaryResources.containsAll(obsoletePrimaries)) {
52+
var result = new HashSet<>(primaryResources);
53+
obsoletePrimaries.removeAll(primaryResources);
54+
obsoletePrimaries.forEach(
55+
p ->
56+
index.computeIfPresent(
57+
p,
58+
(id, currentSet) -> {
59+
currentSet.remove(secondaryId);
60+
return currentSet.isEmpty() ? null : currentSet;
61+
}));
62+
result.addAll(obsoletePrimaries);
63+
return result;
64+
}
65+
}
66+
return primaryResources;
4367
}
4468

4569
@Override
46-
public synchronized void onDelete(R resource) {
70+
public synchronized Set<ResourceID> onDelete(R resource) {
4771
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
4872
primaryResources.forEach(
4973
primaryResource -> {
@@ -58,6 +82,7 @@ public synchronized void onDelete(R resource) {
5882
}
5983
}
6084
});
85+
return primaryResources;
6186
}
6287

6388
@Override

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

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.informer;
1717

18+
import java.util.HashSet;
1819
import java.util.Optional;
1920
import java.util.Set;
2021
import java.util.stream.Collectors;
@@ -127,17 +128,21 @@ public synchronized void onDelete(R resource, boolean deletedFinalStateUnknown)
127128
if (resultEvent.isEmpty()) {
128129
return;
129130
}
130-
primaryToSecondaryIndex.onDelete(resource);
131+
var primaryIds = primaryToSecondaryIndex.onDelete(resource);
131132
if (eventAcceptedByFilter(
132133
ResourceAction.DELETED, resource, null, deletedFinalStateUnknown)) {
133-
propagateEvent(resource);
134+
propagateEvent(resource, null, primaryIds);
134135
}
135136
});
136137
}
137138

138139
@Override
139140
protected void handleEvent(
140-
ResourceAction action, R resource, R oldResource, Boolean deletedFinalStateUnknown) {
141+
ResourceAction action,
142+
R resource,
143+
R oldResource,
144+
Boolean deletedFinalStateUnknown,
145+
Set<ResourceID> relatedPrimaryIds) {
141146
// Called from ManagedInformerEventSource#eventFilteringUpdateAndCacheResource after the temp
142147
// cache decided to surface a (possibly synthesized) event. The user-level filters
143148
// (onAdd/onUpdate/onDelete/genericFilter) still apply, so this path mirrors the direct
@@ -148,7 +153,7 @@ protected void handleEvent(
148153
log.debug(
149154
"handleEvent: removing from primaryToSecondaryIndex. id={}",
150155
ResourceID.fromResource(resource));
151-
primaryToSecondaryIndex.onDelete(resource);
156+
relatedPrimaryIds = primaryToSecondaryIndex.onDelete(resource);
152157
}
153158
if (!eventAcceptedByFilter(action, resource, oldResource, deletedFinalStateUnknown)) {
154159
if (log.isDebugEnabled()) {
@@ -166,7 +171,7 @@ protected void handleEvent(
166171
action,
167172
resource.getMetadata().getResourceVersion());
168173
}
169-
propagateEvent(resource);
174+
propagateEvent(resource, oldResource, relatedPrimaryIds);
170175
}
171176

172177
@Override
@@ -177,12 +182,12 @@ public synchronized void start() {
177182
super.start();
178183
// this makes sure that on first reconciliation all resources are
179184
// present on the index
180-
manager().list().forEach(primaryToSecondaryIndex::onAddOrUpdate);
185+
manager().list().forEach(r -> primaryToSecondaryIndex.onAddOrUpdate(r, null));
181186
}
182187

183188
@SuppressWarnings("unchecked")
184189
private synchronized void onAddOrUpdate(ResourceAction action, R newObject, R oldObject) {
185-
primaryToSecondaryIndex.onAddOrUpdate(newObject);
190+
var primaryIds = primaryToSecondaryIndex.onAddOrUpdate(newObject, oldObject);
186191
var resourceID = ResourceID.fromResource(newObject);
187192

188193
var resultEvent = temporaryResourceCache.onAddOrUpdateEvent(action, newObject, oldObject);
@@ -194,15 +199,22 @@ private synchronized void onAddOrUpdate(ResourceAction action, R newObject, R ol
194199
"Propagating event for {}, resource with same version not result of a our update.",
195200
action);
196201
var event = resultEvent.get();
197-
propagateEvent((R) event.getResource().orElseThrow());
202+
propagateEvent((R) event.getResource().orElseThrow(), oldObject, primaryIds);
198203
} else {
199204
log.debug("Event filtered out for operation: {}, resourceID: {}", action, resourceID);
200205
}
201206
}
202207

203-
protected void propagateEvent(R object) {
204-
var primaryResourceIdSet =
205-
configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(object);
208+
protected void propagateEvent(R resource, R oldResource, Set<ResourceID> primaryResourceIdSet) {
209+
if (primaryResourceIdSet == null) {
210+
primaryResourceIdSet = new HashSet<>();
211+
primaryResourceIdSet.addAll(
212+
configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(resource));
213+
if (oldResource != null) {
214+
primaryResourceIdSet.addAll(
215+
configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(oldResource));
216+
}
217+
}
206218
if (primaryResourceIdSet.isEmpty()) {
207219
return;
208220
}
@@ -249,17 +261,24 @@ public Set<R> getSecondaryResources(P primary) {
249261
@Override
250262
public void handleRecentResourceUpdate(
251263
ResourceID resourceID, R resource, R previousVersionOfResource) {
252-
handleRecentCreateOrUpdate(resource);
264+
handleRecentCreateOrUpdate(resource, previousVersionOfResource);
253265
}
254266

255267
@Override
256268
public void handleRecentResourceCreate(ResourceID resourceID, R resource) {
257-
handleRecentCreateOrUpdate(resource);
269+
handleRecentCreateOrUpdate(resource, null);
270+
}
271+
272+
@Override
273+
protected Set<ResourceID> cacheUpdateAndGetRelatedPrimaryIDs(
274+
R updatedResource, R previousResource) {
275+
return handleRecentCreateOrUpdate(updatedResource, previousResource);
258276
}
259277

260-
private void handleRecentCreateOrUpdate(R newResource) {
261-
primaryToSecondaryIndex.onAddOrUpdate(newResource);
278+
private Set<ResourceID> handleRecentCreateOrUpdate(R newResource, R previousVersion) {
279+
var relatedPrimaryIds = primaryToSecondaryIndex.onAddOrUpdate(newResource, previousVersion);
262280
temporaryResourceCache.putResource(newResource);
281+
return relatedPrimaryIds;
263282
}
264283

265284
private boolean useSecondaryToPrimaryIndex() {

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

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.javaoperatorsdk.operator.processing.event.source.informer;
1717

18+
import java.util.Collections;
1819
import java.util.HashMap;
1920
import java.util.List;
2021
import java.util.Map;
@@ -97,38 +98,48 @@ public R eventFilteringUpdateAndCacheResource(R resourceToUpdate, UnaryOperator<
9798
ResourceID id = ResourceID.fromResource(resourceToUpdate);
9899
log.debug("Starting event filtering and caching update for id={}", id);
99100
R updatedResource = null;
101+
Set<ResourceID> relatedPrimaryIds = null;
100102
try {
101103
temporaryResourceCache.startEventFilteringModify(id);
102104
updatedResource = updateMethod.apply(resourceToUpdate);
103-
handleRecentResourceUpdate(id, updatedResource, resourceToUpdate);
105+
relatedPrimaryIds = cacheUpdateAndGetRelatedPrimaryIDs(updatedResource, resourceToUpdate);
104106
log.debug(
105107
"Caching resource update successful. id={}, rv={}",
106108
id,
107109
updatedResource.getMetadata().getResourceVersion());
108110
return updatedResource;
109111
} finally {
110112
var res = temporaryResourceCache.doneEventFilterModify(id);
111-
res.ifPresentOrElse(
112-
r -> {
113-
log.debug(
114-
"Propagating not own event after filtering update. id={}, action={}, rv={}",
115-
id,
116-
r.getAction(),
117-
r.getResource()
118-
.map(rr -> rr.getMetadata().getResourceVersion())
119-
.orElse("[not set]"));
120-
handleEvent(
121-
r.getAction(),
122-
(R) r.getResource().orElseThrow(),
123-
(R) r.getPreviousResource().orElse(null),
124-
r.isLastStateUnknown());
125-
},
126-
() -> log.debug("No new event present after the filtering update. id={}", id));
113+
if (res.isPresent()) {
114+
var event = res.orElseThrow();
115+
if (log.isDebugEnabled()) {
116+
log.debug(
117+
"Propagating not own event after filtering update. id={}, action={}, rv={}",
118+
id,
119+
event.getAction(),
120+
event
121+
.getResource()
122+
.map(rr -> rr.getMetadata().getResourceVersion())
123+
.orElse("[not set]"));
124+
}
125+
handleEvent(
126+
event.getAction(),
127+
(R) event.getResource().orElseThrow(),
128+
(R) event.getPreviousResource().orElse(null),
129+
event.isLastStateUnknown(),
130+
relatedPrimaryIds);
131+
} else {
132+
log.debug("No new event present after the filtering update. id={}", id);
133+
}
127134
}
128135
}
129136

130137
protected abstract void handleEvent(
131-
ResourceAction action, R resource, R oldResource, Boolean deletedFinalStateUnknown);
138+
ResourceAction action,
139+
R resource,
140+
R oldResource,
141+
Boolean deletedFinalStateUnknown,
142+
Set<ResourceID> relatedPrimaryIDs);
132143

133144
@SuppressWarnings("unchecked")
134145
@Override
@@ -175,6 +186,20 @@ public void handleRecentResourceCreate(ResourceID resourceID, R resource) {
175186
temporaryResourceCache.putResource(resource);
176187
}
177188

189+
/**
190+
* Caches the resource updated through {@link #eventFilteringUpdateAndCacheResource} and returns
191+
* the primary resource IDs related to that update, so they can be propagated to {@link
192+
* #handleEvent}. The base implementation just fills the temporary cache and reports no related
193+
* primaries. Subclasses that maintain a primary-to-secondary index override this to surface the
194+
* affected primaries even after the secondary's references have changed, keeping that concern
195+
* internal to those event sources instead of leaking it into {@link RecentOperationCacheFiller}.
196+
*/
197+
protected Set<ResourceID> cacheUpdateAndGetRelatedPrimaryIDs(
198+
R updatedResource, R previousResource) {
199+
handleRecentResourceUpdate(null, updatedResource, previousResource);
200+
return Collections.emptySet();
201+
}
202+
178203
@Override
179204
public Optional<R> get(ResourceID resourceID) {
180205
// The order of reading from these caches matters

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@ public static <T extends HasMetadata> NOOPPrimaryToSecondaryIndex<T> getInstance
3333
private NOOPPrimaryToSecondaryIndex() {}
3434

3535
@Override
36-
public void onAddOrUpdate(R resource) {
37-
// empty method because of noop implementation
36+
public Set<ResourceID> onAddOrUpdate(R resource, R oldResource) {
37+
return null;
3838
}
3939

4040
@Override
41-
public void onDelete(R resource) {
41+
public Set<ResourceID> onDelete(R resource) {
4242
// empty method because of noop implementation
43+
return null;
4344
}
4445

4546
@Override

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222

2323
public interface PrimaryToSecondaryIndex<R extends HasMetadata> {
2424

25-
void onAddOrUpdate(R resource);
25+
Set<ResourceID> onAddOrUpdate(R resource, R oldResource);
2626

27-
void onDelete(R resource);
27+
Set<ResourceID> onDelete(R resource);
2828

2929
Set<ResourceID> getSecondaryResources(ResourceID primary);
3030
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ public synchronized void checkGhostResources() {
244244
log.debug("Removing ghost resource with ID: {}", e.getKey());
245245
iterator.remove();
246246
eventFilteringSupport.handleGhostResourceRemoval(e.getKey());
247-
managedInformerEventSource.handleEvent(ResourceAction.DELETED, e.getValue(), null, true);
247+
managedInformerEventSource.handleEvent(
248+
ResourceAction.DELETED, e.getValue(), null, true, null);
248249
}
249250
}
250251
}

0 commit comments

Comments
 (0)