Skip to content

fix(amber): bound sync result reads so visualization fetches release their reader - #6883

Open
aglinxinyuan wants to merge 2 commits into
apache:mainfrom
aglinxinyuan:sync-result-bounded-reads
Open

fix(amber): bound sync result reads so visualization fetches release their reader#6883
aglinxinyuan wants to merge 2 commits into
apache:mainfrom
aglinxinyuan:sync-result-bounded-reads

Conversation

@aglinxinyuan

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

SyncExecutionResource.collectOperatorResult read operator results through an unbounded document.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):

document.get()                       ── opens reader on first next()
  ├─ visualization branch            ── next() once, early return   → LEAK (every sync viz fetch)
  ├─ oversized-first-tuple branch    ── next() once, early return   → LEAK
  ├─ truncation loops                ── drain to exhaustion         → ok
  └─ catch-all on exception          ── abandons mid-drain          → LEAK

Since #6882, a bounded getRange(from, until) iterator releases its reader inside the next() that serves its last record, and hasNext probes open nothing. So the fix is to size each read to what the method actually consumes — no close() API added to VirtualDocument:

Path Consumes Read after this PR Close point
visualization result (totalCount == 1) 1 tuple getRange(0, 1) inside the single next()
oversized first tuple 1 tuple same getRange(0, 1) inside the single next()
table result (truncation loops) all remaining getRange(1, totalCount) inside the next() serving the bound / exhaustion
swallowed-exception catch-all partial catch drains the bounded remainder at the bound

Notes for review:

  • The catch-path drain is bounded: it reads at most the records the successful path would have converted to JSON anyway, and a failing reader ends the drain early (finalizer remains the backstop for that truly exceptional case).
  • The truncation loops previously drained whatever the iterator produced, even records appended after getCount was sampled; they are now capped at the same totalCount used for the totalCount/skippedRows fields, keeping the response internally consistent.
  • Deterministic close relies on the iterator semantics of fix(amber): close Iceberg reader streams leaked by iterator probes and bounded reads #6882; until that lands, these bounded reads behave as before (finalizer-reclaimed), so merge order is safe either way.

Any related issues, documentation, discussions?

Follow-up to the residual-leak inventory in #6881 (SyncExecutionResource.collectOperatorResult rows). Builds on the iterator restructure in #6882. The remaining #6881 rows (ResultExportService client-disconnect, InputPortMaterializationReaderThread interrupt) 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; getRange range semantics are covered by the existing VirtualDocumentSpec/IcebergDocumentSpec suites, and this endpoint's behavior is unchanged (same tuples, same truncation, same response shape). Local WorkflowExecutionService/compile and scalafmtCheck pass.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Fable 5)

…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.
Copilot AI review requested due to automatic review settings July 25, 2026 03:57
@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @Yicong-Huang
    You can notify them by mentioning @Yicong-Huang in a comment.

Copilot AI left a comment

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.

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 bounded getRange(0, 1) for the first tuple and getRange(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 totalCount for 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 subsequent hasNext call, 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.

Comment on lines +555 to +557
val firstTupleIterator = document.getRange(0, 1)

if (totalCount == 0 || !firstTupleIterator.hasNext) {
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 2 better · 🔴 2 worse · ⚪ 11 noise (<±5%) · 0 without baseline

Compared against main 06f8e42 benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

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-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.30%. Comparing base (429be11) to head (dea2e58).

Files with missing lines Patch % Lines
...he/texera/web/resource/SyncExecutionResource.scala 0.00% 6 Missing ⚠️

❌ 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     
Flag Coverage Δ *Carryforward flag
access-control-service 70.00% <ø> (ø) Carriedforward from 429be11
agent-service 76.76% <ø> (ø) Carriedforward from 429be11
amber 69.08% <0.00%> (-0.03%) ⬇️
computing-unit-managing-service 20.49% <ø> (ø) Carriedforward from 429be11
config-service 66.66% <ø> (ø) Carriedforward from 429be11
file-service 67.21% <ø> (ø) Carriedforward from 429be11
frontend 82.58% <ø> (ø) Carriedforward from 429be11
notebook-migration-service 78.94% <ø> (ø) Carriedforward from 429be11
pyamber 92.15% <ø> (ø) Carriedforward from 429be11
workflow-compiling-service 55.14% <ø> (ø) Carriedforward from 429be11

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aglinxinyuan
aglinxinyuan requested a review from mengw15 July 25, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants