1515 */
1616package io .javaoperatorsdk .operator .baseapi .readcacheafterwrite .onrelistfilter ;
1717
18+ import java .time .Duration ;
1819import java .util .List ;
1920import java .util .Map ;
21+ import java .util .concurrent .ConcurrentHashMap ;
22+ import java .util .concurrent .ConcurrentMap ;
2023import java .util .concurrent .atomic .AtomicInteger ;
2124import java .util .concurrent .atomic .AtomicReference ;
2225
3235import io .javaoperatorsdk .operator .api .reconciler .EventSourceContext ;
3336import io .javaoperatorsdk .operator .api .reconciler .Reconciler ;
3437import io .javaoperatorsdk .operator .api .reconciler .UpdateControl ;
38+ import io .javaoperatorsdk .operator .processing .event .ResourceID ;
3539import io .javaoperatorsdk .operator .processing .event .source .EventSource ;
3640import 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,11 +168,58 @@ 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 () {
163224 onBeforeList (null );
164225 }
0 commit comments