Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
1179bce
feat: add allocation-accounted mpool ownership
aptend Jul 31, 2026
f18cabe
feat: bridge allocation accounts to hash build budget
aptend Jul 31, 2026
5676b9b
feat: propagate allocation accounts through vectors
aptend Jul 31, 2026
d246455
feat: propagate allocation accounts through expressions
aptend Jul 31, 2026
3c9b701
feat: propagate allocation accounts through spill scratch
aptend Jul 31, 2026
70c2319
feat: add allocation-accounted streaming buffers
aptend Jul 31, 2026
4cb90d6
feat: account spill serialization buffers
aptend Jul 31, 2026
54c973b
feat: close retained vector allocation ownership gaps
aptend Jul 31, 2026
6c1dcb8
feat: account expression bitmap and conversion scratch
aptend Jul 31, 2026
d153bd6
perf: remove row scratch from field and values
aptend Jul 31, 2026
f4ccabf
perf: remove row-scaled function scratch
aptend Jul 31, 2026
f2b0645
feat: add allocation-accounted function scratch
aptend Jul 31, 2026
9fb49b3
perf: close data-scaled function scratch paths
aptend Jul 31, 2026
2f5870e
executor: finalize allocation accounts per attempt
aptend Jul 31, 2026
0938eea
executor: activate allocation-accounted hash tables
aptend Jul 31, 2026
c74d085
executor: account retained join-map batches
aptend Jul 31, 2026
8a71c16
executor: activate accounted hash-build expressions
aptend Jul 31, 2026
ca3b6e7
executor: account spill and runtime-filter memory
aptend Jul 31, 2026
2831273
executor: unify allocation pressure recovery
aptend Jul 31, 2026
e27a332
test: close allocation accounting performance matrix
aptend Jul 31, 2026
1f6cc29
[RFC] allocation-accounted memory admission
aptend Jul 30, 2026
a3aed96
docs: record dormant vector accounting validation
aptend Jul 31, 2026
b8ae526
docs: record expression and spill accounting progress
aptend Jul 31, 2026
f88bfcf
docs: record spill and spool accounting closure
aptend Jul 31, 2026
8830b4b
docs: record bitmap and conversion accounting closure
aptend Jul 31, 2026
7f58b95
docs: record direct-output scratch removal
aptend Jul 31, 2026
6af8b82
docs: record direct function scratch closure
aptend Jul 31, 2026
11f8fc3
docs: update allocation closure implementation evidence
aptend Jul 31, 2026
ba0e217
executor: make allocation activation owner-atomic
aptend Jul 31, 2026
c354031
docs: record allocation activation validation
aptend Jul 31, 2026
3e0cb4f
executor: close accounted vector ownership boundaries
aptend Jul 31, 2026
9ed2cca
test: restore cross-package spill fixture writer
aptend Jul 31, 2026
3a065b7
executor: finalize allocation owners before release
aptend Jul 31, 2026
81426a0
docs: refresh allocation activation closure evidence
aptend Jul 31, 2026
c39f49a
executor: close transaction PK ownership boundary
aptend Jul 31, 2026
f8af2d6
executor: preserve mixed batch allocation provenance
aptend Jul 31, 2026
8ca209c
docs: record allocation provenance closure
aptend Jul 31, 2026
31771e0
executor: converge memory admission on physical ownership
aptend Jul 31, 2026
edc2731
executor: close allocation-account CI regressions
aptend Aug 1, 2026
c7ee02b
fix: account expression and join result storage
aptend Aug 1, 2026
d50c340
executor: finalize remote allocation accounts per statement
aptend Aug 1, 2026
3cf0777
fix: preserve repeated vector alias decoding
aptend Aug 1, 2026
0abf799
executor: close statement result ownership boundaries
aptend Aug 1, 2026
6d26e8b
executor: reconcile physical admission after main rebase
aptend Aug 1, 2026
ea5b314
executor: close allocation accounting review gaps
aptend Aug 1, 2026
258010d
docs: record allocation accounting benchmark evidence
aptend Aug 1, 2026
741f07d
fix: finalize accounted pipeline spools after consumers stop
aptend Aug 1, 2026
60d6544
fix: release accounted prepared result buffers
aptend Aug 2, 2026
74923c4
fix: release consumed merge-order batches
aptend Aug 2, 2026
4cdd43c
perf: remove accounted hash-build copy amplification
aptend Aug 2, 2026
6cb564d
Merge branch 'main' into feature/26459-statement-lifecycle
XuPeng-SH Aug 2, 2026
8ce077f
perf: coalesce equivalent accounted batches
aptend Aug 2, 2026
1129ec8
fix: preserve sparse bitmap row capacity on duplicate
aptend Aug 2, 2026
2aefd52
fix: reject late remote fragments after abort
aptend Aug 2, 2026
d13b910
fix: preserve vector bitmap extent on duplicate
aptend Aug 2, 2026
078d05a
fix: close allocation and remote generation review gaps
aptend Aug 3, 2026
f1bce32
fix: preserve statement errors through allocation cleanup
aptend Aug 3, 2026
bc7a715
merge main and integrate hashbuild spill recovery capacity
aptend Aug 3, 2026
0552adb
merge latest main shuffle scope fix
aptend Aug 3, 2026
60ae274
fix vector unmarshal nil checks
aptend Aug 3, 2026
cd36bbb
fix(hashbuild): admit direct spill by allocation
aptend Aug 3, 2026
95baebc
ci: skip onnxruntime for branch validation images
aptend Aug 3, 2026
57811ed
Revert "ci: skip onnxruntime for branch validation images"
aptend Aug 3, 2026
42ac0d4
Merge branch 'main' into feature/26459-statement-lifecycle
aptend Aug 3, 2026
bf244d6
fix(compile): bound remote execution tombstones
aptend Aug 3, 2026
8efb7a3
Merge remote-tracking branch 'origin/main' into feature/26459-stateme…
aptend Aug 3, 2026
e97b797
Merge branch 'main' into feature/26459-statement-lifecycle
mergify[bot] Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions docs/design/allocation_accounted_memory_admission_impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Allocation-accounted HashBuild memory admission

Issue: #26459

## Goal

Within the controlled domain, HashBuild and its join consumers must admit the
bytes they physically retain, not a predicted multiple of logical payload size.
A query may spill or degrade an optional optimization when the retained
allocation cannot be admitted, but must not fail because an estimator guessed
a larger, non-existent allocation.

The implementation has one production path:

1. the MPool performs the physical allocation;
2. the allocation carries immutable account, owner, and site provenance;
3. the account charges the query generation and the CN aggregate controller;
4. the same allocation releases the charge when MPool frees it.

There is no feature switch, activation gate, or parallel compatibility ledger.
Physical storage is charged only by its allocation. A shuffle HashBuild may
additionally reserve a conservative recovery floor before accepting spillable
retained state; that floor proves the retained state can be drained under a
shared budget. A capacity failure while growing the floor changes the decision
from retain to direct spill; lifecycle and invariant failures remain terminal.
The projection is never reapplied to an upstream-owned direct source as a
query-fatal estimate. Cardinality estimates may likewise select a hash-table
capacity without creating a separately releasable physical allocation charge.

## Scope

The account covers retained physical storage owned by the HashBuild execution
family:

- hash table cells, descriptors, iterator keys, and selection lists;
- copied build batches and retained unique keys;
- JoinMap-owned batches and grouping metadata;
- HashJoin, LoopJoin, DedupJoin, and RightDedupJoin result/finalize state;
- join matched/capture state;
- Product result state;
- runtime-filter payloads until ownership transfer;
- spill encode/decode/scatter buffers and rebuilt retained state.

HashBuild and join expression trees receive the same account before
construction. Their fixed/variable values, nested function results, selected
row buffers, list results, and other owned MPool vectors allocate directly
through it. Function-result wrappers retain the immutable selection even when
the current vector is transferred, so later reuse cannot fall back to an
unaccounted vector. Borrowed input and serialized-plan vectors keep their
source ownership.

