-
Notifications
You must be signed in to change notification settings - Fork 626
[GLUTEN-12426][FLINK] Feat: Add columnar StreamRecordTimestampInserter operator #12428
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
ef38250
c2fd991
b0062d7
cb649b0
ae4ffe6
b686da9
a7565e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
| @@ -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 { | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All three sink factories (Print, Fuzzer/Discard, FileSystem) wire Batch-max computation itself is covered by velox C++ UT |
||
| 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; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In gluten-flink,operator
StreamingFileWriterwill totally offload to velox, and velox'sfile writerwill not rely on theStreamRecord.timestamp to determine partition, roll file, or commit partition, therefore,StreamRecordTimestampInserteris just a useless operator, we should just remove it from the op chain.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Updated
FileSystemSinkFactoryto callGlutenRowtimeInserterHelper.processTransformation(input, /*requiresTimestamp=*/false), which removes any nativeStreamRecordTimestampInserterfrom the input chain instead of replacing it with the columnar one. Same treatment applied toPrintSinkFactoryandFuzzerSourceSinkFactory.