Skip to content

Commit 5325dbf

Browse files
committed
improve: using fabric8 re-list callback to cover event filtering edge cases (#3448)
Fabric8 client now supports callback onBeforeList in Informers, that allows to enhance edge case support for event filtering: Since on re-list some events could be lost, we will trigger the reconciliation in such scenarios. Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 8b6b9e0 commit 5325dbf

6 files changed

Lines changed: 84 additions & 50 deletions

File tree

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,14 @@ public synchronized void stop() {
155155

156156
@Override
157157
public void onList(String resourceVersion, boolean remainedEmpty) {
158-
// re-list supported by fabric8 client https://github.com/fabric8io/kubernetes-client/pull/7899
159-
// temporaryResourceCache.setRelistFinished(resourceVersion);
158+
temporaryResourceCache.setRelistFinished();
160159
temporaryResourceCache.checkGhostResources();
161160
}
162161

163-
// @Override (enable when
164-
// re-list supported by fabric8 client https://github.com/fabric8io/kubernetes-client/pull/7899
165-
// public void onBeforeList(String lastSyncResourceVersion) {
166-
// temporaryResourceCache.setOngoingRelist(lastSyncResourceVersion);
167-
// }
162+
@Override
163+
public void onBeforeList(String lastSyncResourceVersion) {
164+
temporaryResourceCache.setOngoingRelist();
165+
}
168166

169167
@Override
170168
public void handleRecentResourceUpdate(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,11 +265,11 @@ EventFilterSupport getEventFilterSupport() {
265265
return eventFilteringSupport;
266266
}
267267

268-
public void setOngoingRelist(String lastKnownSyncVersion) {
268+
public void setOngoingRelist() {
269269
eventFilteringSupport.setStartingReList();
270270
}
271271

272-
public void setRelistFinished(String syncResourceVersions) {
272+
public void setRelistFinished() {
273273
eventFilteringSupport.setRelistFinished();
274274
}
275275
}

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

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

18-
import org.junit.jupiter.api.Disabled;
1918
import org.junit.jupiter.api.Test;
2019

2120
import io.fabric8.kubernetes.api.model.ConfigMap;
@@ -385,25 +384,6 @@ void additionalEventAndDeleteEvent() {
385384
assertThat(eventFilterWindow.canBeRemoved()).isTrue();
386385
}
387386

388-
@Test
389-
@Disabled("should be part of event filter support")
390-
void additionalEventAndDeleteEventNoUpdate() {
391-
eventFilterWindow.increaseActiveUpdates();
392-
eventFilterWindow.addToOwnUpdateVersions(s(FIRST_OWN_VERSION));
393-
eventFilterWindow.addRelatedEvent(updateEvent(FIRST_OWN_VERSION));
394-
eventFilterWindow.addRelatedEvent(updateEvent(FIRST_OWN_VERSION + 1));
395-
eventFilterWindow.addRelatedEvent(deleteEvent(FIRST_OWN_VERSION + 2));
396-
397-
assertThat(eventFilterWindow.check())
398-
.hasValueSatisfying(e -> assertDeleteEvent(e, FIRST_OWN_VERSION + 2));
399-
assertThat(eventFilterWindow.check()).isEmpty();
400-
401-
assertEmptyState();
402-
eventFilterWindow.decreaseActiveUpdates();
403-
404-
assertThat(eventFilterWindow.canBeRemoved()).isTrue();
405-
}
406-
407387
@Test
408388
void deleteEventInMiddleTwoUpdates() {
409389
eventFilterWindow.increaseActiveUpdates();

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/readcacheafterwrite/onrelistfilter/OnRelistFilterIT.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.time.Duration;
1919

20-
import org.junit.jupiter.api.Disabled;
2120
import org.junit.jupiter.api.Test;
2221
import org.junit.jupiter.api.extension.RegisterExtension;
2322

@@ -37,7 +36,6 @@
3736
* <li>re-list starts WHILE the update window is open — own write is propagated
3837
* </ul>
3938
*/
40-
@Disabled("enable when fabric8 supports relist")
4139
class OnRelistFilterIT {
4240

4341
static final String RESOURCE_NAME = "test-resource";

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/readcacheafterwrite/onrelistfilter/OnRelistFilterReconciler.java

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
*/
1616
package io.javaoperatorsdk.operator.baseapi.readcacheafterwrite.onrelistfilter;
1717

18+
import java.time.Duration;
1819
import java.util.List;
1920
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.ConcurrentMap;
2023
import java.util.concurrent.atomic.AtomicInteger;
2124
import java.util.concurrent.atomic.AtomicReference;
2225

@@ -32,6 +35,7 @@
3235
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
3336
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
3437
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
38+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
3539
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
3640
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
3741

@@ -78,7 +82,13 @@ public UpdateControl<OnRelistFilterCustomResource> reconcile(
7882
case NO_RELIST -> context.resourceOperations().serverSideApply(cm, configMapEventSource);
7983
case RELIST_AROUND_UPDATE -> {
8084
configMapEventSource.simulateOnBeforeList();
81-
context.resourceOperations().serverSideApply(cm, configMapEventSource);
85+
var applied = context.resourceOperations().serverSideApply(cm, configMapEventSource);
86+
// Make the simulation deterministic: the own-write watch event arrives asynchronously,
87+
// so we must wait for it to be received (and buffered into the still-open re-list
88+
// window, where it is tagged as part of the re-list) BEFORE the re-list finishes.
89+
// Otherwise onList may clear the window's re-list flag before the event lands and the
90+
// event would be filtered as an own write — the race this test originally flaked on.
91+
configMapEventSource.awaitWatchEventReceived(applied);
8292
configMapEventSource.simulateOnList();
8393
}
8494
case RELIST_COMPLETES_BEFORE_UPDATE -> {
@@ -90,20 +100,24 @@ public UpdateControl<OnRelistFilterCustomResource> reconcile(
90100
// Drive the event-filtering update path manually so we can fire onBeforeList AFTER the
91101
// window has been opened by startEventFilteringModify but BEFORE the SSA hits the API.
92102
var fieldManager = context.getControllerConfiguration().fieldManager();
93-
configMapEventSource.eventFilteringUpdateAndCacheResource(
94-
cm,
95-
r -> {
96-
configMapEventSource.simulateOnBeforeList();
97-
return context
98-
.getClient()
99-
.resource(r)
100-
.patch(
101-
new PatchContext.Builder()
102-
.withForce(true)
103-
.withFieldManager(fieldManager)
104-
.withPatchType(PatchType.SERVER_SIDE_APPLY)
105-
.build());
106-
});
103+
var applied =
104+
configMapEventSource.eventFilteringUpdateAndCacheResource(
105+
cm,
106+
r -> {
107+
configMapEventSource.simulateOnBeforeList();
108+
return context
109+
.getClient()
110+
.resource(r)
111+
.patch(
112+
new PatchContext.Builder()
113+
.withForce(true)
114+
.withFieldManager(fieldManager)
115+
.withPatchType(PatchType.SERVER_SIDE_APPLY)
116+
.build());
117+
});
118+
// See RELIST_AROUND_UPDATE: wait for the own-write event to be buffered while the
119+
// re-list is still in progress, so it is tagged as part of the re-list and propagated.
120+
configMapEventSource.awaitWatchEventReceived(applied);
107121
configMapEventSource.simulateOnList();
108122
}
109123
}
@@ -154,14 +168,60 @@ private static ConfigMap prepareConfigMap(OnRelistFilterCustomResource p) {
154168
static class RelistAwareInformerEventSource<R extends HasMetadata, P extends HasMetadata>
155169
extends InformerEventSource<R, P> {
156170

171+
// Highest resourceVersion the informer has actually delivered (as a watch event) per resource.
172+
// Lets a test block until the event for its own write has been received and processed.
173+
private final ConcurrentMap<ResourceID, Long> latestReceivedVersion = new ConcurrentHashMap<>();
174+
157175
RelistAwareInformerEventSource(
158176
InformerEventSourceConfiguration<R> configuration, EventSourceContext<P> context) {
159177
super(configuration, context);
160178
}
161179

180+
@Override
181+
public void onAdd(R newResource) {
182+
super.onAdd(newResource);
183+
recordReceived(newResource);
184+
}
185+
186+
@Override
187+
public void onUpdate(R oldResource, R newResource) {
188+
super.onUpdate(oldResource, newResource);
189+
recordReceived(newResource);
190+
}
191+
192+
private void recordReceived(R resource) {
193+
latestReceivedVersion.merge(
194+
ResourceID.fromResource(resource),
195+
Long.parseLong(resource.getMetadata().getResourceVersion()),
196+
Math::max);
197+
}
198+
199+
/**
200+
* Blocks until the informer has delivered a watch event for the given resource at a
201+
* resourceVersion at least as recent as the one supplied (i.e. our own write has come back
202+
* through the watch). Calling {@code super.onAdd/onUpdate} before recording guarantees the
203+
* event is already buffered in the event-filter window by the time this returns.
204+
*/
205+
void awaitWatchEventReceived(R resource) {
206+
var id = ResourceID.fromResource(resource);
207+
var target = Long.parseLong(resource.getMetadata().getResourceVersion());
208+
var deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos();
209+
while (latestReceivedVersion.getOrDefault(id, -1L) < target) {
210+
if (System.nanoTime() > deadline) {
211+
throw new IllegalStateException(
212+
"Timed out waiting for watch event with rv>=" + target + " for " + id);
213+
}
214+
try {
215+
Thread.sleep(20);
216+
} catch (InterruptedException e) {
217+
Thread.currentThread().interrupt();
218+
throw new IllegalStateException(e);
219+
}
220+
}
221+
}
222+
162223
void simulateOnBeforeList() {
163-
// uncomment when fabric8 supports re-list
164-
// onBeforeList(null);
224+
onBeforeList(null);
165225
}
166226

167227
void simulateOnList() {

operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/readcacheafterwrite/ownsecondaryupdate/OwnSecondaryUpdateIT.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import java.time.Duration;
1919

20-
import org.junit.jupiter.api.Disabled;
2120
import org.junit.jupiter.api.Test;
2221
import org.junit.jupiter.api.extension.RegisterExtension;
2322

@@ -33,7 +32,6 @@
3332
* the secondary are filtered and do NOT trigger additional reconciliations. Counterpart to {@code
3433
* ExternalSecondaryUpdateIT}, which asserts the opposite for third-party updates.
3534
*/
36-
@Disabled("enable if re-list notification supported by fabric8 client")
3735
class OwnSecondaryUpdateIT {
3836

3937
static final String RESOURCE_NAME = "test-resource";

0 commit comments

Comments
 (0)