2727import org .apache .flink .util .Preconditions ;
2828
2929import io .fabric8 .kubernetes .api .model .ConfigMap ;
30+ import io .fabric8 .kubernetes .client .KubernetesClientException ;
3031import io .fabric8 .kubernetes .client .informers .ResourceEventHandler ;
3132import org .slf4j .Logger ;
3233import org .slf4j .LoggerFactory ;
@@ -46,11 +47,18 @@ abstract class GateProcessFunction<I> extends ProcessFunction<I, I> implements S
4647
4748 protected final BlueGreenDeploymentType blueGreenDeploymentType ;
4849
49- // TODO: make this configurable? This cannot be a constant
50- protected final int subtaskIndexGuide = 1 ;
50+ // How long (ms) to wait after a write condition is first detected before writing to the
51+ // ConfigMap. This window gives other subtasks a chance to observe the write via the informer,
52+ // reducing duplicate K8s API calls.
53+ private static final long WRITE_DEDUP_DELAY_MS = 500L ;
54+
55+ // Processing time (ms) when a pending write was first requested; -1 means no write pending.
56+ private long pendingWriteSince = -1L ;
57+ // Set by notifyClearToTeardown() so that maybePerformPendingWrite() performs the teardown
58+ // write even if the pending write was originally scheduled for a different operation.
59+ private boolean pendingClearToTeardown = false ;
5160
5261 private GateKubernetesService gateKubernetesService ;
53- private Boolean clearToTeardown = false ;
5462 protected GateContext baseContext ;
5563 private String namespace ;
5664 private String configMapName ;
@@ -79,6 +87,17 @@ public void open(Configuration parameters) throws Exception {
7987 processConfigMap (gateKubernetesService .parseConfigMap ());
8088 }
8189
90+ /**
91+ * Records the current processing time as the start of a pending write window. No-op if a write
92+ * is already pending. The actual write is performed by {@link #maybePerformPendingWrite} on the
93+ * next element processed after the dedup delay has elapsed.
94+ */
95+ protected final void scheduleWriteTimer (Context ctx ) {
96+ if (pendingWriteSince < 0 ) {
97+ pendingWriteSince = ctx .timerService ().currentProcessingTime ();
98+ }
99+ }
100+
82101 protected abstract void processElementActive (
83102 I value , ProcessFunction <I , I >.Context ctx , Collector <I > out )
84103 throws IllegalAccessException ;
@@ -89,7 +108,8 @@ protected abstract void processElementStandby(
89108
90109 @ Override
91110 public void processElement (I value , ProcessFunction <I , I >.Context ctx , Collector <I > out )
92- throws IllegalAccessException {
111+ throws Exception {
112+ maybePerformPendingWrite (ctx );
93113 switch (baseContext .getOutputMode ()) {
94114 case ACTIVE :
95115 processElementActive (value , ctx , out );
@@ -104,6 +124,29 @@ public void processElement(I value, ProcessFunction<I, I>.Context ctx, Collector
104124 }
105125 }
106126
127+ private void maybePerformPendingWrite (Context ctx ) throws Exception {
128+ if (pendingWriteSince < 0 ) {
129+ return ;
130+ }
131+ long now = ctx .timerService ().currentProcessingTime ();
132+ if (now - pendingWriteSince < WRITE_DEDUP_DELAY_MS ) {
133+ return ;
134+ }
135+ pendingWriteSince = -1L ;
136+ boolean handled = handleScheduledWrite (ctx );
137+ if (!handled && pendingClearToTeardown ) {
138+ pendingClearToTeardown = false ;
139+ if (baseContext .getGateStage () != CLEAR_TO_TEARDOWN ) {
140+ logInfo ("Writing " + CLEAR_TO_TEARDOWN + " to ConfigMap" );
141+ gateKubernetesService .updateConfigMapEntries (
142+ Map .of (TRANSITION_STAGE .getLabel (), CLEAR_TO_TEARDOWN .toString ()));
143+ logInfo (CLEAR_TO_TEARDOWN + " set!" );
144+ } else {
145+ logInfo (CLEAR_TO_TEARDOWN + " already set, skipping" );
146+ }
147+ }
148+ }
149+
107150 private void setKubernetesEnvironment () {
108151 this .gateKubernetesService = new GateKubernetesService (namespace , configMapName );
109152
@@ -161,19 +204,22 @@ private void processConfigMap(ConfigMap configMap) {
161204 onContextUpdate (baseContext , filteredData );
162205 }
163206
164- protected final void notifyClearToTeardown () {
165- // Notify only once
166- if (!clearToTeardown && getRuntimeContext ().getIndexOfThisSubtask () == subtaskIndexGuide ) {
167- logInfo ("Setting " + CLEAR_TO_TEARDOWN );
168- gateKubernetesService .updateConfigMapEntries (
169- Map .of (TRANSITION_STAGE .getLabel (), CLEAR_TO_TEARDOWN .toString ()));
170- logInfo (CLEAR_TO_TEARDOWN + " set!" );
171- this .clearToTeardown = true ;
172- }
207+ protected final void notifyClearToTeardown (Context ctx ) {
208+ pendingClearToTeardown = true ;
209+ scheduleWriteTimer (ctx );
210+ }
211+
212+ /**
213+ * Hook for subclasses to handle watermark-specific ConfigMap writes when the dedup window
214+ * elapses. Returns {@code true} if the write was handled (base-class CLEAR_TO_TEARDOWN logic is
215+ * skipped), {@code false} to fall through.
216+ */
217+ protected boolean handleScheduledWrite (Context ctx ) throws Exception {
218+ return false ;
173219 }
174220
175221 protected final void updateConfigMapCustomEntries (Map <String , String > customEntries )
176- throws IllegalAccessException {
222+ throws Exception {
177223 // Validating only "custom" entries/keys can be updated
178224 var keysToUpdate = customEntries .keySet ();
179225 var baseContextKeys =
@@ -189,7 +235,22 @@ protected final void updateConfigMapCustomEntries(Map<String, String> customEntr
189235 throw new IllegalAccessException (error );
190236 }
191237 logInfo ("Updating custom entries: " + customEntries );
192- gateKubernetesService .updateConfigMapEntries (customEntries );
238+ int maxRetries = 3 ;
239+ for (int attempt = 0 ; attempt <= maxRetries ; attempt ++) {
240+ try {
241+ gateKubernetesService .updateConfigMapEntries (customEntries );
242+ return ;
243+ } catch (KubernetesClientException e ) {
244+ if (e .getCode () == 409 && attempt < maxRetries ) {
245+ logInfo (
246+ "ConfigMap update conflict on attempt "
247+ + (attempt + 1 )
248+ + ", retrying..." );
249+ } else {
250+ throw e ;
251+ }
252+ }
253+ }
193254 }
194255
195256 // Temporary "utility" function for development
0 commit comments