Skip to content

fix: DH-23758: Close pushdown resources on error and result paths - #8727

Merged
lbooker42 merged 19 commits into
deephaven:mainfrom
lbooker42:engine/dh-23758-pushdown-resource-leaks
Oct 7, 2026
Merged

lbooker42 merged 19 commits into
deephaven:mainfrom
lbooker42:engine/dh-23758-pushdown-resource-leaks

Conversation

@lbooker42

@lbooker42 lbooker42 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the resource-leak and error-path findings grouped under T-14 of the pushdown review (DH-23367). Most of them share one cause: resources were released only on the success path, or never. They are fixed together to simplify back-porting, with one commit per finding.

Scheduler (PD-061, PD-063)

  • JobScheduler: if submit throws (for example, a pool that is shutting down rejects the task), the iteration is now closed and failed through onError. Previously the per-task reference was never released, so onComplete, onError and cleanup never ran, and the task's context leaked. A scheduler that runs tasks inline (ImmediateJobScheduler) can also throw from submit after the task ran. That task has already closed itself, so only a task that never started is treated as rejected.
  • JobScheduler: when several tasks fail, the later failures are now kept as suppressed exceptions on the one that is delivered, instead of being dropped.
  • ColumnRegion and AbstractTableLocation: a failing pushdown action or estimate now goes to onError instead of being thrown synchronously.
  • ImmediateJobScheduler: documents its drain order (most recently submitted first) and that it rejects submissions from another thread during a drain.

Filter driver and OR (PD-035, PD-036)

  • AbstractFilterExecution:

    • filter inputs are closed when replaced;
    • a fully resolving pushdown result is closed;
    • the deferred path's final filter output is closed;
    • the parallel filter result is closed if a segment fails.

    A partially resolving pushdown result is now owned by its StatelessFilter as soon as it arrives. This closes it on every failure path, including a failed re-sort or final filter, which the finding did not list.

  • DataIndexPushdownManager: the wrapped matcher's intermediate result is closed on both branches. When the index is skipped, it is closed exactly once, because a throwing consumer can route back into onError.

  • DisjunctiveFilter.orImpl: returns its accumulator instead of leaking it behind a copy, and closes it if a component throws. A disjunction with no components now matches nothing instead of everything.

Regioned, union and parquet (PD-018, PD-046, PD-019, PD-071, PD-024)

  • RegionedColumnSourceManager and UnionSourceManager: per-region and per-constituent row sets are also closed from onError, because the iteration's cleanup runs only after onComplete succeeds. cleanup itself is unchanged, since several callers already release the same resources in both cleanup and onError. Union constituents now keep their shifted selections in an array that both paths close, which also removes a copy. The union cost estimate records each constituent's cost in its own slot and takes the minimum on completion, instead of updating a shared minimum under a lock. Both union paths return early when no constituent that overlaps the selection supports pushdown.
  • PageStorePushdownHelper and ParquetTableLocation: close the RowSequence.asRowSet() intermediate. loadDataIndex closes the location row set copy when the location is flat and the copy goes unused.
  • Column regions build exact-match results with PushdownResult.exactMatch instead of passing an inline RowSetFactory.empty() that was never closed. ColumnRegionObject's lazily built dictionary regions are now volatile.

Python table data service (PD-063)

  • PythonTableDataService.readChunkPage closes the chunks it reads.

Not in this PR: PD-071(c) (IO during the dictionary estimate) and PD-071(d) (a data index cached under two names) are left to DH-23755, which replaces both code paths. PD-063(a), WritableBooleanChunk.sort() throwing for every input, is also left out: nothing calls it, and it is not a resource leak.

lbooker42 and others added 13 commits September 29, 2026 09:05
…submission

A throwing submit left the per-task reference unreleased, so onComplete, onError
and cleanup never ran, and the task's context leaked (PD-061).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er's result paths