Library-internal Go heap objects remain outside the controlled domain. This
explicit boundary avoids pretending that regexp, JSON, JQ, and other libraries
with no allocator/free hooks are exactly charged.

The account is therefore not advertised as total query RSS. Unobservable
library allocations must not be represented by an estimated charge inside
this exact, terminal-zero ledger.

ProductL2 scratch and native CPU/GPU index storage are also outside this first
controlled domain. ProductL2 still consumes an accounted JoinMap: those source
allocations keep their original provenance through transfer and are released
by JoinMap `Free`. Its additional search index and scratch remain under the
existing implementation until their CPU and GPU allocators expose one common
physical capacity contract. Adding only the visible Go buffers would claim
false exactness and create a partial second path.

## Ownership model

Each physical allocation has one owner and one release path. Provenance is
attached before the first owned allocation and cannot change while storage is
live. Views borrow storage and do not create another charge. Copies allocate
new storage in their destination account.

The main transfer boundaries are:

| Storage | Initial owner | Transfer | Terminal release |
| --- | --- | --- | --- |
| retained build batch | HashmapBuilder | JoinMap | JoinMap `Free` |
| hash cells/descriptors | HashmapBuilder | JoinMap | JoinMap `Free` |
| grouping selections | HashmapBuilder | JoinMap | JoinMap `Free` |
| spill file + disk/FD tokens | HashBuild | SpillBuildPayload | SpillEngine/file close |
| matched bitmap | parallel worker | BitmapMailbox/merger | merger or mailbox drain |
| Product build batches | producer JoinMap | Product | Product reset/free |
| runtime-filter payload | HashBuild | message board | message destruction |

Transfers are move-only. A successful send clears the sender's ownership; a
failed send leaves ownership with the sender. Cancellation seals mailboxes and
drains queued accounted objects before terminal validation.

## Execution lifecycle

Compile opens one allocation generation for each local statement attempt.
Every operator implementing `SetAllocationAccount` / `ClearAllocationAccount`
is an owner in that generation.

The sequence is:

1. collect owners from physical scope templates;
2. open the account with the live HashBuild capacity controller;
3. configure all owners atomically, rolling back in reverse order on failure;
4. attach parallel scan/load clones created during `runOnce` to the same
generation before worker `Prepare` runs;
5. execute and drain the message board;
6. clear owners in reverse order;
7. seal and finalize the account;
8. export exactly one terminal snapshot.

For a coordinator-local attempt, that sequence owns one MessageBoard. Remote
PipelineMessage handlers are different: all fragments of the same statement on
one CN share the board, and accounted JoinMap/result storage may remain owned by
a producer account until a sibling consumes it. The coordinator therefore
computes the complete physical scope graph's RPC count per CN, creates a unique
physical execution ID, and carries both in ProcessInfo. Each remote handler
registers before decoding/execution, stages its account and MPool after its
operators quiesce, and detaches the shared board before Compile cleanup. A
quiesced handler returns without waiting for siblings and adds one counted
pending terminal-memory-domain signal. This avoids a B-to-C-to-B response
dependency cycle.
The final fragment drains the board, completes every staged account, samples
every staged MPool, and publishes the aggregate plus a completion marker exactly
once. The coordinator reduces pending/completed markers independent of response
order; unresolved counts preserve every suppressed fragment MPool domain and
make the attempt explicitly partial instead of silently omitting memory.

If dispatch fails before every planned handler arrives, the first quiesced
fragment starts a bounded orphan-registration timer. Registration of the full
set cancels it. Expiry closes only that board generation; active registered
fragments retain their accounts until they quiesce, after which the final
registered fragment completes the group. If no response remains to carry a
completion marker, the already-returned pending marker preserves the missing
domain as an explicit partial result. A fragment failure aborts an incomplete
group without waiting for the timer and cancels active registered siblings.
This bounds the MessageCenter/group maps without sealing live
allocations or imposing an execution timeout on a fully registered query.
Prepared executions and retries use fresh execution IDs, so a stale group and
board cannot capture the next generation.

