DanielLeens commented on PR #10931:
URL: https://github.com/apache/seatunnel/pull/10931#issuecomment-4519962536
Thanks for working on this! I went through the full diff, checked the latest
head `f2b4dac187987e29c0c309e0e60b98742991220b` locally against `upstream/dev`,
and traced the real call paths in both StarRocks and Doris because this common
Arrow reader is shared by both connectors. I did not run Maven locally in this
pass; this is a source-level review only.
# What problem this PR solves
- User pain point
When StarRocks source reads Arrow batches, the old implementation keeps
Arrow reader / allocator state on the `ArrowToSeatunnelRowReader` instance, so
delayed or missing cleanup can eventually surface as Arrow-memory-related
exceptions.
- Fix approach
This PR moves Arrow parsing into try-with-resources inside `readArrow()`
so `RootAllocator`, `ByteArrayInputStream`, and `ArrowStreamReader` are closed
immediately after the batch is converted to `SeaTunnelRow`.
- One sentence
The goal is good: shrink the Arrow off-heap lifetime to a single
deserialize pass instead of the whole row-batch object lifetime.
# Runtime chain I checked
```text
StarRocks source
-> StarRocksBeReadClient.hasNext()
[connector-starrocks/.../StarRocksBeReadClient.java:145-170]
-> new ArrowToSeatunnelRowReader(result.getRows(),
seaTunnelRowType).readArrow()
-> ArrowToSeatunnelRowReader.readArrow()
[connector-common/.../ArrowToSeatunnelRowReader.java:94-116]
-> ArrowStreamReader.loadNextBatch()
-> convertSeatunnelRow()
-> keep seatunnelRowBatch for later row-by-row consumption
-> old batch is closed at the next-batch boundary [145-147]
Doris source async path
-> DorisValueReader async thread
[connector-doris/.../DorisValueReader.java:154-170]
-> new ArrowToSeatunnelRowReader(nextResult.getRows(),
seaTunnelRowType).readArrow()
-> rowBatch.close()
-> rowBatchBlockingQueue.put(rowBatch)
-> consumer thread later takes the row batch and iterates rows [199-218]
```
# Core logic review
Before this PR, the reader kept Arrow objects on the instance:
```java
private ArrowStreamReader arrowStreamReader;
private RootAllocator rootAllocator;
private void initArrowReader(byte[] byteArray) {
this.rootAllocator = new RootAllocator(Integer.MAX_VALUE);
this.arrowStreamReader =
new ArrowStreamReader(new ByteArrayInputStream(byteArray),
rootAllocator);
}
...
finally {
close();
}
```
After this PR, Arrow resources are local to `readArrow()`, but the raw
payload is now stored on the object and `close()` is a no-op:
```java
private byte[] byteArray;
private void initArrowReader(byte[] byteArray) {
this.byteArray = byteArray;
}
public ArrowToSeatunnelRowReader readArrow() {
try (RootAllocator rootAllocator = new RootAllocator(Integer.MAX_VALUE);
ByteArrayInputStream byteArrayInputStream = new
ByteArrayInputStream(byteArray);
ArrowStreamReader arrowStreamReader =
new ArrowStreamReader(byteArrayInputStream,
rootAllocator)) {
...
}
}
@Override
public void close() {}
```
Key findings:
1. The normal runtime path definitely hits this change in both StarRocks and
Doris.
2. The PR does improve off-heap Arrow cleanup timing.
3. The current implementation still keeps the raw Arrow payload attached to
the row-batch object, so it does not fully solve the memory-pressure story.
4. Doris async mode is the most sensitive path because queued row batches
can accumulate both the fully materialized rows and the original Arrow bytes at
the same time.
# Findings
Issue 1: `close()` no longer releases the batch-side retained payload, so
the main-path release hook is weakened
- Location:
`seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/reader/ArrowToSeatunnelRowReader.java:64,90-116,317-318`
- Problem description:
This class sits on the common Arrow deserialize path for StarRocks and
Doris. The PR stores the raw `byte[]` in the instance field `byteArray`, but
`close()` is now empty. At the same time, callers still rely on the old
batch-boundary cleanup pattern:
- StarRocks closes the previous batch before fetching the next one at
`connector-starrocks/.../StarRocksBeReadClient.java:145-147`
- Doris async mode closes the batch before queueing it at
`connector-doris/.../DorisValueReader.java:161-169`
- Doris sync mode closes the previous batch before advancing at
`connector-doris/.../DorisValueReader.java:227-230`
So the off-heap reader is closed earlier, but the row-batch object still
keeps the original Arrow payload alive for the rest of its lifetime.
- Potential risk:
This can still create a high heap peak on the real source path, especially
in Doris async mode where multiple row batches may sit in the queue at once. In
practice that means one extra full copy of each batch can remain reachable even
after conversion, which directly weakens the memory-fix goal of this PR.
- Best improvement suggestion:
Option A: keep the try-with-resources change, but explicitly clear
`byteArray`, `root`, and `fieldVectors` after conversion, and make `close()` at
least release those helper references.
Option B: do not store the raw `byte[]` on the instance at all; keep Arrow
parsing fully method-local and only retain the final `seatunnelRowBatch`.
- Severity: High
- Already raised by others: No
Issue 2: there is no regression test for the new lifecycle contract on this
shared reader
- Location:
`seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/ArrowToSeatunnelRowReaderTest.java:310-326,447-458`
- Problem description:
The existing tests cover the happy path of reading rows out of Arrow, but
this PR changes the lifecycle semantics of a shared connector-common reader.
There is still no regression test that locks down the new “read first, then
close, then continue consuming rows” contract, and no test that protects
against retaining the original Arrow payload after conversion.
- Potential risk:
Since both StarRocks and Doris reuse this class, a lifecycle regression
here can come back later without CI catching it.
- Best improvement suggestion:
Please add at least one regression test that reflects the real caller
contract. For example:
1. call `readArrow()`, then `close()`, and verify rows are still readable,
and/or
2. verify the reader no longer retains Arrow-side helper state after
conversion.
- Severity: Medium
- Already raised by others: No
# Compatibility
- API: compatible
- Config/defaults: no change
- Protocol/serialization: no change to Arrow payload format
- Historical behavior: the implementation-level meaning of `close()`
changed, which is exactly why issue 1 matters on the existing call path
# Tests / coverage
- No new UT/E2E test code was added in this PR.
- The main gap is coverage, not flaky-test structure.
# CI status
- Current GitHub state on this head:
- `mergeable=MERGEABLE`
- `mergeStateStatus=BLOCKED`
- `Build` is still `QUEUED`
- So from the GitHub side, CI is not green yet, but the source-level blocker
above is independent of the queued Build.
### Conclusion: can merge after fixes
1. Blocking items
- Issue 1: the shared reader still retains the raw Arrow payload on the
normal StarRocks / Doris path, so the memory-fix story is not complete yet.
2. Suggested but non-blocking follow-up
- Issue 2: add a regression test that locks down the new lifecycle contract
of this common reader.
Overall, the direction makes sense and the earlier Arrow-resource cleanup is
a real improvement. But for the current head, I do not think we should merge
before the retained-payload problem is closed as well, because that is still on
the exact memory-sensitive path this PR is trying to fix.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]