AbstractFilterExecution replaced filter inputs without closing them, never closed
a fully-resolving pushdown result or the deferred path's final filter output, and
dropped its parallel filter result on error. DataIndexPushdownManager abandoned the
wrapped matcher's intermediate result on both branches (PD-035).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
orImpl returned a copy of its accumulator and never closed it, on every OR
evaluation and when a component threw; a disjunction of no filters returned the
whole selection rather than nothing (PD-036).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn fails

The regioned pushdown closed its per-region results only in the iteration's
cleanup, which runs after onComplete succeeds, so a failed region or a throwing
consumer abandoned the results already produced (PD-018).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…constituent fails

UnionSourceManager closed each constituent's selection copy only in its success
callback, and its per-constituent results only in the iteration's success
cleanup, so a failing constituent leaked both (PD-046).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The result of RowSequence.asRowSet() must be closed, but only its shifted copy
was, leaking one row set per overlapping region per pushdown action (PD-019).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and index loading

The dictionary pushdown never closed its RowSequence.asRowSet() intermediate,
and loadDataIndex never closed the location row set copy when the location is
flat and the copy goes unused (PD-071).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nclosed empty row set

Column regions passed an inline RowSetFactory.empty() to PushdownResult.of, which
copies it, leaving the temporary unclosed; use PushdownResult.exactMatch. Make the
object regions' lazily built dictionary regions volatile, so their unlocked
initialization publishes them safely (PD-024).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The replicator removed the boolean sort body, so the public sort() fell through
to the throwing default and failed for every input (PD-063).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ervice

readChunkPage never closed the chunks returned by getColumnValues, on success or
when it rejected them (PD-063).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ceptions

onTaskError kept only the first failure and silently dropped the rest (PD-063).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ColumnRegion and AbstractTableLocation took an onError consumer but let a
failing pushdown action or estimate throw synchronously instead (PD-063).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its depth-first drain order, and its refusal of submissions from a second thread
during a drain, were undocumented traps for asynchronous completers (PD-063).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lbooker42 lbooker42 self-assigned this Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

No docs changes detected for d47acdc

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.

Copilot review overview

🔵 Needs a closer look

It changes asynchronous scheduler and pushdown resource ownership across multiple engine and extension paths, warranting final human review.

Review effort: Balanced
Findings: None

What changed in this PR

Fixes resource ownership and error propagation across pushdown filtering, scheduling, Parquet, Barrage, and chunk sorting.

Changes:

  • Closes pushdown intermediates on success and failure paths.
  • Handles scheduler rejection and preserves suppressed failures.
  • Implements boolean chunk sorting with regression coverage.
File Description
replication/​static/​.../​ReplicateSourcesAndChunks.java Generates boolean sorting.
extensions/​parquet/​.../​ParquetTableLocation.java Closes temporary row sets.
extensions/​barrage/​.../​PythonTableDataService.java Closes returned chunks.
engine/​table/​.../​TestJobScheduler.java Tests rejection and failure handling.
engine/​table/​.../​UnionSourcePushdownTest.java Tests constituent cleanup.
engine/​table/​.../​TestRegionedColumnSourceManager.java Tests regional cleanup and errors.
engine/​table/​.../​QueryTableWhereTest.java Tests filter and OR cleanup.
engine/​table/​.../​DataIndexPushdownManagerTest.java Tests index-result cleanup.
engine/​table/​.../​JobScheduler.java Handles submission failures and suppression.
engine/​table/​.../​ImmediateJobScheduler.java Documents execution semantics.
engine/​table/​.../​UnionSourceManager.java Cleans constituent resources.
engine/​table/​.../​RegionedColumnSourceManager.java Cleans regional results.
engine/​table/​.../​PageStorePushdownHelper.java Closes row-set conversion.
engine/​table/​.../​ColumnRegionShort.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegionObject.java Safely publishes dictionary regions.
engine/​table/​.../​ColumnRegionLong.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegionInt.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegionFloat.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegionDouble.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegionChar.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegionByte.java Uses leak-safe exact matches.
engine/​table/​.../​ColumnRegion.java Routes action failures to callbacks.
engine/​table/​.../​DisjunctiveFilter.java Fixes accumulator ownership and empty OR.
engine/​table/​.../​AbstractTableLocation.java Cleans failed pushdown results.
engine/​table/​.../​DataIndexPushdownManager.java Closes intermediate matcher results.
engine/​table/​.../​AbstractFilterExecution.java Closes replaced and failed filter results.
engine/​chunk/​.../​TestChunkSort.java Tests boolean sorting.
engine/​chunk/​.../​WritableBooleanChunk.java Implements boolean sorting.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@lbooker42

