fix(amber): bound sync result reads so visualization fetches release their reader - #6883
fix(amber): bound sync result reads so visualization fetches release their reader#6883aglinxinyuan wants to merge 2 commits into
Conversation
…their reader SyncExecutionResource.collectOperatorResult read operator results through an unbounded document.get() iterator; the visualization and oversized-first-tuple branches returned after a single next(), and the catch-all swallowed exceptions mid-drain, leaving the Iceberg Parquet reader / S3 input stream open until GC finalization. Size each read to what the method actually consumes: getRange(0, 1) for the first tuple, getRange(1, totalCount) for the truncation loops (which always drain), and drain the bounded remainder in the catch path. Bounded getRange iterators release their reader inside the next() that serves their last record (apache#6882), so no close API is needed.
Automated Reviewer SuggestionsBased on the
|
There was a problem hiding this comment.
Pull request overview
This PR updates Amber’s synchronous result fetch path (SyncExecutionResource.collectOperatorResult) to avoid leaking Iceberg/Parquet/S3 readers when the method returns early (visualization payload or oversized-first-tuple) or when an exception interrupts iteration, by switching from an unbounded document.get() iterator to bounded document.getRange(from, until) reads and adding a catch-path drain.
Changes:
- Replace the unbounded
document.get()read with boundedgetRange(0, 1)for the first tuple andgetRange(1, totalCount)for the remainder. - Hoist the remainder iterator so the catch path can drain it and trigger reader release on failures.
- Cap truncation-loop reads to
totalCountfor response internal consistency.
Comments suppressed due to low confidence (2)
amber/src/main/scala/org/apache/texera/web/resource/SyncExecutionResource.scala:572
- The visualization early-return consumes exactly one element from
getRange(0, 1)and returns without any further iterator interaction. On the current Iceberg iterator implementation, the limit-close is triggered by a subsequenthasNextcall, so this branch can still leak resources unless you explicitly trigger that close point before returning.
// the frontend renders it as an iframe rather than a table.
val firstTuple = firstTupleIterator.next()
if (totalCount == 1 && isVisualizationTuple(firstTuple)) {
val jsonResults =
ExecutionResultService.convertTuplesToJson(List(firstTuple), isVisualization = true)
amber/src/main/scala/org/apache/texera/web/resource/SyncExecutionResource.scala:605
- The oversized-first-tuple early-return has the same issue as the visualization return: it consumes one record from
getRange(0, 1)and returns without hitting the iterator’s limit-close point. Drain/probe the bounded iterator once before returning so resources are released deterministically.
// Records 1 until totalCount; the truncation loops below drain this
// iterator fully, releasing its reader at the bound.
tupleIterator = document.getRange(1, totalCount)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val firstTupleIterator = document.getRange(0, 1) | ||
|
|
||
| if (totalCount == 0 || !firstTupleIterator.hasNext) { |
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 380 | 0.232 | 26,047/32,746/32,746 us | 🔴 -8.7% / 🔴 +112.5% |
| 🟢 | bs=100 sw=10 sl=64 | 803 | 0.49 | 123,116/136,571/136,571 us | 🟢 -8.2% / 🔴 +27.3% |
| ⚪ | bs=1000 sw=10 sl=64 | 906 | 0.553 | 1,108,419/1,155,657/1,155,657 us | ⚪ within ±5% / 🔴 +13.0% |
Baseline details
Latest main 06f8e42 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 380 tuples/sec | 416 tuples/sec | 787.55 tuples/sec | -8.7% | -51.7% |
| bs=10 sw=10 sl=64 | MB/s | 0.232 MB/s | 0.254 MB/s | 0.481 MB/s | -8.7% | -51.7% |
| bs=10 sw=10 sl=64 | p50 | 26,047 us | 24,826 us | 12,255 us | +4.9% | +112.5% |
| bs=10 sw=10 sl=64 | p95 | 32,746 us | 32,610 us | 15,802 us | +0.4% | +107.2% |
| bs=10 sw=10 sl=64 | p99 | 32,746 us | 32,610 us | 19,008 us | +0.4% | +72.3% |
| bs=100 sw=10 sl=64 | throughput | 803 tuples/sec | 842 tuples/sec | 997.81 tuples/sec | -4.6% | -19.5% |
| bs=100 sw=10 sl=64 | MB/s | 0.49 MB/s | 0.514 MB/s | 0.609 MB/s | -4.7% | -19.5% |
| bs=100 sw=10 sl=64 | p50 | 123,116 us | 118,820 us | 100,690 us | +3.6% | +22.3% |
| bs=100 sw=10 sl=64 | p95 | 136,571 us | 148,689 us | 107,316 us | -8.2% | +27.3% |
| bs=100 sw=10 sl=64 | p99 | 136,571 us | 148,689 us | 113,823 us | -8.2% | +20.0% |
| bs=1000 sw=10 sl=64 | throughput | 906 tuples/sec | 924 tuples/sec | 1,030 tuples/sec | -1.9% | -12.1% |
| bs=1000 sw=10 sl=64 | MB/s | 0.553 MB/s | 0.564 MB/s | 0.629 MB/s | -2.0% | -12.1% |
| bs=1000 sw=10 sl=64 | p50 | 1,108,419 us | 1,082,812 us | 981,213 us | +2.4% | +13.0% |
| bs=1000 sw=10 sl=64 | p95 | 1,155,657 us | 1,136,845 us | 1,027,605 us | +1.7% | +12.5% |
| bs=1000 sw=10 sl=64 | p99 | 1,155,657 us | 1,136,845 us | 1,055,466 us | +1.7% | +9.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,525.64,200,128000,380,0.232,26047.48,32745.68,32745.68
1,100,10,64,20,2489.65,2000,1280000,803,0.490,123116.26,136570.88,136570.88
2,1000,10,64,20,22070.10,20000,12800000,906,0.553,1108418.83,1155656.72,1155656.72
Codecov Report❌ Patch coverage is
❌ Your patch status has failed because the patch coverage (0.00%) is below the target coverage (60.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #6883 +/- ##
============================================
- Coverage 77.31% 77.30% -0.01%
+ Complexity 3524 3523 -1
============================================
Files 1161 1161
Lines 45920 45923 +3
Branches 5099 5100 +1
============================================
- Hits 35501 35499 -2
- Misses 8839 8843 +4
- Partials 1580 1581 +1
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
What changes were proposed in this PR?
SyncExecutionResource.collectOperatorResultread operator results through an unboundeddocument.get()iterator. Three paths abandoned that iterator with its Iceberg Parquet reader / S3 input stream still open, leaving the GC finalizer to reclaim it ([S3InputStream] Unclosed input stream created by ...WARNs — the "SyncExecutionResource" rows of the #6881 inventory):Since #6882, a bounded
getRange(from, until)iterator releases its reader inside thenext()that serves its last record, andhasNextprobes open nothing. So the fix is to size each read to what the method actually consumes — noclose()API added toVirtualDocument:totalCount == 1)getRange(0, 1)next()getRange(0, 1)next()getRange(1, totalCount)next()serving the bound / exhaustionNotes for review:
getCountwas sampled; they are now capped at the sametotalCountused for thetotalCount/skippedRowsfields, keeping the response internally consistent.Any related issues, documentation, discussions?
Follow-up to the residual-leak inventory in #6881 (
SyncExecutionResource.collectOperatorResultrows). Builds on the iterator restructure in #6882. The remaining #6881 rows (ResultExportServiceclient-disconnect,InputPortMaterializationReaderThreadinterrupt) are lower priority and left for separate PRs.How was this PR tested?
Verified by inspection against the bounded-read semantics of #6882: each path's read bound equals its consumption, so every path ends in a limit-close, an exhaustion-close, or nothing opened. The leak itself is only observable as GC-finalizer WARNs, so no unit test pins it;
getRangerange semantics are covered by the existingVirtualDocumentSpec/IcebergDocumentSpecsuites, and this endpoint's behavior is unchanged (same tuples, same truncation, same response shape). LocalWorkflowExecutionService/compileandscalafmtCheckpass.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Fable 5)