ProcessInfo without both topology and execution-ID metadata remains decodable.
Remote plans outside the accounted owner domain keep their legacy lifecycle;
plans containing an allocation-account owner are rejected explicitly. The
server never guesses a group size and never silently falls back to unaccounted
HashBuild execution. In the opposite upgrade direction, query-candidate
discovery already excludes CNs whose CommitID differs from the coordinator
binary, so a new coordinator cannot dispatch this lifecycle to an old handler.

Prepared statements and retries create a new generation. Reset frees all
generation-bound state; it does not carry an executor, bitmap, mailbox payload,
or allocation selection into the next attempt. Runtime parallel clones are
also registered as owners and are cleared before their reuse-pool release.

A valid terminal snapshot requires zero live bytes and zero live allocation
metadata. A mismatch suspends new admission until late physical frees drain the
tombstone; it is never converted into retryable capacity pressure.

## Capacity and pressure

The controller enforces both the query-generation cap and the CN aggregate
cap. Admission uses checked arithmetic and charges the physical MPool capacity
requested by the allocator.

Pressure reasons are typed and disjoint:

- memory capacity: reclaim, spill, reduce an unpublished input unit, or degrade
an optional runtime filter;
- account sealed/suspended: terminal lifecycle error;
- owner/site mismatch or allocator invariant: terminal correctness error;
- spill disk cap: spill-resource error, never a memory-reduction retry;
- spill FD cap: spill-resource error, never a memory-reduction retry;
- minimum input unit: terminal capacity error after monotonic progress is no
longer possible.

Retry is allowed only when progress is observable: retained bytes decrease,
spill epoch advances, input units shrink, or optional state is disabled. This
prevents a capacity loop from replaying the same publication or I/O.

Runtime filters are optional. If their retained payload cannot be admitted,
HashBuild publishes PASS and releases unpublished scratch. Required hash/join
state does not silently bypass admission.

## Spill resources

SpillEngine requires the same live budget generation as its producer. There is
no nil-budget file path.

Memory, disk bytes, and open file descriptors are separate physical resources:

- memory is charged by MPool allocations;
- each spill file owns one growable disk token;
- each open spill file owns one FD token;
- a file handoff moves both tokens with the file;
- close releases all three exactly once.

Recursive spill validates schema, framing, row conservation, queue bounds, and
file metadata. Repartitioning keeps only bounded control arrays plus admitted
scatter buffers. Test fixtures use the same builder copy and budget paths as
production.

## Grouping semantics

Grouping sentinels are a distinct key domain from ordinary zero/empty values.
HashBuild selects grouping-aware key encoding whenever input contains grouping
bits. Hashing preserves this distinction for partitioning, equality preserves
it in resident maps, copies preserve the bitmap, and ordering treats the
sentinel as SQL NULL for NULLS FIRST/LAST behavior.

Sample keeps its ordinary batched hash path unchanged. If grouping bits appear
after ordinary groups were already inserted, it lazily opens a grouping-aware
key domain and translates both maps' local IDs into one sample-pool ID space.
Its iterators are reused per batch, so alternating grouping bits do not create
one iterator allocation per row.

## Non-goals

- estimating or limiting total query RSS;
- charging regexp/JQ/JSON/library-internal Go heap as if it were exact;
- changing optimizer join selection;
- making spill as fast as a sufficient-memory in-memory join;
- using remote benchmark workflows as a correctness oracle.

## Completion criteria

The implementation is complete only when:

- no estimated HashBuild value is a query-fatal admission gate for unretained
work; the recovery projection is limited to the retain-versus-spill decision;
- every retained HashBuild/join allocation in the controlled domain has
immutable provenance;
- runtime parallel clones join the current attempt before `Prepare`;
- every transfer has exactly one owner after success and on cancellation;
- memory, disk, and FD rejection remain distinct;
- prepared/retry generations terminate independently at zero;
- remote fragments sharing one MessageBoard terminate at the per-CN statement
boundary, with incomplete dispatch bounded and old board generations unable
to close a replacement;
- local unit, race, build, vet, lifecycle, spill, and performance checks pass;
- independent reviews report no blocker or major correctness/performance issue.
Loading
Loading