lbooker42 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Test coverage of changes vs upstream/main

Files changed

File Lines Branches Changed lines
mod AbstractFilterExecution 245/254 (96%) 76/88 (86%) 15/15 (100%)
mod AbstractTableLocation 121/146 (82%) 33/52 (63%) 9/16 (56%)
mod ColumnRegion 96/115 (83%) 32/38 (84%) 14/25 (56%)
mod ColumnRegionByte 20/57 (35%) 2/16 (12%) 0/2 (0%)
mod ColumnRegionChar 18/52 (34%) 2/16 (12%) 0/2 (0%)
mod ColumnRegionDouble 20/52 (38%) 2/16 (12%) 0/2 (0%)
mod ColumnRegionFloat 20/52 (38%) 2/16 (12%) 0/2 (0%)
mod ColumnRegionInt 37/52 (71%) 8/16 (50%) 2/2 (100%)
mod ColumnRegionLong 22/52 (42%) 2/16 (12%) 0/2 (0%)
mod ColumnRegionObject 57/121 (47%) 11/48 (22%) 2/2 (100%)
mod ColumnRegionShort 18/52 (34%) 2/16 (12%) 0/2 (0%)
mod DataIndexPushdownManager 100/111 (90%) 28/32 (87%) 40/46 (86%)
mod DisjunctiveFilter 33/36 (91%) 16/18 (88%) 5/5 (100%)
mod ImmediateJobScheduler 22/23 (95%) 10/12 (83%) n/a
mod JobScheduler 118/143 (82%) 32/38 (84%) 17/18 (94%)
mod PageStorePushdownHelper 27/30 (90%) 5/8 (62%) 2/2 (100%)
mod ParquetTableLocation 429/457 (93%) 162/196 (82%) 5/11 (45%)
mod PythonTableDataService 0/369 (0%) 0/112 (0%) 0/17 (0%)
mod RegionedColumnSourceManager 376/406 (92%) 131/156 (83%) 4/4 (100%)
mod UnionSourceManager 471/483 (97%) 121/136 (88%) 44/48 (91%)

All new and modified files

0 new, 20 modified — lines 2250/3063 (73%), branches 677/1046 (64%)

All new and modified lines

223 executable lines added or changed — 159/223 (71%) covered

5 of these files are replicated (ColumnRegionDouble, ColumnRegionFloat, ColumnRegionInt, ColumnRegionLong, ColumnRegionShort); coverage fixes belong at the template.

Gaps

The most serious gap is a capability no test drives end to end: loading a Parquet data index for a location whose row set is not flat. Every Parquet file with more than one row group produces such a location, because each row group gets its own block of row keys, but the indexed files in the tests all have a single row group. So neither the position-to-row-key translation nor this change's cleanup of the location row set when building that index fails is exercised. Next is the Python table data service's chunk-closing fix, which is never entered from Java: that service is driven only from the Python server tests, which these numbers do not measure, so the fix is verified by inspection alone. The rest is mostly error handling this change added. A failed pushdown action is shown to reach onError for a column region, but nothing injects a failure into a column region's estimate, into either table-location path, into a context that fails to close, or into the data index manager's second round when its matcher throws instead of calling onError or the merge of its results fails. The union manager's early returns, taken when no constituent that overlaps the selection supports pushdown, are also never reached. The replicated constant-region pushdowns are covered only for int, and their byte, char, short, long, float and double siblings are never entered, though they run identical logic. One defensive branch remains: the scheduler rethrowing an exception from an inline submit after its task already ran.

