This is an automated email from the ASF dual-hosted git repository.
Tartarus0zm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/auron.git
The following commit(s) were added to refs/heads/master by this push:
new c1e59433 [AURON #2329] Test and document multi-consumer source-Calc
fan-out safety (#2415)
c1e59433 is described below
commit c1e5943327482fe7190febfbfba78cf9d23bce28
Author: Weiqing Yang <[email protected]>
AuthorDate: Mon Jul 27 05:48:46 2026 -0700
[AURON #2329] Test and document multi-consumer source-Calc fan-out safety
(#2415)
# Which issue does this PR close?
Closes #2329
# Rationale for this change
#1865 moved Calc-source fusion to a graph-level pass
(`AuronOperatorFusionProcessor`) that counts each source's consumers and
fuses only sole-consumer sources. A source feeding more than one Calc
(for example `UNION ALL` over one table under Flink's default source
reuse) is declined at the sole-consumer gate, and each Calc runs as a
standalone native operator over the shared source stream.
That multi-consumer path is correct but was under-tested and
under-documented. The existing shared-source test only covered the
default config, where object reuse is effectively off so Flink
deep-copies each row to heap per consumer. The interesting case is
object reuse ON: Flink then hands the same `AuronColumnarRowData`
reference to both Calc consumers with no defensive copy in between,
which is exactly where a fan-out aliasing or use-after-free bug would
surface. Nothing pinned that. And the reason a multi-consumer source
declines fusion was only a one-line comment.
# What changes are included in this PR?
A new end-to-end test
`testSharedSourceUnionAllFanOutSafeWithObjectReuse` in
`AuronKafkaSourceMergeITCase`: the same shared-source `UNION ALL` as the
existing test, but with object reuse enabled. It asserts the row set
stays correct and both Calcs remain standalone operators (the
multi-consumer source is not fused).
An expanded doc-comment at the sole-consumer gate in
`AuronOperatorFusionProcessor` explaining why a multi-consumer source is
not fused (the native source runtime is single-plan and single-output,
so N per-consumer plans cannot share one source) and why declining is
safe (each consumer runs a standalone native Calc that copies every
column out of the shared columnar view).
No production logic changes.
# Are there any user-facing changes?
No.
# How was this patch tested?
`./build/mvn test -Pspark-3.5 -Pscala-2.12 -Pflink-1.18 -pl
auron-flink-extension/auron-flink-planner -am
-Dtest=AuronKafkaSourceMergeITCase` with the native library built. All 4
tests pass.
# Was this patch authored or co-authored using generative AI tooling?
- [x] Yes (Claude Code (Opus 4.8))
- [ ] No
---
.../processor/AuronOperatorFusionProcessor.java | 8 +++-
.../table/kafka/AuronKafkaSourceMergeITCase.java | 43 ++++++++++++++++++++++
2 files changed, 50 insertions(+), 1 deletion(-)
diff --git
a/auron-flink-extension/auron-flink-planner/src/main/java/org/apache/auron/flink/table/planner/processor/AuronOperatorFusionProcessor.java
b/auron-flink-extension/auron-flink-planner/src/main/java/org/apache/auron/flink/table/planner/processor/AuronOperatorFusionProcessor.java
index fcd7d972..1f9beadb 100644
---
a/auron-flink-extension/auron-flink-planner/src/main/java/org/apache/auron/flink/table/planner/processor/AuronOperatorFusionProcessor.java
+++
b/auron-flink-extension/auron-flink-planner/src/main/java/org/apache/auron/flink/table/planner/processor/AuronOperatorFusionProcessor.java
@@ -197,7 +197,13 @@ public class AuronOperatorFusionProcessor implements
ExecNodeGraphProcessor {
Map<Integer, Integer> consumerCount,
Function<StreamExecTableSourceScan, DynamicTableSource>
sourceResolver,
ReadableConfig tableConfig) {
- // Sole-consumer gate: multi-consumer fusion is out of scope (tracked
separately).
+ // Sole-consumer gate: a source feeding more than one consumer is
deliberately not fused.
+ // The native source runtime is single-plan / single-output — one
source runs exactly one
+ // PhysicalPlanNode and emits one stream, so N distinct per-consumer
Calc plans cannot share
+ // a single source. Declining loses nothing structurally: each
consumer instead translates
+ // on its own over the source's shared row stream (a standalone native
Calc when the Calc
+ // converts, otherwise Flink's codegen Calc), so no per-consumer plan
is ever staged onto the
+ // shared source and there is no last-write-wins between consumers.
if (consumerCount.getOrDefault(scan.getId(), 0) != 1) {
return;
}
diff --git
a/auron-flink-extension/auron-flink-planner/src/test/java/org/apache/auron/flink/table/kafka/AuronKafkaSourceMergeITCase.java
b/auron-flink-extension/auron-flink-planner/src/test/java/org/apache/auron/flink/table/kafka/AuronKafkaSourceMergeITCase.java
index 89114a9f..36724665 100644
---
a/auron-flink-extension/auron-flink-planner/src/test/java/org/apache/auron/flink/table/kafka/AuronKafkaSourceMergeITCase.java
+++
b/auron-flink-extension/auron-flink-planner/src/test/java/org/apache/auron/flink/table/kafka/AuronKafkaSourceMergeITCase.java
@@ -140,4 +140,47 @@ public class AuronKafkaSourceMergeITCase extends
AuronKafkaSourceTestBase {
rows.sort(Comparator.comparingInt(o -> (int) o.getField(0)));
assertThat(rows).isEqualTo(Arrays.asList(Row.of(21), Row.of(21),
Row.of(22), Row.of(22), Row.of(23)));
}
+
+ /**
+ * The same shared-source {@code UNION ALL} as {@link
+ * #testSharedSourceUnionAllDoesNotFuseUnderDefaultReuse}, but with object
reuse enabled. Under
+ * object reuse Flink hands the <em>same</em> {@code AuronColumnarRowData}
reference to both
+ * standalone Calc consumers with no defensive copy in between (the
sibling test runs with reuse
+ * off, where Flink deep-copies the row to heap per consumer). The row set
is the empirical
+ * guarantee here: the projections and filters convert, so each consumer
runs as a standalone
+ * native Auron Calc that eagerly copies every column out of the shared
columnar view into its
+ * own Arrow batch before returning, and neither consumer retains a
reference to the reused row.
+ * If a native Calc instead aliased the shared row id or read the batch
after it was recycled,
+ * the row set would collapse or corrupt (all rows folding onto the last
row of a batch) or fault
+ * on freed off-heap buffers, so the row-set assertion below is what pins
that native path safe.
+ *
+ * <p>The gate assertion still holds under object reuse: a multi-consumer
source is never fused,
+ * so both Calcs remain standalone operators and {@code calcOperatorCount}
is 2.
+ */
+ @Test
+ public void testSharedSourceUnionAllFanOutSafeWithObjectReuse() {
+ environment.setParallelism(1);
+ boolean prevObjectReuse =
environment.getConfig().isObjectReuseEnabled();
+ environment.getConfig().enableObjectReuse();
+ try {
+ String sql =
+ "SELECT `age` FROM T5 WHERE `age` > 20 " + "UNION ALL
SELECT `age` + 1 FROM T5 WHERE `age` > 10";
+
+ assertThat(calcOperatorCount(sql))
+ .as(
+ "a shared (multi-consumer) source must not fuse
under object reuse; both Calcs must remain operators")
+ .isEqualTo(2);
+
+ List<Row> rows = CollectionUtil.iteratorToList(
+ tableEnvironment.executeSql(sql).collect());
+ rows.sort(Comparator.comparingInt(o -> (int) o.getField(0)));
+ assertThat(rows).isEqualTo(Arrays.asList(Row.of(21), Row.of(21),
Row.of(22), Row.of(22), Row.of(23)));
+ } finally {
+ if (prevObjectReuse) {
+ environment.getConfig().enableObjectReuse();
+ } else {
+ environment.getConfig().disableObjectReuse();
+ }
+ }
+ }
}