Skip to content
Open
4 changes: 2 additions & 2 deletions .github/workflows/flink.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ jobs:
export VELOX_DEPENDENCY_SOURCE=BUNDLED
export fmt_SOURCE=BUNDLED
export folly_SOURCE=BUNDLED
git clone -b gluten-0530 https://github.com/bigo-sg/velox4j.git
cd velox4j && git reset --hard 9811c337d5e37ab8d16d5bf0791b48836fcd2afd
git clone -b feat/stateful-stream-record-timestamp-inserter https://github.com/ggjh-159/velox4j.git
cd velox4j
git apply $GITHUB_WORKSPACE/gluten-flink/patches/fix-velox4j.patch
$GITHUB_WORKSPACE/build/mvn clean install -DskipTests -Dgpg.skip -Dspotless.skip=true
cd ..
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,15 @@ public Transformation<RowData> buildVeloxSink(
"FileSystemInsertTable");
GlutenOneInputOperatorFactory<?, ?> operatorFactory =
new GlutenOneInputOperatorFactory(onewInputOperator);
// If rowtime transformation was applied (rowtimeFieldIndex != -1), a native
// StreamRecordTimestampInserter sits at this position. Replace it with a Gluten columnar
// inserter so the chain stays columnar end-to-end; otherwise this is a no-op.
Transformation<RowData> veloxFileWriterInput =
GlutenRowtimeInserterHelper.processTransformation(
(Transformation<RowData>) fileWriterTransformation.getInputs().get(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In gluten-flink,operator StreamingFileWriter will totally offload to velox, and velox's file writer will not rely on the StreamRecord.timestamp to determine partition, roll file, or commit partition, therefore, StreamRecordTimestampInserter is just a useless operator, we should just remove it from the op chain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Updated FileSystemSinkFactory to call GlutenRowtimeInserterHelper.processTransformation(input, /*requiresTimestamp=*/false), which removes any native StreamRecordTimestampInserter from the input chain instead of replacing it with the columnar one. Same treatment applied to PrintSinkFactory and FuzzerSourceSinkFactory.

Transformation<RowData> veloxFileWriterTransformation =
new OneInputTransformation(
fileWriterTransformation.getInputs().get(0),
veloxFileWriterInput,
fileWriterTransformation.getName(),
operatorFactory,
fileWriterTransformation.getOutputType(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ public Transformation<RowData> buildVeloxSink(
RowData.class,
"FuzzerSink"));
DataStream<RowData> newInputStream =
sinkTransformation
.getInputStream()
GlutenRowtimeInserterHelper.process(sinkTransformation.getInputStream())
.transform("Writer", CommittableMessageTypeInfo.noOutput(), operatorFactory);
return new SinkTransformation<RowData, RowData>(
newInputStream,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.gluten.velox;

import org.apache.gluten.table.runtime.operators.GlutenOneInputOperator;
import org.apache.gluten.util.LogicalTypeConverter;
import org.apache.gluten.util.PlanNodeIdGenerator;
import org.apache.gluten.util.ReflectUtils;

import io.github.zhztheplayer.velox4j.expression.FieldAccessTypedExpr;
import io.github.zhztheplayer.velox4j.expression.InputTypedExpr;
import io.github.zhztheplayer.velox4j.plan.EmptyNode;
import io.github.zhztheplayer.velox4j.plan.ProjectNode;
import io.github.zhztheplayer.velox4j.plan.StatefulPlanNode;
import io.github.zhztheplayer.velox4j.plan.StreamRecordTimestampInserterNode;
import io.github.zhztheplayer.velox4j.stateful.StatefulRecord;
import io.github.zhztheplayer.velox4j.type.RowType;

import org.apache.flink.api.dag.Transformation;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
import org.apache.flink.streaming.api.operators.SimpleOperatorFactory;
import org.apache.flink.streaming.api.transformations.OneInputTransformation;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.runtime.operators.sink.StreamRecordTimestampInserter;
import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;

import java.util.List;
import java.util.Map;

/**
* Replaces a native Flink {@link StreamRecordTimestampInserter} (per-row timestamp) with a Gluten
* columnar inserter (batch-max timestamp) so the columnar chain stays intact when the downstream
* sink is offloaded to Velox.
*
* <p>The native inserter is constructed by {@code CommonExecSink.applyRowtimeTransformation} and
* sits somewhere on the sink's input chain (position depends on the sink: for a simple sink like
* Fuzzer/DiscardingSink it is the direct input; for FileSystem it sits below the
* StreamingFileWriter and PartitionCommitter). Each sink factory's {@code buildVeloxSink} invokes
* this helper on the inserter-bearing transformation it is about to replace; if no inserter is
* present (e.g., {@code rowtimeFieldIndex == -1}), the helper is a no-op and returns the input
* unchanged.
*/
public final class GlutenRowtimeInserterHelper {

private GlutenRowtimeInserterHelper() {}

/**
* Convenience overload that accepts a {@link DataStream} (typical entry point for factories that
* use {@code sinkTransformation.getInputStream()}). Inspects the underlying transformation; if it
* is a native inserter, rebuilds it as a Gluten columnar inserter and returns a new DataStream
* whose terminal node is the Gluten inserter. Otherwise returns the inputStream unchanged.
*/
public static DataStream<RowData> process(DataStream<RowData> inputStream) {
Transformation<RowData> inputTrans = inputStream.getTransformation();
Transformation<RowData> newTrans = processTransformation(inputTrans);
if (newTrans == inputTrans) {
return inputStream;
}
return new DataStream<>(inputStream.getExecutionEnvironment(), newTrans);
}

/**
* Inspect {@code inputTrans}; if it is a native {@link StreamRecordTimestampInserter}, rebuild it
* as a Gluten columnar inserter whose input is the native inserter's upstream. Returns the new
* transformation, or the original inputTrans when no replacement happened.
*/
public static Transformation<RowData> processTransformation(Transformation<RowData> inputTrans) {
if (!(inputTrans instanceof OneInputTransformation)) {
return inputTrans;
}
OneInputTransformation<?, ?> oneInput = (OneInputTransformation<?, ?>) inputTrans;
if (!(oneInput.getOperatorFactory() instanceof SimpleOperatorFactory)) {
return inputTrans;
}
@SuppressWarnings("rawtypes")
Object op = ((SimpleOperatorFactory) oneInput.getOperatorFactory()).getOperator();
if (!(op instanceof StreamRecordTimestampInserter)) {
return inputTrans;
}
int rowtimeIndex =
(int) ReflectUtils.getObjectField(StreamRecordTimestampInserter.class, op, "rowtimeIndex");
List<Transformation<?>> inputs = oneInput.getInputs();
if (inputs.isEmpty()) {
return inputTrans;
}
@SuppressWarnings("unchecked")
Transformation<RowData> aboveInserter = (Transformation<RowData>) inputs.get(0);
return buildGlutenInserter(aboveInserter, rowtimeIndex, oneInput.getParallelism());
}

private static Transformation<RowData> buildGlutenInserter(
Transformation<RowData> aboveInserter, int rowtimeFieldIndex, int parallelism) {
@SuppressWarnings("unchecked")
InternalTypeInfo<RowData> internalTypeInfo =
(InternalTypeInfo<RowData>) aboveInserter.getOutputType();
final org.apache.flink.table.types.logical.RowType inputRowType =
(org.apache.flink.table.types.logical.RowType) internalTypeInfo.toLogicalType();
final RowType vlInputType = (RowType) LogicalTypeConverter.toVLType(inputRowType);
final List<String> fieldNames = inputRowType.getFieldNames();
final String rowtimeFieldName = fieldNames.get(rowtimeFieldIndex);
final InputTypedExpr inputExpr = new InputTypedExpr(vlInputType);
final ProjectNode project =
new ProjectNode(
PlanNodeIdGenerator.newId(),
List.of(new EmptyNode(vlInputType)),
List.of(rowtimeFieldName),
List.of(FieldAccessTypedExpr.create(inputExpr, rowtimeFieldName)));
final StreamRecordTimestampInserterNode inserterNode =
new StreamRecordTimestampInserterNode(
PlanNodeIdGenerator.newId(), null, project, rowtimeFieldIndex);
final StatefulPlanNode statefulPlan = new StatefulPlanNode(inserterNode.getId(), inserterNode);

final GlutenOneInputOperator<StatefulRecord, StatefulRecord> operator =
new GlutenOneInputOperator<>(
statefulPlan,
PlanNodeIdGenerator.newId(),
vlInputType,
Map.of(inserterNode.getId(), vlInputType),
StatefulRecord.class,
StatefulRecord.class,
"StreamRecordTimestampInserter");

@SuppressWarnings({"rawtypes", "unchecked"})
final OneInputStreamOperator rawOperator = (OneInputStreamOperator) operator;
return new OneInputTransformation<>(
aboveInserter,
"StreamRecordTimestampInserter",
SimpleOperatorFactory.of(rawOperator),
aboveInserter.getOutputType(),
parallelism);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ public boolean isStdErr() {
public Transformation buildVeloxSink(
Transformation<RowData> transformation, Map<String, Object> parameters) {
Transformation inputTrans = (Transformation) transformation.getInputs().get(0);
// If the upstream chain contains a native StreamRecordTimestampInserter (e.g., when this
// sink is reached via a rowtime-bearing path), replace it with a Gluten columnar inserter.
// For the typical PrintSink path (SinkFunctionProvider), no inserter is present and this is
// a no-op.
inputTrans = GlutenRowtimeInserterHelper.processTransformation(inputTrans);
InternalTypeInfo inputTypeInfo = (InternalTypeInfo) inputTrans.getOutputType();

PrintOptions printOpts = extractPrintOptions(transformation);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.gluten.velox;

import org.apache.gluten.table.runtime.operators.GlutenOneInputOperator;

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.dag.Transformation;
import org.apache.flink.streaming.api.operators.SimpleOperatorFactory;
import org.apache.flink.streaming.api.operators.StreamMap;
import org.apache.flink.streaming.api.transformations.OneInputTransformation;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.runtime.operators.sink.StreamRecordTimestampInserter;
import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
import org.apache.flink.table.types.logical.BigIntType;
import org.apache.flink.table.types.logical.RowType;

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

class GlutenRowtimeInserterHelperTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use max timestamp in a batch record to replace the per-row timestamp, which is a different semantics,can we add a test for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three sink factories (Print, Fuzzer/Discard, FileSystem) wire requiresTimestamp = false, so the inserter is removed from the op chain and the batch-max path in GlutenRowtimeInserterHelper has no runtime caller in this PR. A test here would exercise a path no real sink reaches.

Batch-max computation itself is covered by velox C++ UT StreamRecordTimestampInserterTest.emitsBatchMaxTimestamp (PR bigo-sg/velox#64). The replacement/removal branches are covered by GlutenRowtimeInserterHelperTest.

private static final RowType UPSTREAM_ROW_TYPE = RowType.of(new BigIntType());

private static Transformation<RowData> newUpstream() {
return new StubTransformation("upstream", InternalTypeInfo.of(UPSTREAM_ROW_TYPE));
}

private static OneInputTransformation<RowData, RowData> newNativeInserterTx(
Transformation<RowData> upstream, int rowtimeIndex) {
StreamRecordTimestampInserter op = new StreamRecordTimestampInserter(rowtimeIndex);
return new OneInputTransformation<>(
upstream, "native-inserter", op, upstream.getOutputType(), 1);
}

private static OneInputTransformation<RowData, RowData> newOtherOperatorTx(
Transformation<RowData> upstream) {
StreamMap<RowData, RowData> other = new StreamMap<>(new IdentityMapFunction());
return new OneInputTransformation<>(upstream, "other-op", other, upstream.getOutputType(), 1);
}

@Test
void testNoOpForNonOneInputTransformation() {
Transformation<RowData> stub =
new StubTransformation("stub", InternalTypeInfo.of(UPSTREAM_ROW_TYPE));
assertSame(stub, GlutenRowtimeInserterHelper.processTransformation(stub));
}

@Test
void testNoOpForNonInserterOperator() {
Transformation<RowData> upstream = newUpstream();
OneInputTransformation<RowData, RowData> tx = newOtherOperatorTx(upstream);
assertSame(tx, GlutenRowtimeInserterHelper.processTransformation(tx));
}

@Test
void testReplacesNativeInserter() {
Transformation<RowData> upstream = newUpstream();
OneInputTransformation<RowData, RowData> nativeTx = newNativeInserterTx(upstream, 0);

Transformation<RowData> result = GlutenRowtimeInserterHelper.processTransformation(nativeTx);

assertNotSame(nativeTx, result);
assertTrue(result instanceof OneInputTransformation);
@SuppressWarnings("unchecked")
OneInputTransformation<RowData, RowData> out =
(OneInputTransformation<RowData, RowData>) result;
assertTrue(out.getOperatorFactory() instanceof SimpleOperatorFactory);
Object op = ((SimpleOperatorFactory<?>) out.getOperatorFactory()).getOperator();
assertTrue(op instanceof GlutenOneInputOperator);
assertSame(upstream, out.getInputs().get(0));
assertSame(1, out.getParallelism());
}

private static final class StubTransformation extends Transformation<RowData> {
StubTransformation(String name, InternalTypeInfo<RowData> typeInfo) {
super(name, typeInfo, 1);
}

@Override
public List<Transformation<?>> getInputs() {
return Collections.emptyList();
}

@Override
protected List<Transformation<?>> getTransitivePredecessorsInternal() {
return Collections.emptyList();
}
}

private static final class IdentityMapFunction implements MapFunction<RowData, RowData> {
@Override
public RowData map(RowData value) {
return value;
}
}
}
Loading