Skip to content

fix(amber): close Iceberg reader streams leaked by iterator probes and bounded reads - #6882

Open
aglinxinyuan wants to merge 3 commits into
apache:mainfrom
aglinxinyuan:iceberg-reader-leak
Open

fix(amber): close Iceberg reader streams leaked by iterator probes and bounded reads#6882
aglinxinyuan wants to merge 3 commits into
apache:mainfrom
aglinxinyuan:iceberg-reader-leak

Conversation

@aglinxinyuan

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

Root cause. IcebergDocument.getUsingFileSequenceOrder's iterator opened the Parquet reader (and the S3InputStream beneath it) inside hasNext. Two consumer shapes then leak the stream until the GC finalizer reclaims it — producing the [S3InputStream] Unclosed input stream created by … warnings all over the amber CI jobs:

Consumer shape Why it leaked
Probes — isEmpty / nonEmpty / lone hasNext (e.g. VirtualDocumentSpec's "clear the document" test) hasNext opened a stream to answer, the caller abandoned the iterator; no close point ever ran
Bounded reads — getRange(from, until) consumed to exactly the limit The limit close ran only on a subsequent hasNext call that bounded consumers never make

Fix — hasNext no longer acquires resources:

  • hasNext claims the next data file from FileScanTask.recordCount metadata alone. This adds no new trust: the whole-file skip in seekToUsableFile already relies on the same field; planFiles() never splits files in Iceberg 1.9.2 (splitting is planTasks()-only) and amber's write paths are strictly append-only (no delete files anywhere in-repo), so the count is exact.
  • The Parquet reader opens lazily in next() (openPendingFile()), the only resource-acquisition point.
  • next() closes the reader deterministically the moment a bounded read has served its last record — before → after: getRange(a, b).toList used to keep the last file's stream open until GC; it now closes inside the final next().
  • All closes funnel through an idempotent closeCurrentReader() that also resets the record iterator, so a closed reader is never polled (the old code did poll one on the exhaustion path — benign with today's Iceberg iterator internals, but the hazard is gone).

Record sequences, hasNext semantics, NoSuchElementException behavior, and the incremental-snapshot refresh (lastSnapshotId bookkeeping) are unchanged — only where streams open and close moved.

Also: close the RESTCatalog in IcebergRestCatalogIntegrationSpec.afterAll. Iceberg 1.9.2's RESTSessionCatalog tracks per-table FileIO instances (FileIOTracker) and closes them with the catalog, which removes the sibling Unclosed S3FileIO instance finalizer warnings.

Residual (pre-existing, untouched) abandonment paths — notably SyncExecutionResource.collectOperatorResult's visualization early-return — are inventoried in #6881 as follow-ups rather than expanded here.

Any related issues, documentation, discussions?

Closes #6881

How was this PR tested?

The changed paths are pinned by the existing VirtualDocumentSpec contract suite (probe, range, getAfter, incremental second-batch arrival, concurrent writes), which IcebergDocumentSpec runs against real Iceberg storage in the amber-integration CI job — those specs exercise every branch of the restructured iterator. Verified locally: WorkflowCore/Test/compile, WorkflowExecutionService/Test/compile, and scalafmtCheck on both source sets all pass.

Additionally verified by an exhaustive old-vs-new state-machine review covering: probe-only use, repeated hasNext, multi-file drains, bounded reads with partial mid-file skip, from beyond EOF, empty ranges, empty tables, snapshot arrival mid-read, zero-record files, loop termination, and closed-reader polling — record sequences and hasNext booleans are identical in every scenario; the only differences are the intended open/close points. The partial-skip invariant (from - numOfSkippedRecords non-zero only for the first claimed file) follows from seekToUsableFile's dropWhile guarantee and is documented in the code.

No new unit test is added because the leak itself is only observable through GC-finalizer instrumentation; the observable regression signal is the disappearance of the Unclosed input stream warnings from the amber CI logs.

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

Generated-by: Claude Code (Opus 4.8 [1M context])

IcebergDocument's read iterator opened a Parquet reader (and the
S3InputStream beneath it) inside hasNext, so probe-shaped consumers -
isEmpty/nonEmpty/hasNext-only checks, e.g. VirtualDocumentSpec's
"clear the document" test - opened a stream and then abandoned the
iterator with no close path. The GC finalizer eventually reclaimed
each one, logging "[S3InputStream] Unclosed input stream created by
..." warnings all over CI. Bounded reads had the same hole: the
limit close only ran on a subsequent hasNext call that bounded
consumers never make.

Restructure the iterator so hasNext acquires nothing:

* hasNext claims the next data file from FileScanTask.recordCount
  metadata alone - the same field the whole-file skip already
  trusts; planFiles() never splits files in Iceberg 1.9.2 and the
  write path is strictly append-only, so the count is exact;
* the Parquet reader opens lazily in next() via openPendingFile();
* next() closes the reader deterministically the moment a bounded
  read (getRange) has served its last record;
* all closes go through an idempotent closeCurrentReader() that also
  resets the record iterator, so a closed reader is never polled.

Record sequences, hasNext semantics, and the incremental-snapshot
refresh behavior are unchanged - only where streams open and close
moved.

Also close the RESTCatalog in IcebergRestCatalogIntegrationSpec's
afterAll: Iceberg 1.9.2 tracks per-table FileIO instances and closes
them with the catalog, removing the sibling "Unclosed S3FileIO
instance" finalizer warnings from the same CI logs.
Copilot AI review requested due to automatic review settings July 25, 2026 03:28
@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: @Ma77Ball, @mengw15
    You can notify them by mentioning @Ma77Ball, @mengw15 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 fixes real resource leaks in Amber’s Iceberg result iteration by ensuring Parquet/S3 readers are opened only when records are actually consumed and are closed deterministically for bounded reads. This change lives in the Iceberg-backed result storage layer (common/workflow-core) and adds a missing catalog close in an Amber integration spec to eliminate Iceberg finalizer warnings.

Changes:

  • Refactors IcebergDocument iteration so hasNext no longer opens Parquet/S3 resources; readers are opened in next() and closed via a centralized idempotent close path.
  • Ensures bounded reads (e.g., getRange(...).toList) close the active reader immediately after serving the final record rather than waiting for a subsequent hasNext.
  • Closes RESTCatalog in IcebergRestCatalogIntegrationSpec.afterAll to release HTTP client + tracked S3FileIO instances.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/result/iceberg/IcebergDocument.scala Moves resource acquisition out of hasNext, adds pending-file claiming + deterministic reader close to prevent Parquet/S3 stream leaks.
amber/src/test/integration/org/apache/texera/amber/storage/iceberg/IcebergRestCatalogIntegrationSpec.scala Closes RESTCatalog in afterAll to prevent S3FileIO finalizer leak warnings in integration tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov-commenter

codecov-commenter commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.47%. Comparing base (084309c) to head (126882f).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6882      +/-   ##
============================================
+ Coverage     79.59%   81.47%   +1.88%     
+ Complexity     3838      212    -3626     
============================================
  Files          1159      529     -630     
  Lines         46122    30257   -15865     
  Branches       5127     3215    -1912     
============================================
- Hits          36709    24652   -12057     
+ Misses         7787     4830    -2957     
+ Partials       1626      775     -851     
Flag Coverage Δ *Carryforward flag
access-control-service 71.10% <ø> (+1.10%) ⬆️
agent-service 77.42% <ø> (ø) Carriedforward from 421e660
amber 18.28% <ø> (-54.84%) ⬇️
computing-unit-managing-service 16.66% <ø> (-3.84%) ⬇️
config-service 63.57% <ø> (-3.10%) ⬇️
file-service 63.85% <ø> (-3.37%) ⬇️
frontend 83.05% <ø> (ø) Carriedforward from 421e660
notebook-migration-service 78.94% <ø> (ø)
pyamber 97.36% <ø> (ø) Carriedforward from 421e660
workflow-compiling-service 51.78% <ø> (+25.46%) ⬆️

*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.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

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

Compared against main 084309c 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 620 0.379 15,292/23,034/23,034 us 🔴 +17.5% / 🔴 +45.9%
bs=100 sw=10 sl=64 1,372 0.837 71,100/89,825/89,825 us ⚪ within ±5% / 🟢 +41.2%
🟢 bs=1000 sw=10 sl=64 1,609 0.982 619,018/647,831/647,831 us 🟢 -8.0% / 🟢 +60.7%
Baseline details

Latest main 084309c from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 620 tuples/sec 692 tuples/sec 771.62 tuples/sec -10.4% -19.6%
bs=10 sw=10 sl=64 MB/s 0.379 MB/s 0.422 MB/s 0.471 MB/s -10.2% -19.5%
bs=10 sw=10 sl=64 p50 15,292 us 14,789 us 12,626 us +3.4% +21.1%
bs=10 sw=10 sl=64 p95 23,034 us 19,611 us 15,786 us +17.5% +45.9%
bs=10 sw=10 sl=64 p99 23,034 us 19,611 us 19,344 us +17.5% +19.1%
bs=100 sw=10 sl=64 throughput 1,372 tuples/sec 1,362 tuples/sec 971.56 tuples/sec +0.7% +41.2%
bs=100 sw=10 sl=64 MB/s 0.837 MB/s 0.831 MB/s 0.593 MB/s +0.7% +41.1%
bs=100 sw=10 sl=64 p50 71,100 us 69,791 us 103,463 us +1.9% -31.3%
bs=100 sw=10 sl=64 p95 89,825 us 89,657 us 110,296 us +0.2% -18.6%
bs=100 sw=10 sl=64 p99 89,825 us 89,657 us 118,690 us +0.2% -24.3%
bs=1000 sw=10 sl=64 throughput 1,609 tuples/sec 1,553 tuples/sec 1,001 tuples/sec +3.6% +60.7%
bs=1000 sw=10 sl=64 MB/s 0.982 MB/s 0.948 MB/s 0.611 MB/s +3.6% +60.7%
bs=1000 sw=10 sl=64 p50 619,018 us 641,727 us 1,008,988 us -3.5% -38.6%
bs=1000 sw=10 sl=64 p95 647,831 us 704,356 us 1,055,260 us -8.0% -38.6%
bs=1000 sw=10 sl=64 p99 647,831 us 704,356 us 1,074,689 us -8.0% -39.7%
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,322.50,200,128000,620,0.379,15291.62,23034.14,23034.14
1,100,10,64,20,1457.60,2000,1280000,1372,0.837,71099.51,89825.34,89825.34
2,1000,10,64,20,12429.29,20000,12800000,1609,0.982,619017.52,647830.75,647830.75

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.

IcebergDocument iterators leak S3/Parquet input streams on probe and bounded reads

5 participants