Skip to content

Commit dfa344c

Browse files
committed
Cleaning up/organizing the GateInjector* classes
1 parent 270be74 commit dfa344c

5 files changed

Lines changed: 403 additions & 0 deletions

File tree

flink-kubernetes-operator-bluegreen-client/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ under the License.
6060
<scope>provided</scope>
6161
</dependency>
6262

63+
<!-- Required by GateInjectorExecutor to detect SQL jobs via RowDataSerializer -->
64+
<dependency>
65+
<groupId>org.apache.flink</groupId>
66+
<artifactId>flink-table-runtime</artifactId>
67+
<version>${flink.version}</version>
68+
<optional>true</optional>
69+
</dependency>
70+
6371
<!-- Test dependencies -->
6472
<dependency>
6573
<groupId>org.junit.jupiter</groupId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.flink.kubernetes.operator.bluegreen.client;
19+
20+
/** Controls where the BlueGreen gate operator is injected into the StreamGraph. */
21+
public enum GateInjectionPosition {
22+
/** Inject immediately after the source node. Preferred for fan-out DAGs. */
23+
AFTER_SOURCE,
24+
/** Inject immediately before the sink node. Default. */
25+
BEFORE_SINK
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.flink.kubernetes.operator.bluegreen.client;
19+
20+
import org.apache.flink.api.common.typeinfo.TypeInformation;
21+
import org.apache.flink.api.dag.Pipeline;
22+
import org.apache.flink.api.java.typeutils.GenericTypeInfo;
23+
import org.apache.flink.configuration.Configuration;
24+
import org.apache.flink.core.execution.JobClient;
25+
import org.apache.flink.core.execution.PipelineExecutor;
26+
import org.apache.flink.streaming.api.graph.StreamEdge;
27+
import org.apache.flink.streaming.api.graph.StreamGraph;
28+
import org.apache.flink.streaming.api.graph.StreamNode;
29+
import org.apache.flink.streaming.api.operators.ProcessOperator;
30+
import org.apache.flink.streaming.api.operators.SimpleOperatorFactory;
31+
import org.apache.flink.table.data.RowData;
32+
import org.apache.flink.table.runtime.typeutils.RowDataSerializer;
33+
import org.apache.flink.util.InstantiationUtil;
34+
35+
import lombok.AllArgsConstructor;
36+
import org.slf4j.Logger;
37+
38+
import java.io.IOException;
39+
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.concurrent.CompletableFuture;
43+
import java.util.stream.Collectors;
44+
45+
/**
46+
* {@link PipelineExecutor} decorator that transparently injects a BlueGreen gate operator into the
47+
* {@link StreamGraph} before delegating to the wrapped executor.
48+
*
49+
* <p>Can be used programmatically (wrap your executor) or automatically via the {@code
50+
* flink-kubernetes-operator-bluegreen-agent}, which intercepts {@code
51+
* StreamExecutionEnvironment.execute(StreamGraph)} and calls {@link #injectGates} without any user
52+
* code changes.
53+
*/
54+
@AllArgsConstructor
55+
public class GateInjectorExecutor implements PipelineExecutor {
56+
57+
private final PipelineExecutor delegate;
58+
private final Configuration config;
59+
private final Logger logger;
60+
61+
@Override
62+
public CompletableFuture<JobClient> execute(
63+
Pipeline pipeline, Configuration config, ClassLoader classLoader) throws Exception {
64+
65+
if (pipeline instanceof StreamGraph) {
66+
injectGateOperators((StreamGraph) pipeline, config, classLoader);
67+
}
68+
return delegate.execute(pipeline, config, classLoader);
69+
}
70+
71+
/**
72+
* Entry point for Option A (Application mode): call this from the user entry-point after
73+
* building the StreamGraph and before env.execute(graph).
74+
*
75+
* <pre>{@code
76+
* StreamGraph graph = env.getStreamGraph("My Job");
77+
* GateInjectorExecutor.injectGates(graph, flinkConfig);
78+
* env.execute(graph);
79+
* }</pre>
80+
*/
81+
public static void injectGates(StreamGraph graph, Configuration config) {
82+
injectGateOperators(graph, config, Thread.currentThread().getContextClassLoader());
83+
}
84+
85+
private static void injectGateOperators(
86+
StreamGraph graph, Configuration config, ClassLoader cl) {
87+
// These keys are injected by the BlueGreen controller when it creates the deployment.
88+
// On the initial deployment (no transition in progress) or in non-BlueGreen clusters
89+
// they will be absent — skip injection rather than crash.
90+
String activeDeploymentType = config.getString("bluegreen.active-deployment-type", null);
91+
String configMapName = config.getString("bluegreen.configmap.name", null);
92+
if (activeDeploymentType == null || configMapName == null) {
93+
System.err.println(
94+
"[BlueGreen] bluegreen.active-deployment-type or bluegreen.configmap.name"
95+
+ " not found in config — skipping gate injection");
96+
return;
97+
}
98+
99+
GateInjectionPosition position =
100+
GateInjectionPosition.valueOf(
101+
config.getString(
102+
"bluegreen.gate.injection.position",
103+
GateInjectionPosition.BEFORE_SINK.name()));
104+
105+
switch (position) {
106+
case AFTER_SOURCE:
107+
{
108+
List<StreamNode> sources =
109+
graph.getStreamNodes().stream()
110+
.filter(n -> n.getInEdges().isEmpty())
111+
.collect(Collectors.toList());
112+
113+
// Only single-source DAGs are supported. Multiple sources imply independent
114+
// event-time domains; a single ConfigMap watermark W derived from one source
115+
// is meaningless for another — no safe multi-source topology exists.
116+
// TODO: consider supporting fan-in (N sources, 1 sink) with per-source gate
117+
// coordination once the watermark aggregation design is finalised.
118+
if (sources.size() != 1) {
119+
throw new IllegalStateException(
120+
"bluegreen.gate.injection.position=AFTER_SOURCE requires exactly 1 source, "
121+
+ "found "
122+
+ sources.size()
123+
+ ": "
124+
+ sources.stream()
125+
.map(StreamNode::getOperatorName)
126+
.collect(Collectors.joining(", ")));
127+
}
128+
129+
StreamNode source = sources.get(0);
130+
List.copyOf(source.getOutEdges())
131+
.forEach(
132+
edge -> {
133+
StreamNode downstream =
134+
graph.getStreamNode(edge.getTargetId());
135+
injectGate(
136+
graph,
137+
config,
138+
cl,
139+
edge,
140+
source,
141+
downstream,
142+
"BlueGreen-Gate[" + source.getOperatorName() + "]");
143+
});
144+
break;
145+
}
146+
case BEFORE_SINK:
147+
{
148+
List<StreamNode> sinks =
149+
graph.getStreamNodes().stream()
150+
.filter(n -> n.getOutEdges().isEmpty())
151+
.collect(Collectors.toList());
152+
153+
// Only single-sink DAGs are supported. Fan-out (1 source → N sinks) is safe in
154+
// principle — all branches share the same event-time domain and W is consistent
155+
// — but distinguishing it from independent source-per-sink chains requires a
156+
// full reachability traversal. For now we keep the invariant simple: exactly 1
157+
// sink. For fan-out DAGs, prefer AFTER_SOURCE instead, which naturally places a
158+
// single gate before all branches.
159+
// TODO: consider adding reachability-based fan-out detection to lift this
160+
// restriction.
161+
if (sinks.size() != 1) {
162+
throw new IllegalStateException(
163+
"bluegreen.gate.injection.position=BEFORE_SINK requires exactly 1 sink, "
164+
+ "found "
165+
+ sinks.size()
166+
+ ": "
167+
+ sinks.stream()
168+
.map(StreamNode::getOperatorName)
169+
.collect(Collectors.joining(", "))
170+
+ ". For fan-out DAGs use bluegreen.gate.injection.position=AFTER_SOURCE instead.");
171+
}
172+
173+
StreamNode sink = sinks.get(0);
174+
List.copyOf(sink.getInEdges())
175+
.forEach(
176+
edge -> {
177+
StreamNode upstream =
178+
graph.getStreamNode(edge.getSourceId());
179+
injectGate(
180+
graph,
181+
config,
182+
cl,
183+
edge,
184+
upstream,
185+
sink,
186+
"BlueGreen-Gate[" + sink.getOperatorName() + "]");
187+
});
188+
break;
189+
}
190+
}
191+
}
192+
193+
private static void injectGate(
194+
StreamGraph graph,
195+
Configuration config,
196+
ClassLoader cl,
197+
StreamEdge edge,
198+
StreamNode upstream,
199+
StreamNode downstream,
200+
String gateName) {
201+
202+
int gateId =
203+
graph.getStreamNodes().stream().mapToInt(StreamNode::getId).max().getAsInt() + 1;
204+
205+
// TypeInformation recovery is the known friction point (see design notes).
206+
// Gate is a passthrough: we use GenericTypeInfo as a placeholder for addOperator(),
207+
// then immediately override with the upstream serializer directly.
208+
TypeInformation<Object> typeInfo = new GenericTypeInfo<>(Object.class);
209+
210+
WatermarkGateProcessFunction<Object> gateFunction = buildGateFunction(config, upstream, cl);
211+
ProcessOperator<Object, Object> gateOperator = new ProcessOperator<>(gateFunction);
212+
213+
graph.addOperator(
214+
gateId,
215+
downstream.getSlotSharingGroup(),
216+
null,
217+
SimpleOperatorFactory.of(gateOperator),
218+
typeInfo,
219+
typeInfo,
220+
gateName);
221+
222+
// Override with the correct serializer from upstream
223+
graph.getStreamNode(gateId).setSerializersIn(upstream.getTypeSerializerOut());
224+
graph.getStreamNode(gateId).setSerializerOut(upstream.getTypeSerializerOut());
225+
226+
// Gate always matches the parallelism of the operator immediately downstream of it.
227+
graph.setParallelism(gateId, downstream.getParallelism());
228+
// StreamGraph.setMaxParallelism(int,int) only applies values > 0.
229+
// ExecutionConfig.getMaxParallelism() returns the job-level setting
230+
// (set via env.setMaxParallelism()), or -1 when unset. Flink's
231+
// scheduler normalises -1 → 128 internally; we do the same here
232+
// so the gate node always gets a valid positive value.
233+
int configuredMaxPar = graph.getExecutionConfig().getMaxParallelism();
234+
graph.setMaxParallelism(gateId, configuredMaxPar > 0 ? configuredMaxPar : 128);
235+
236+
// Rewire: upstream ──✕──> downstream → upstream ──> gate ──> downstream
237+
downstream.getInEdges().remove(edge);
238+
upstream.getOutEdges().remove(edge);
239+
graph.addEdge(upstream.getId(), gateId, 0);
240+
graph.addEdge(gateId, downstream.getId(), 0);
241+
}
242+
243+
private static WatermarkGateProcessFunction<Object> buildGateFunction(
244+
Configuration config, StreamNode upstream, ClassLoader cl) {
245+
246+
GateStrategy strategy =
247+
GateStrategy.valueOf(
248+
config.getString("bluegreen.gate.strategy", GateStrategy.WATERMARK.name()));
249+
250+
switch (strategy) {
251+
case WATERMARK:
252+
{
253+
Map<String, String> flinkConfigMap = new HashMap<>();
254+
flinkConfigMap.put(
255+
"bluegreen.active-deployment-type",
256+
config.getString("bluegreen.active-deployment-type", null));
257+
flinkConfigMap.put(
258+
"kubernetes.namespace", config.getString("kubernetes.namespace", null));
259+
flinkConfigMap.put(
260+
"bluegreen.configmap.name",
261+
config.getString("bluegreen.configmap.name", null));
262+
WatermarkExtractor<Object> extractor =
263+
buildWatermarkExtractor(config, upstream, cl);
264+
return WatermarkGateProcessFunction.create(flinkConfigMap, extractor);
265+
}
266+
default:
267+
throw new IllegalStateException("Unsupported gate strategy: " + strategy);
268+
}
269+
}
270+
271+
private static WatermarkExtractor<Object> buildWatermarkExtractor(
272+
Configuration config, StreamNode upstream, ClassLoader cl) {
273+
274+
boolean isSqlJob = upstream.getTypeSerializerOut() instanceof RowDataSerializer;
275+
276+
if (isSqlJob) {
277+
int fieldIdx = config.getInteger("bluegreen.gate.watermark.field-index", -1);
278+
// fieldIdx is a captured primitive — lambda is serializable via WatermarkExtractor
279+
return (WatermarkExtractor<Object>)
280+
record ->
281+
(fieldIdx < 0 || ((RowData) record).isNullAt(fieldIdx))
282+
? Long.MIN_VALUE
283+
: ((RowData) record).getLong(fieldIdx);
284+
}
285+
286+
String extractorClass = config.getString("bluegreen.gate.watermark.extractor-class", null);
287+
if (extractorClass != null) {
288+
try {
289+
Object instance =
290+
Class.forName(extractorClass, true, cl)
291+
.getDeclaredConstructor()
292+
.newInstance();
293+
// Full serialization dry-run — surfaces non-serializable fields anywhere
294+
// in the object graph before JobGraph submission, not at TaskManager distribution
295+
InstantiationUtil.serializeObject(instance);
296+
return (WatermarkExtractor<Object>) instance;
297+
} catch (IOException e) {
298+
throw new IllegalArgumentException(
299+
extractorClass + " is not fully serializable: " + e.getMessage(), e);
300+
} catch (ReflectiveOperationException e) {
301+
throw new IllegalArgumentException(
302+
"Could not instantiate extractor class: " + extractorClass, e);
303+
}
304+
}
305+
306+
// No extractor configured — fall back to processing-time gating
307+
return (WatermarkExtractor<Object>) record -> Long.MIN_VALUE;
308+
}
309+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.flink.kubernetes.operator.bluegreen.client;
19+
20+
/**
21+
* Selects which {@link GateProcessFunction} implementation is injected into the StreamGraph.
22+
* Configured via {@code bluegreen.gate.strategy}.
23+
*
24+
* <p>Currently only {@link #WATERMARK} is supported. Additional strategies (e.g. processing-time
25+
* only, count-based) can be added here as the gate architecture evolves.
26+
*/
27+
public enum GateStrategy {
28+
/** Watermark-based cutoff using {@link WatermarkGateProcessFunction}. */
29+
WATERMARK
30+
}

0 commit comments

Comments
 (0)