Skip to content

Commit 2293e63

Browse files
committed
improve: extend SecondaryToPrimaryMapper so it also gets the old resource
This might needed in some corner cases where might help with optimizations to which resource to trigger. Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 417fb0d commit 2293e63

7 files changed

Lines changed: 76 additions & 15 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,25 @@ rare corner cases. Returning an empty set means that the mapper considered the s
135135
resource event as irrelevant and the SDK will thus not trigger a reconciliation of the primary
136136
resource in that situation.
137137

138+
`SecondaryToPrimaryMapper` exposes two methods:
139+
140+
- `toPrimaryResourceIDs(R resource)` — the original mapper. Implementing it is sufficient for
141+
the vast majority of use cases.
142+
- `toPrimaryResourceIDs(R newResource, R oldResource)` — a variant that is the one actually
143+
invoked by the SDK on every secondary event. Its default implementation delegates to the
144+
single-argument method, so existing mappers keep working unchanged.
145+
146+
Override the two-argument variant only in edge cases where the set of primary resources to
147+
reconcile depends on what changed between the previous and the new version of the secondary
148+
resource (e.g. a reference that moved from one primary to another, where both primaries need
149+
to be reconciled). **Use it with caution:** `oldResource` is sourced from the informer cache and
150+
is only populated for genuine update events observed while the controller is already running.
151+
On controller startup the cache is empty, so the initial events received for resources that
152+
already exist in the cluster are delivered as adds with `oldResource == null` — even if those
153+
resources had been updated before the operator came up. `oldResource` is also `null` for delete
154+
events and for events triggered through the primary-to-secondary index. Implementations must
155+
therefore handle a `null` `oldResource` gracefully.
156+
138157
Adding a `SecondaryToPrimaryMapper` is typically sufficient when there is a one-to-many relationship
139158
between primary and secondary resources. The secondary resources can be mapped to its primary
140159
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: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,48 @@
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. Implementing this single-argument form is sufficient for the vast majority of use
33+
* cases — prefer it unless you specifically need access to the previous version of the
34+
* secondary resource (see {@link #toPrimaryResourceIDs(Object, Object)}).
35+
*
36+
* @param resource the secondary resource for which an event was received
37+
* @return set of primary resource IDs to enqueue for reconciliation; an empty set means the
38+
* event is irrelevant and no reconciliation is triggered
3239
*/
3340
Set<ResourceID> toPrimaryResourceIDs(R resource);
41+
42+
/**
43+
* Variant invoked by the framework for every secondary resource event, providing both the new and
44+
* the previous version of the resource (when available). The default implementation simply
45+
* delegates to {@link #toPrimaryResourceIDs(Object)} and ignores {@code oldResource}, so existing
46+
* mappers keep working unchanged.
47+
*
48+
* <p>Override this method only for edge cases where the set of primary resources to reconcile
49+
* depends on what changed between the old and the new version of the secondary resource (for
50+
* example, when a reference held by the secondary resource has moved from one primary to another
51+
* and both primaries need to be reconciled).
52+
*
53+
* <p><strong>Use with caution.</strong> {@code oldResource} is sourced from the informer cache
54+
* and is therefore only populated for genuine update events observed while the controller is
55+
* already running. In particular, when the controller starts up, the cache is empty and the
56+
* initial events received for resources that already existed in the cluster are delivered as adds
57+
* with {@code oldResource == null} (even if those resources had been updated previously). {@code
58+
* oldResource} is also {@code null} for delete events and for events triggered through the
59+
* primary-to-secondary index.
60+
*
61+
* <p>Implementations must therefore handle a {@code null} {@code oldResource} gracefully and not
62+
* rely on it being present for correctness — overriding this method is intended for edge cases
63+
* only.
64+
*
65+
* @param newResource the current version of the secondary resource
66+
* @param oldResource the previous version of the secondary resource, or {@code null} if not
67+
* available (see above)
68+
* @return set of primary resource IDs to enqueue for reconciliation
69+
*/
70+
default Set<ResourceID> toPrimaryResourceIDs(R newResource, R oldResource) {
71+
return toPrimaryResourceIDs(newResource);
72+
}
3473
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public DefaultPrimaryToSecondaryIndex(SecondaryToPrimaryMapper<R> secondaryToPri
3333

3434
@Override
3535
public synchronized void onAddOrUpdate(R resource) {
36-
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
36+
Set<ResourceID> primaryResources =
37+
secondaryToPrimaryMapper.toPrimaryResourceIDs(resource, null);
3738
primaryResources.forEach(
3839
primaryResource -> {
3940
var resourceSet =
@@ -44,7 +45,8 @@ public synchronized void onAddOrUpdate(R resource) {
4445

4546
@Override
4647
public synchronized void onDelete(R resource) {
47-
Set<ResourceID> primaryResources = secondaryToPrimaryMapper.toPrimaryResourceIDs(resource);
48+
Set<ResourceID> primaryResources =
49+
secondaryToPrimaryMapper.toPrimaryResourceIDs(resource, null);
4850
primaryResources.forEach(
4951
primaryResource -> {
5052
var secondaryResources = index.get(primaryResource);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ public synchronized void onDelete(R resource, boolean deletedFinalStateUnknown)
126126
primaryToSecondaryIndex.onDelete(resource);
127127
temporaryResourceCache.onDeleteEvent(resource, deletedFinalStateUnknown);
128128
if (acceptedByDeleteFilters(resource, deletedFinalStateUnknown)) {
129-
propagateEvent(resource);
129+
propagateEvent(resource, null);
130130
}
131131
});
132132
}
133133

134134
@Override
135135
protected void handleEvent(
136136
ResourceAction action, R resource, R oldResource, Boolean deletedFinalStateUnknown) {
137-
propagateEvent(resource);
137+
propagateEvent(resource, oldResource);
138138
}
139139

140140
@Override
@@ -161,15 +161,15 @@ private synchronized void onAddOrUpdate(ResourceAction action, R newObject, R ol
161161
log.debug(
162162
"Propagating event for {}, resource with same version not result of a reconciliation.",
163163
action);
164-
propagateEvent(newObject);
164+
propagateEvent(newObject, oldObject);
165165
} else {
166166
log.debug("Event filtered out for operation: {}, resourceID: {}", action, resourceID);
167167
}
168168
}
169169

170-
private void propagateEvent(R object) {
170+
private void propagateEvent(R resource, R oldResource) {
171171
var primaryResourceIdSet =
172-
configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(object);
172+
configuration().getSecondaryToPrimaryMapper().toPrimaryResourceIDs(resource, oldResource);
173173
if (primaryResourceIdSet.isEmpty()) {
174174
return;
175175
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ void setup() {
8787
SecondaryToPrimaryMapper secondaryToPrimaryMapper = mock(SecondaryToPrimaryMapper.class);
8888
when(informerEventSourceConfiguration.getSecondaryToPrimaryMapper())
8989
.thenReturn(secondaryToPrimaryMapper);
90-
when(secondaryToPrimaryMapper.toPrimaryResourceIDs(any()))
90+
when(secondaryToPrimaryMapper.toPrimaryResourceIDs(any(), any()))
9191
.thenReturn(Set.of(ResourceID.fromResource(testDeployment())));
9292
when(informerEventSourceConfiguration.getInformerConfig()).thenReturn(informerConfig);
9393

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/MappersTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ void secondaryToPrimaryMapperFromOwnerReference() {
4040
var secondary = getConfigMap(primary);
4141
secondary.addOwnerReference(primary);
4242

43-
var res = Mappers.fromOwnerReferences(TestCustomResource.class).toPrimaryResourceIDs(secondary);
43+
var res =
44+
Mappers.fromOwnerReferences(TestCustomResource.class).toPrimaryResourceIDs(secondary, null);
4445

4546
assertThat(res).contains(ResourceID.fromResource(primary));
4647
}
@@ -65,7 +66,7 @@ void secondaryToPrimaryMapperFromOwnerReferenceWhereGroupIdIsEmpty() {
6566
.build();
6667
secondary.addOwnerReference(primary);
6768

68-
var res = Mappers.fromOwnerReferences(ConfigMap.class).toPrimaryResourceIDs(secondary);
69+
var res = Mappers.fromOwnerReferences(ConfigMap.class).toPrimaryResourceIDs(secondary, null);
6970

7071
assertThat(res).contains(ResourceID.fromResource(primary));
7172
}
@@ -79,7 +80,7 @@ void secondaryToPrimaryMapperFromOwnerReferenceFiltersByType() {
7980

8081
var res =
8182
Mappers.fromOwnerReferences(TestCustomResourceOtherV1.class)
82-
.toPrimaryResourceIDs(secondary);
83+
.toPrimaryResourceIDs(secondary, null);
8384

8485
assertThat(res).isEmpty();
8586
}
@@ -103,7 +104,7 @@ void fromOwnerReferenceIgnoresVersionFromApiVersion() {
103104
HasMetadata.getGroup(TestCustomResource.class) + "/v2",
104105
HasMetadata.getKind(TestCustomResource.class),
105106
false)
106-
.toPrimaryResourceIDs(secondary);
107+
.toPrimaryResourceIDs(secondary, null);
107108

108109
assertThat(res).contains(ResourceID.fromResource(primary));
109110
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/PrimaryToSecondaryIndexTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class PrimaryToSecondaryIndexTest {
4646

4747
@BeforeEach
4848
void setup() {
49-
when(secondaryToPrimaryMapperMock.toPrimaryResourceIDs(any()))
49+
when(secondaryToPrimaryMapperMock.toPrimaryResourceIDs(any(), any()))
5050
.thenReturn(Set.of(primaryID1, primaryID2));
5151
}
5252

0 commit comments

Comments
 (0)