Measured at d47acdc against upstream/main, over test, testOutOfBand, testParallel and testSerial in :engine-table (with the :extensions-parquet-table execution data folded into its report), test and testOutOfBand in :extensions-parquet-table, and test in :extensions-barrage.

lbooker42 and others added 2 commits October 2, 2026 12:13
This reverts commit 074926d. No caller sorts a boolean chunk, and the fix is
unrelated to the pushdown resource leaks this branch addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

Copilot review overview

🔵 Needs a closer look

It changes concurrent scheduling and resource ownership across several engine pushdown layers, warranting final human validation.

Review effort: Balanced
Findings: None

…hdown-resource-leaks

# Conflicts:
#	engine/table/src/test/java/io/deephaven/engine/table/impl/dataindex/DataIndexPushdownManagerTest.java
#	engine/table/src/test/java/io/deephaven/engine/table/impl/sources/regioned/TestRegionedColumnSourceManager.java
@lbooker42
lbooker42 marked this pull request as ready for review October 6, 2026 01:45
@lbooker42
lbooker42 requested review from cpwright and a balanced review from Copilot October 6, 2026 01:45

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.

Copilot review overview

🟡 Changes recommended

Cleanup exceptions can still escape synchronously, and two DataIndex failure paths can leak owned results.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)

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.

Suggested change
matched.subsume(filterMatched);

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.

The suggestion is wrong, in that it should replace the insert but has a - for an empty line. This is optional; as is the next one.

onError);
closeRegionResults,
e -> {
try (final SafeCloseable ignored = closeRegionResults::run) {

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.

Should you use your new SafeCloseable.closeAllOnFailure callback?

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.

Copilot review overview

🔵 Needs a closer look

The non-flat parquet index construction path still leaks its copied location row set when construction fails.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Close locationRowSet when adjusted index construction fails

extensions/​parquet/​table/​src/​main/​java/​io/​deephaven/​parquet/​table/​location/​ParquetTableLocation.java:355

If updateView or StandaloneDataIndex.from throws (for example, while validating a malformed index table), the copied locationRowSet has not acquired a successful lifetime owner and is never closed. Wrap construction of the adjusted index in a RuntimeException | Error catch that closes locationRowSet with SafeCloseable.closeAllDuringFailure before rethrowing.

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.

Copilot review overview

🔵 Needs a closer look

The data-index wrapper can still leak owned results when its nested matcher or result merge throws synchronously.

Review effort: Balanced
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Medium severity Prevent owned result leaks on matcher and merge failures

engine/​table/​src/​main/​java/​io/​deephaven/​engine/​table/​impl/​dataindex/​DataIndexPushdownManager.java:149

This branch still leaks owned results if the wrapped matcher fails synchronously: result is released only by the callbacks. It also owns nextResult when that callback starts, so an exception while merging the earlier matches leaks nextResult. Guard the invocation and close nextResult if the merge fails; ownership transfers onward only when onComplete is called.

                        wrappedMatcher.pushdownFilter(
                                filter,
                                result.maybeMatch(),
                                usePrev,
                                ctx.wrappedContext,

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.

Copilot review overview

🔵 Needs a closer look

It changes asynchronous ownership and failure semantics across several query-engine subsystems, warranting final human review.

Review effort: Balanced
Findings: None

@lbooker42
lbooker42 requested a review from cpwright October 6, 2026 23:56
@lbooker42
lbooker42 enabled auto-merge (squash) October 6, 2026 23:56
@lbooker42
lbooker42 merged commit 9603149 into deephaven:main Oct 7, 2026
27 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants