Fan Deng created FLINK-40232:
--------------------------------
Summary: NullPointerException in
OutputConversionOperator.processElement when rowtime attribute is null
Key: FLINK-40232
URL: https://issues.apache.org/jira/browse/FLINK-40232
Project: Flink
Issue Type: Bug
Components: Table SQL / API, Table SQL / Planner
Affects Versions: 2.1.3, 1.20.5, 2.0.2, 1.19.3, 1.18.1, 1.17.2, 1.16.3,
1.15.4
Environment: Flink 1.15+ (verified on release-1.15, release-2.1,
master)
Reporter: Fan Deng
h2. Problem
{{OutputConversionOperator.processElement}} (line 74) throws
{{NullPointerException}} when a rowtime attribute is {{null}} and
{{consumeRowtimeMetadata=true}}. The operator calls
{{rowData.getTimestamp(...).getMillisecond()}} without null check.
h2. Root Cause
File:
{{flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/sink/OutputConversionOperator.java}}
{code:java}
@Override
public void processElement(StreamRecord<RowData> element) throws Exception {
final RowData rowData = element.getValue();
if (consumeRowtimeMetadata) {
// timestamp is TIMESTAMP_LTZ
final long rowtime = rowData.getTimestamp(rowData.getArity() - 1,
3).getMillisecond(); // ← line 74 NPE
outRecord.setTimestamp(rowtime);
} else if (rowtimeIndex != -1) {
// timestamp might be TIMESTAMP or TIMESTAMP_LTZ
final long rowtime = rowData.getTimestamp(rowtimeIndex,
3).getMillisecond(); // ← line 78 same issue
outRecord.setTimestamp(rowtime);
}
...
}
{code}
The code calls {{rowData.getTimestamp(...)}} and immediately invokes
{{.getMillisecond()}} on the result without null check. When the rowtime field
is {{null}}, {{getTimestamp()}} returns Java {{null}} for certain
{{RowData}} implementations, causing {{NullPointerException}}.
h2. Why NPE Occurs with BoxedWrapperRowData
Calc codegen produces {{BoxedWrapperRowData}} as output (see
{{CalcCodeGenerator.scala:61}}):
{code:scala}
// CalcCodeGenerator.scala:61
classOf[BoxedWrapperRowData],
{code}
{{BoxedWrapperRowData.getTimestamp}} returns Java {{null}} for null fields
(unlike {{BinaryRowData}} which returns {{TimestampData.fromEpochMillis(0)}}):
{code:java}
// BoxedWrapperRowData.java:115-116
public TimestampData getTimestamp(int pos, int precision) {
return (TimestampData) this.fields[pos]; // fields[pos]==null → returns
Java null
}
{code}
||RowData implementation||{{getTimestamp(pos, precision)}} for null
field||{{.getMillisecond()}}||
|{{BinaryRowData}}|{{TimestampData.fromEpochMillis(0)}} (non-null)|Returns 0,
no NPE|
|{{BoxedWrapperRowData}}|Java {{null}}|NPE|
|{{GenericRowData}}|Java {{null}}|NPE|
h2. Trigger Conditions
The NPE requires all of the following:
{{consumeRowtimeMetadata=true}}: The sink schema declares a rowtime metadata
column (e.g., via {{Schema.columnByMetadata("rowtime", ...)}})
Null rowtime field: A record with null rowtime reaches
{{OutputConversionOperator}}
{{BoxedWrapperRowData}} passed directly: Calc codegen output
({{BoxedWrapperRowData}}) is passed directly to {{OutputConversionOperator}}
without serialization (operator chaining enabled, object reuse enabled)
No upstream {{WatermarkAssignerOperator}}: Source performs watermark pushdown
(e.g., Kafka source implements {{SupportsWatermarkPushDown}}), so the planner
does not insert an independent
{{WatermarkAssignerOperator}} that would otherwise reject null rowtime earlier
h2. Reproduction
h3. End-to-end MiniCluster test
Verified on release-1.15:
{code:java}
// Source with watermark pushdown (no independent WatermarkAssignerOperator)
tenv.executeSql(
"CREATE TABLE src (\n"
+ " f0 STRING,\n"
+ " rtime TIMESTAMP(3),\n"
+ " WATERMARK FOR rtime AS rtime - INTERVAL '10' SECOND\n"
+ ") WITH (\n"
+ " 'connector' = 'pushdown-watermark-values',\n"
+ " 'data-id' = '...' // emits Row with null rtime\n"
+ ")");
Table table = tenv.from("src");
// Sink schema with rowtime metadata column → consumeRowtimeMetadata=true
Schema sinkSchema = Schema.newBuilder()
.column("f0", DataTypes.STRING())
.columnByMetadata("rtime", DataTypes.TIMESTAMP_LTZ(3).notNull(),
"rowtime")
.build();
// toChangelogStream triggers
OutputConversionOperator(consumeRowtimeMetadata=true)
// Calc codegen produces BoxedWrapperRowData → getTimestamp returns null → NPE
tenv.toChangelogStream(table, sinkSchema, ChangelogMode.insertOnly()).print();
env.execute("repro"); // → NPE at OutputConversionOperator.processElement:74
{code}
h3. Stack trace
{code}
java.lang.NullPointerException
at
org.apache.flink.table.runtime.operators.sink.OutputConversionOperator.processElement(OutputConversionOperator.java:74)
at
org.apache.flink.streaming.runtime.tasks.CopyingChainingOutput.pushToOperator(CopyingChainingOutput.java:82)
...
Caused by: java.lang.NullPointerException
at
org.apache.flink.table.runtime.operators.sink.OutputConversionOperator.processElement(OutputConversionOperator.java:74)
{code}
h2. Why disable-operator-chaining works as workaround
When operator chaining is disabled, the chain boundary forces
{{RowDataSerializer}} serialization/deserialization. The deserialized row
becomes {{BinaryRowData}}, whose {{getTimestamp}} is null-tolerant (returns
{{TimestampData.fromEpochMillis(0)}} for null fields). This is not a fix —
it's a workaround that masks the underlying bug.
h2. Impact
- Affects: release-1.15, release-2.1, master (verified byte-identical via
{{git diff}})
- Introduced in: FLINK-19980 (Timo Walther, 2021-04-13)
- Triggered in production: Oceanus (Tencent's Flink platform) — Kafka source
with source-watermark pushdown + {{toChangelogStream}} with rowtime metadata
column
- No existing JIRA: This bug has not been reported to the community for 5+
years
h2. Suggested Fix
Add null check in {{OutputConversionOperator.processElement}}:
{code:java}
@Override
public void processElement(StreamRecord<RowData> element) throws Exception {
final RowData rowData = element.getValue();
if (consumeRowtimeMetadata) {
// timestamp is TIMESTAMP_LTZ
TimestampData ts = rowData.getTimestamp(rowData.getArity() - 1, 3);
if (ts != null) {
final long rowtime = ts.getMillisecond();
outRecord.setTimestamp(rowtime);
}
} else if (rowtimeIndex != -1) {
// timestamp might be TIMESTAMP or TIMESTAMP_LTZ
TimestampData ts = rowData.getTimestamp(rowtimeIndex, 3);
if (ts != null) {
final long rowtime = ts.getMillisecond();
outRecord.setTimestamp(rowtime);
}
}
...
}
{code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)