Repository navigation
Description
Describe the bug
NestedLoopJoinExec's memory-limited (spill) path allocates more memory than it reserves through the MemoryPool. The accounting-pool framework added in #22626 surfaces this if HEADROOM_FACTOR in the SLT pool is tightened — see #22721, which lowers it from 8.0 → 5.0 and trips this test.
To Reproduce
In datafusion/sqllogictest/src/accounting_pool.rs, set:
```rust
const HEADROOM_FACTOR: f64 = 5.0;
```
Then run nested_loop_join_spill.slt. First query at line 33:
```sql
SET datafusion.execution.target_partitions = 1;
SET datafusion.runtime.memory_limit = '150K';
SELECT count(*) as cnt, min(v1) as mn, max(v1) as mx
FROM generate_series(1, 100000) AS t1(v1)
INNER JOIN generate_series(1, 1) AS t2(v2)
ON (t1.v1 + t2.v2) > 0;
```
Expected behavior
Query succeeds with allocator usage bounded by the configured memory_limit (×~10% slop for legitimate untracked overhead like Tokio/Rayon thread state).
Actual behavior
```
External error: 1 errors in file datafusion/sqllogictest/test_files/nested_loop_join_spill.slt
- query failed: Other Error: allocator overdraft: account balance at panic = -20245 bytes
at nested_loop_join_spill.slt:33
```
Peak allocator usage reaches ~770KB against a declared 150KB pool — 5.13× the budget. The first ~750KB (5×) is absorbed by the headroom factor; the additional 20245 bytes is allocator usage that was never reserved through MemoryPool::try_grow.
Likely root cause
Spill path in NestedLoopJoinExec has at least these untracked allocation sites (need verification with targeted instrumentation):
generate_next_batchbuffering —RecordBatches accumulated for the build side before the spill decisionconcat_batchesat the spill boundary — copies into a coalesced batch before the IPC writer sees ittake_nativeduring the probe phase — gather kernels allocate output buffers- IPC reader path on spill re-read — the
StreamReader/FileReaderdecoder owns buffers that aren't accounted
The first query in the test stays small (100K × i32 = ~400KB build side), so the overshoot is whatever lives off-pool in the spill setup, not the bulk data itself.
Additional context
- Surfaced by the allocator-vs-pool tracker added in Track allocator-level memory vs MemoryPool during SLTs to prevent OOMs #22626.
- Demonstration PR: Lower SLT HEADROOM_FACTOR 8.0 -> 5.0 to surface nested_loop_join_spill leak #22721 (test-only: lowers
HEADROOM_FACTOR; expected to fail until this is fixed). - Fix should reserve memory through
MemoryReservationbefore each of the sites above, not bump the headroom constant — the constant is a leak detector, not a budget.
Component
- Physical Plan
Activity
avantgardnerio commentedon Jun 3, 2026
@comphead @andygrove the first of many issues that will probably be found tightening the ratchet. @alamb do we want an epic?
2010YOUY01 commentedon Jun 3, 2026
I think, given the current implementation conventions, this kind of validation is not feasible in this failing SLT.
Issue 1 -- operators don't track in-progress batches
Operators don't account for in-progress batch memory for simplicity. This is typically one batch inside each operator per partition. If the batches are wide or the partition count is high, this can become significant.
This query already contains a substantial amount of untracked in-progress memory:
generate_seriesholds one temporary batch: 8K rows * 8 bytes = 64KB(v1, v2, 0), plus a result batch: roughly 8K rows * 8 bytes * 3Given the current implementation, this behavior is expected and not specific to NLJ. Addressing it would require updating all operators to account for in-progress batches.
Issue 2 -- memory allocator noise
I'm assuming the 770KB measurement is allocator RSS. This may also include internal fragmentation, or memory that the allocator has not yet returned to the OS. In such cases RSS can appear high, even though the allocator could potentially release the memory immediately if the OS requests it.
Ideas on follow up works
Overall, I don't think issue 1 is easy to solve, and perhaps these very small memory-limited queries should not be used for allocator-vs-pool validation. They are still useful as correctness tests.
This kind of validation is more feasible when
memory pool limit >> single batch size. That likely means using memory limits in the hundreds of MB range and adding a separate extended test suite with dedicated test cases.Issue 2 remains a challenge, and I'm not sure how to make the validation more accurate. It likely requires a deeper investigation into allocator and OS memory accounting behavior.
avantgardnerio commentedon Jun 3, 2026
Thanks @2010YOUY01 , you raise some valid points. I'm working on a full response, but I wanted to just respond now to say I'm not ignoring you :)
comphead commentedon Jun 3, 2026
Thanks @avantgardnerio and @2010YOUY01
Hope this initiative would bring us closer to broader spilling support. I strongly agree for epic to group current spill tickets into single place
https://github.com/apache/datafusion/issues?q=is%3Aissue%20state%3Aopen%20spill
I would also prob add a spilling support for windows as well
avantgardnerio commentedon Jun 3, 2026
The tracker I just checked in was specifically aimed at catching memory pool regressions, but since it tracks all allocations one could imagine using a similar thing to better flag when things need to spill.
avantgardnerio commentedon Jun 3, 2026
@2010YOUY01 let me address issue 2 first, since that is a straightforward response. The AccountingAllocator tracks each alloc and free, so RSS / page bloat is not an issue.
andygrove commentedon Jun 3, 2026
I am experimenting with a version of the allocator / OOM guard in Comet in apache/datafusion-comet#4582
avantgardnerio commentedon Jun 3, 2026
@2010YOUY01 , re: issue 1 - PTAL at the actual issue for the DataFusion 51 regression that motivated this whole path. I should have lead with that, and I think it answers a few of your questions. Instead of doing that, I just cranked
HEADROOMdown to5and filed a bug for the first thing it hit (NLJ).Addressing your points (as I perceive them) not addressed by the above:
HEADROOMand anABS_HEADROOMand it doesn't fire unless it crosses both. I would lightly push back though because that means we specifically need to run all these with large amounts of data, which means we don't get the benefit of the existing suite.I think your other concerns (difficulty of tracking, size required to track, etc) are all addressed in the other issue. I hope I was able to give a proper response after I put in proper effort.
2010YOUY01 commentedon Jun 4, 2026
Thanks for raising these points. I agree this is an important issue to solve, but I think the proposed approach may be difficult to maintain with the current memory accounting model. Here are a few thoughts on:
The current operator-level memory accounting is intentionally incomplete. It mostly tracks buffers that can grow without a fixed bound, such as aggregation hash tables. It generally treats one in-flight batch per operator per partition as constant overhead and leaves it untracked. This can still become large when the operator pipeline is long or when the target partition count is high. Your validation approach would be valid if all of this memory were accounted for, but today's implementation trades completeness for simplicity.
If we do not fix that first, the
HEADROOM+ABS_HEADROOMmodel is wrong. It could produce false positives and make the tests hard to maintain.TLDR If we decide to continue with #22626, we will need to address all of these issues, i.e. track all allocations in the
MemoryPooland update all operators accordingly. The main question is whether we should do that.memory_limit >> batch_size * pipeline_length * partition_count, which would allow us to keep the operators simpler.alamb commentedon Jun 4, 2026
Maybe this is something we can track / test using a dedicated external tool (like SQL fuzzer) -- for example setup a memory pool as @avantgardnerio suggests in #22739 that compares the allocation reported by the OS and the allocation reported by the memory reservations and then compare them / find areas where they diverge too much
That would avoid the complexity of trying to unit test this type of thing in SLT / etc. but also give us coverage
alamb commentedon Jun 4, 2026
I filed an epic with a bunch of related memory items to try and coordinate some response
avantgardnerio commentedon Jun 4, 2026
Extreme Programming breaks everything down into:
IME, people almost always agree on 1 & 3. The principles are usually what takes time to come agreement about, so it's good and natural to be discussing what the optimal way to do this is.
So to avoid getting too abstract, I'll raise a specific grounded question about not doing SLTs:
When our customer runs a query like:
If we have memory based SLTs in place, we can translate that into:
And see an error like:
Then make a PR to fix the error , which stays in the regression suite forever.
So my question is: if we adopt another approach - like SqlFuzz, what would the workflow look like to do the same?
avantgardnerio commentedon Jun 4, 2026
@alamb and @2010YOUY01 - what if I introduced a
SET runtime.enable_headroom = 5.0that tests specifically opted into? Would that alleviate the concern? I think that would be better anyway as each test could 1. opt in 2. set it appropriatelyalamb commentedon Jun 4, 2026
To be clear I don't have anything for/against using SLT as a test harness, I am more concerned about the process and maintainability of the tests over the long term
This sounds like a reasonable approach to me provided:
enable_headroomsettingsFor example, Imagine a new contributor to DataFusion that writes a SLT test for some unrelated feature and it this error
I think it would be very unclear what to do (maybe in the age of LLMs it is easier).
Ideally we could make the error self explanatory and provide the answer of how to solve the issue. Something like this perhaps
And then have the ticket explain the backstory and have a list of places where memory was over committed
avantgardnerio commentedon Jun 4, 2026
@alamb I think that's a great solution, and a very reasonable compromise! I'll send a PR with the improvement shortly.
avantgardnerio commentedon Jun 5, 2026
@alamb I hope this PR addresses your concerns: #22776
2010YOUY01 commentedon Jun 5, 2026
I don't think the error message is the key issue. The problem is that this direction enforces an invariant that does not exist today.
More specifically, the bug reported by this issue — internal accounting showing 150 KB vs. actual allocation of 770 KB — is arguably a feature rather than a bug under the current convention. See:
datafusion/datafusion/execution/src/memory_pool/mod.rs
Lines 60 to 69 in 84bc876
I think it is fair to say that this convention may be a bad design, and I am +1 on exploring a direction to fix it. However, I do not think we should write tests that assume this convention has already been invalidated.
In particular, enabling this
sltcheck would effectively assert thatMemoryPoolaccounting must closely track actual allocator usage. That is inconsistent with both the current implementation and the documented convention, where the memory pool intentionally accounts for selected operator-managed memory rather than every byte actually allocated. So the test would not just catch regressions; it would implicitly change the contract by assuming a new accounting convention.If we can agree on that, I am happy to help provide pointers for exploring an alternative memory-accounting convention, and eventually enabling this
sltvalidation once the new convention is defined.avantgardnerio commentedon Jun 5, 2026
@2010YOUY01 thanks for the link to the policy, I was unaware. I agree enforcing stricter invariants in the test framework than in the documentation is a bad idea. I'll wait a bit to see if anyone else wants to respond, but otherwise I'll make a revert PR for all of it and close the related issues as "won't fix". Coralogix has a downstream workaround now that alleviates our need for memory tracking.
alamb commentedon Jun 8, 2026
I agree with @2010YOUY01 here -- trying to make DataFusion's memory model better by enforcing the desired invariants in SLT is probably not a good way to get a clear and coherent design -- we would probably left chasing down memory accounting discrepancies indefinitely
So let's make a revert PR
SOmething I am not quite sure about is if there is a bug in the current code (where the stated policy, however flawed, is not actually followed by the current code)?
avantgardnerio commentedon Jun 9, 2026
Closing in favor of #22834. Revert PR pending.
2010YOUY01 commentedon Jun 9, 2026
@alamb Yes, this is possible. The challenge is for
sltscale queries they can't be easily caught due to temporary batches noises. If we scale up the queries (GBs of memory), then those temporary batches memory become a small constant, and the accounted memory and actual memory can be validated against each other.We could definitely set them up in CI, ideally once we get more CI resources to run those slow and memory-consuming queries.
avantgardnerio commentedon Jun 9, 2026
Here is a revert PR: #22860
Revert custom allocator auditing of MemoryPool tracking in SLTs (apac…
Revert custom allocator auditing of MemoryPool tracking in SLTs (apac…