Repository navigation
[feature](be) Support spill for non-streaming window functions - #68500
Conversation
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
0e969ba to
83fdbf9
Compare
| DCHECK_GT(block.rows(), 0); | ||
| _rows += block.rows(); | ||
| if (_data_writer) { | ||
| return _data_writer->write_block(state, block); |
There was a problem hiding this comment.
[P2] Coalesce small blocks before writing analytic spill, following the hash join spill pattern
Once _data_writer exists, every incoming Block is immediately passed to write_block(), which serializes and ZSTD-compresses it and appends it to the file. _append_spill_rows() only splits oversized input blocks; it does not combine smaller ones. The initial spill() loop also writes each retained Block separately.
For a large window partition arriving in 128 KiB blocks with an 8 MiB spill buffer, this produces roughly 64 serialization/compression/append operations per 8 MiB of input, instead of one coalesced write. This is a call-count comparison, not a measured 64x slowdown; the extra per-block work can limit spill throughput for narrow rows or small input batches.
We already have a suitable pattern in the hash join implementation:
_partition_block()accumulates rows in aMutableBlock._execute_spill_partitioned_blocks()flushes whenallocated_bytes() >= spill_buffer_size_bytes()or a flush is forced.revoke_memory()forces the remaining buffers out so reclamation can make progress.
Please consider the same bounded buffering pattern here: combine small payload blocks in input order, flush at the size threshold, and flush the tail on revoke and before sealing/closing the writer. Include the buffer in memory tracking and revocable_mem_size(); already large blocks can retain the direct-write path. This can stay within the batch store without changing aggregate-update boundaries or peer offsets.
A focused test should cover multiple small appends, the final partial buffer, and forced reclamation. A small-block benchmark comparing SpillWriteBlockCount, serialization time, throughput, and peak memory would quantify the tradeoff.
…spill ### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: After a window function batch starts spilling, every appended Block was written to the spill file on its own, and each write serializes, ZSTD-compresses and indexes one Block. Input Blocks are usually far smaller than `spill_buffer_size_bytes` (8MB by default), so a large partition was written as many small spill blocks. On TPC-DS SF10000, one window sink instance wrote about 1.29M blocks of about 65KB each. In addition, `enable_force_spill` or a proactive threshold smaller than the buffer triggered a spill for every append after the first one. Once a batch is spilled, small Blocks are now merged into a write buffer in input order and written when the buffer reaches `spill_buffer_size_bytes`. A Block that already reaches that size flushes the buffer first and is written directly. `spill()` (used by revoke) and `seal()` always flush the buffer, the buffer is part of `revocable_mem_size()`, and a spilled store no longer takes the proactive spill path. Because a replayed Block can now be much larger than the batch size, the source slices it into `batch_size` rows before computing window results. Local benchmark with 50M narrow rows (two bigint columns), `spill_analytic_sink_mem_limit_bytes=1MB`: - `sum(v) over ()`: spill blocks 6152 -> 96, sink serialize time 958ms -> 388ms, source deserialize time 273ms -> 145ms, spill file bytes 38MB -> 11.8MB, query 1.81s -> 1.26s; sink peak memory 1.06MB -> 4.76MB, bounded by the spill buffer size. - `sum(v) over (partition by k % 8)`: spill blocks 6128 -> 192, spill file bytes 35.5MB -> 20.3MB, query 0.90s -> 0.76s. ### Release note None ### Check List (For Author) - Test: Regression test / Unit Test / Manual test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (34 passed), including new cases for coalescing small appends, the final partial buffer, forced flush on revoke, and replay slicing by batch size - `./run-regression-test.sh --run -d spill_p0` passed - Manual benchmark above; scaled-down TPC-DS validation passed with a 1MB spill threshold - Behavior changed: No - Does this need documentation: No Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Three gaps in the window function spill path: 1. The analytic source had no reservation for reading spilled data. Before a replay reads a new spilled record it relied on the generic previous-call estimate (at least 4MB), while opening a batch reader allocates a buffer for the largest serialized record and reading deserializes a whole record, each up to about `spill_buffer_size_bytes` after small Blocks are coalesced. The source now adds one spill buffer to its reservation when it is about to read a record of a spilled batch, and two when it is about to open the next batch (reader buffer plus deserialized Block). In-memory batches and partially consumed Blocks add nothing. 2. The peer group end buffers of the batch store and of the sealed batch used `std::vector`, which bypasses the query MemTracker, although they hold up to `spill_buffer_size_bytes` per task. They now use `DorisVector`. 3. After a batch was spilled the store no longer forced a spill on every append, so no test created the peer group sidecar file any more. New tests cover sidecar creation on revoke with two metadata Blocks, sidecar flushing at the 1MB spill buffer threshold while the rows stay in memory, and the replay reservation. ### Release note None ### Check List (For Author) - Test: Regression test / Unit Test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (36 passed) - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold - Behavior changed: No - Does this need documentation: No Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Before publishing an in-memory batch to the source, the window function sink spilled it whenever its revocable memory exceeded `spill_min_revocable_mem`, regardless of memory pressure. FE sends 4MB for that option by default, and with high-cardinality partitions every input Block that ends a partition is sealed as a batch, so rows with a wide column (for example a ~1KB STRING, about 16MB per 8160-row Block) wrote and read back every Block, turning an in-memory query into a disk-bound one. `spill_min_revocable_mem` is only a "worth revoking" threshold elsewhere; the per-batch in-memory limit of this operator is `spill_analytic_sink_mem_limit_bytes` (64MB by default). A sealed batch is now spilled only when it reaches `spill_analytic_sink_mem_limit_bytes`, the same limit that triggers the proactive spill of the open partition, so memory that cannot be revoked after publication stays bounded by that limit plus one Block. Also: - add UTs that keep a sealed batch above `min_revocable_mem` but below the sink limit in memory, and that trigger the proactive spill through `spill_analytic_sink_mem_limit_bytes` without forced spill or revoke; - assert the scheduler-facing `get_reserve_mem_size` of the analytic source in the replay reservation test; - let `~SpillFileWriter` close writers left open by a failed or cancelled query so that close failures are logged instead of discarded. ### Release note None ### Check List (For Author) - Test: Regression test / Unit Test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed) - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold - Behavior changed: No - Does this need documentation: No Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
|
/review-light |
There was a problem hiding this comment.
Complete static review of a1dcf05. I found two P2 memory-admission issues in the analytic source replay path, both commented inline. No existing P0/P1 inline finding still applies. The earlier P2 thread about small spill writes is addressed by the current write buffer and is not repeated.
Review checkpoints
- Goal and tests: the eligible path incrementally reduces supported full-partition aggregates, records NTILE/peer metadata, spills input when required, and replays rows in order. Dedicated BE unit cases and a 12-query regression suite cover in-memory, forced-spill, boundaries, nullable values, and peer groups. This review was static: I did not run builds, tests, or a cluster, and the checked-in results are author artifacts rather than independent runtime proof.
- Scope and reuse: the implementation uses the existing spill file and pipeline dependency interfaces. The async result writer declaration adjustment is explained by unity-build interaction. No unrelated change was identified.
- Concurrency and lifecycle: sink and source pipeline tasks hand sealed batches through the shared queue under
buffer_mutex; EOS usessink_eos_lock. I found no additional lock-order or wakeup defect. Writers are sealed before readers open, and close paths release readers and batch references. The first inline finding concerns workload-group admission of a non-revocable in-memory batch; the second concerns simultaneous data and peer-sidecar read allocations. - Configuration and compatibility: FE forwards the new session option through optional Thrift field 233; BE supplies the 64 MiB default and clamps it like adjacent spill limits. This is a per-query option, with no dynamic process setting or storage-format change. Optional Thrift delivery preserves the older-peer path.
- Parallel paths and conditions: spill-disabled, streaming, and unsupported function/frame cases retain the existing evaluator. Capability wrappers, frame gates, partition boundaries, nullable results, and NTILE's positive constant argument were traced; no separate correctness issue was substantiated.
- Test results and observability: the 12 checked-in outputs match across disabled, enabled, and forced-spill modes. The new profile counters distinguish eligible, spilled, and in-memory partitions. Existing tests do not exercise workload-group admission or measure the peak allocation when both replay files are open; those are the two inline findings.
- Persistence, writes, and performance: the change writes temporary spill files but does not alter table transactions, metadata persistence, or data visibility. Small-block coalescing now flushes on threshold, revoke, and seal, addressing the existing P2 performance thread. No other substantiated performance issue was found.
The supplied focus text is -light and names no additional code path; I still reviewed the full changed-file set. Two complete review rounds converged, and all accepted new findings are inline below.
| if (_current_batch && _batch_output_position < _current_batch->rows) { | ||
| return _current_batch->data_file ? spill_buffer_bytes : 0; | ||
| } | ||
| return 2 * spill_buffer_bytes; |
There was a problem hiding this comment.
[P2] Inspect the queued batch before reserving spill-reader memory. This returns two spill buffers at every batch boundary, even when the next batch was kept entirely in memory below the sink spill threshold. After publication its bytes are no longer sink-revocable, yet PipelineTask must reserve these unused reader buffers before _get_spill_block() can dequeue it. If a workload group rejects that reservation at its high watermark while the query has no revocable task, the only consumer can remain paused and hit the paused-queue timeout although replay would open no file. Base the reservation on the queued batch's data_file state, or otherwise let in-memory batches be consumed without a disk-read reservation.
| _batch_reader = _current_batch->data_file->create_reader(state, operator_profile()); | ||
| RETURN_IF_ERROR(_batch_reader->open()); | ||
| } | ||
| if (_current_batch->peer_group_file) { |
There was a problem hiding this comment.
[P2] Include the peer-group sidecar in replay memory reservation. A batch can have both data_file and peer_group_file; this opens both readers and _next_peer_group_end() retains a decoded sidecar Block before the same get_block() reads the data Block. Each reader also retains its own serialized buffer/PBlock, while _spill_replay_reserve_bytes() adds only two spill buffers for opening any batch. With many distinct peers, the sidecar Block alone can approach spill_buffer_size_bytes(), so the first replay can allocate beyond its reservation and hit normal memory limits. Account for the sidecar reader and Block when reserving replay memory.
… peer group sidecar ### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Two gaps in the replay memory reservation of the analytic source: 1. At every batch boundary the source reserved two spill buffers before it knew whether the next batch was spilled. A batch sealed below `spill_analytic_sink_mem_limit_bytes` is published in memory, and its bytes are no longer revocable by the sink; if a workload group rejects that reservation at its high watermark while the query has no revocable task, the only consumer stays paused until the paused-queue timeout although replaying the batch would open no file. The source now inspects the batch at the head of the queue under `buffer_mutex` and reserves two spill buffers for each of its spilled files (`data_file`, `peer_group_file`), nothing for an in-memory batch and nothing when the queue is empty. 2. The reservation ignored the peer group sidecar. Opening a batch opens the sidecar reader and reads a sidecar Block before the first data record, and the sidecar is read again during replay; each record holds up to `spill_buffer_size_bytes` of group ends, so a replay could allocate well beyond its reservation. The sidecar now gets the same two buffers when the batch is opened, and during replay one buffer is reserved exactly when the next `batch_size` slice contains a row beyond the last group end read so far. UTs assert the reservation for a data-only spilled batch, a batch with both files, an in-memory batch, and the slice that reads the second sidecar record while the rows stay in memory. The design document describes the reservation rule. ### Release note None ### Check List (For Author) - Test: Unit Test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed) - `build-support/check-format.sh`, `build-support/check-build-hygiene.sh`, `git diff --check` passed - Behavior changed: No - Does this need documentation: No
|
/review-light |
There was a problem hiding this comment.
Static review of PR #68500 at head 088339d. Seven new P2 issues are inline. No P0/P1 existing inline finding still applies; the three existing P2 threads were treated as duplicate fences, and their specific coalescing, in-memory queue reservation, and sidecar reservation cases appear addressed on this head.
Review status: Incomplete convergence. The maximum three full review rounds were completed, but round 3 still produced two distinct findings. Every candidate identified in these rounds was validated, deduplicated or dismissed, and all seven accepted findings are inline; another round was not permitted.
Critical checkpoints:
- Goal and tests: The new sink/store/source path supports partition-wide window reductions, ntile and peer results with in-memory and spilled replay. The inspected unit and regression cases cover those modes, and the 36 result sections are internally consistent across 12 queries and three modes. This review was static only: no build or test ran, and the regression fixture does not assert that physical spilling occurred. The memory-pressure cases in the inline findings remain untested here.
- Scope and conditions: The changed paths form a coherent analytic-spill feature plus aggregate capability, query-option, test and documentation changes. I traced eligibility and frame gates, partition and peer boundaries, nullable result handling, and supported versus legacy analytic paths; no separate row-result error was substantiated.
- Concurrency and lifecycle: Sink and source run in separate pipeline tasks; batch queue accesses use its mutex and dependency wakeups. M1 is a reservation-to-dequeue race despite individually locked accesses. I found no additional lock-order issue. Spill writers close before batches are published; reader, error, cancellation, LIMIT and temporary-file ownership paths were traced without a separate lifecycle finding. No cross-translation-unit static initialization was introduced.
- Memory, writes and performance: M2 and M4-M7 cover record-size admission, write scratch, stale or missing result estimates, and hidden revocable buffers. The earlier small-Block write-amplification thread is already addressed by coalescing. Spill files are local temporary data; this PR does not alter table writes, committed-data visibility, EditLog or transactions.
- Configuration and compatibility: The optional FE-to-BE query option, SQL/load forwarding, BE default and clamp, and fallback for unsupported frames were checked. No rolling-upgrade or dynamic-config defect was substantiated. Spill and memory profile counters provide diagnostic context; no separate observability issue was established.
The supplied -light focus was applied to prioritization while the complete changed-file inventory and the relevant upstream/downstream paths were reviewed.
| // spilled files, reads the first peer group record and then the first data record. | ||
| LockGuard lock(_shared_state->buffer_mutex); | ||
| if (_shared_state->spill_batches.empty()) { | ||
| return 0; |
There was a problem hiding this comment.
[P2] Keep the next batch stable across replay admission. After this empty-queue check returns zero, the sink can publish a spilled batch before the same source call finishes its current batch and dequeues the new one. PipelineTask has already completed its memory reservation, so opening that batch allocates its reader buffers and first decoded records without admission under workload-group pressure. Defer a newly arrived batch to a later scheduling round, or reserve from a stable batch snapshot.
| // allocates the buffer for the largest serialized record of the file and every read | ||
| // deserializes a whole record into a new Block. In-memory batches cost nothing to replay. | ||
| const auto spill_buffer_bytes = static_cast<size_t>(state->spill_buffer_size_bytes()); | ||
| if (!_current_batch || _batch_output_position >= _current_batch->rows) { |
There was a problem hiding this comment.
[P2] Reserve for the actual spilled record size. The sink can write a record larger than spill_buffer_size_bytes: one wide String row is unsplittable, and two ordinary sub-buffer Blocks can coalesce before the post-merge flush. SpillFileReader opens a buffer sized to the file's actual maximum record and then deserializes it, but this estimates only two configured buffers for a data-only batch (and one on later reads). With an 8 MiB buffer, one incompressible 32 MiB row already exceeds the 16 MiB opening estimate with the read buffer alone. Bound record size at write time or carry its actual maximum into replay admission.
| const int64_t last_peer_group_end = column.get_data().back(); | ||
| const int64_t slice_end = | ||
| std::min<int64_t>(_batch_output_position + state->batch_size(), _current_batch->rows); | ||
| return last_peer_group_end < slice_end; |
There was a problem hiding this comment.
[P2] Limit sidecar lookahead to the next data Block. This uses batch_size even when _next_replay_rows will return fewer rows at the current data Block boundary. If the loaded sidecar record ends at that boundary, the next call needs no sidecar read, yet it reserves one full spill buffer and can pause the only consumer at the workload-group high watermark. Use the actual next output row count when deciding whether another peer record will be read.
| local_state._reserve_mem_size = 0; | ||
| SCOPED_PEAK_MEM(&local_state._reserve_mem_size); | ||
| if (local_state._spill_enabled) { | ||
| return local_state._sink_spill(state, input_block, eos); |
There was a problem hiding this comment.
[P2] Reserve spill-write scratch before the first sink call. This new branch can force-spill its first ordinary buffer-sized Block, but get_reserve_mem_size still returns only the prior call's measured peak, which starts at zero. PipelineTask then skips sink memory admission while SpillFileWriter allocates a PBlock and serialized/compressed buffers; a peer-sidecar flush can do the same without data spill. Under workload-group pressure this can hit the hard limit instead of pausing for revocation. Estimate the pending write from the current Block and spill settings before entering this branch.
| size_t AnalyticSourceOperatorX::get_reserve_mem_size(RuntimeState* state) { | ||
| auto& local_state = get_local_state(state); | ||
| return OperatorX<AnalyticLocalState>::get_reserve_mem_size(state) + | ||
| local_state._spill_replay_reserve_bytes(state); |
There was a problem hiding this comment.
[P2] Recompute admission for a retained replay Block. The first read of a coalesced spill Block records a large reader-open/deserialization peak in the base operator estimate. On later calls that only cut a small batch_size slice from _replay_block, this override still requests that prior peak even though _spill_replay_reserve_bytes adds zero and no disk read occurs. At a tight workload-group watermark the sole consumer can be paused before it can produce a smaller estimate. Size the next call from its actual slice and result allocations instead of reusing the previous read peak.
| const auto bytes = local_state._batch_store->revocable_mem_size(); | ||
| const auto min_revocable_mem = static_cast<size_t>(state->spill_min_revocable_mem()); | ||
| return bytes > min_revocable_mem ? bytes : 0; | ||
| } |
There was a problem hiding this comment.
[P2] Expose staged spill bytes to revocation below 32 MiB. After a partition begins spilling, the write buffer and peer-end buffer can retain several MiB yet stay below the default spill_min_revocable_mem threshold, so this returns zero. PipelineTask would revoke at 512 KiB, and spill() can flush those buffers. If workload-group admission then pauses this sink while the partition remains open, no batch is sealed for the source and the pause handler finds no revocable task; it can time out. Report flushable bytes at least down to the pipeline's revoke floor under pressure.
| for (size_t i = 0; i < function_count; ++i) { | ||
| results[i] = _current_batch->result_types[i]->create_column(); | ||
| results[i]->reserve(rows); | ||
| if (_current_batch->strategies[i] == WindowSpillStrategy::PARTITION_CARDINALITY || |
There was a problem hiding this comment.
[P2] Admit the first in-memory replay for its result columns. A queued batch with no spill files adds zero to source reservation, and the first call gets only the 4 MiB base minimum. With the legal batch_size of 65,535, one narrow input Block can remain in memory while 32 Int64 window results reserve about 16 MiB here before insertion. Under a tight memory limit this bypasses admission and can hit the hard limit. Include next-slice row count and result widths/function count in the source estimate even for in-memory batches.
|
run buildall |
…spill ### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: After a window function batch starts spilling, every appended Block was written to the spill file on its own, and each write serializes, ZSTD-compresses and indexes one Block. Input Blocks are usually far smaller than `spill_buffer_size_bytes` (8MB by default), so a large partition was written as many small spill blocks. On TPC-DS SF10000, one window sink instance wrote about 1.29M blocks of about 65KB each. In addition, `enable_force_spill` or a proactive threshold smaller than the buffer triggered a spill for every append after the first one. Once a batch is spilled, small Blocks are now merged into a write buffer in input order and written when the buffer reaches `spill_buffer_size_bytes`. A Block that already reaches that size flushes the buffer first and is written directly. `spill()` (used by revoke) and `seal()` always flush the buffer, the buffer is part of `revocable_mem_size()`, and a spilled store no longer takes the proactive spill path. Because a replayed Block can now be much larger than the batch size, the source slices it into `batch_size` rows before computing window results. Local benchmark with 50M narrow rows (two bigint columns), `spill_analytic_sink_mem_limit_bytes=1MB`: - `sum(v) over ()`: spill blocks 6152 -> 96, sink serialize time 958ms -> 388ms, source deserialize time 273ms -> 145ms, spill file bytes 38MB -> 11.8MB, query 1.81s -> 1.26s; sink peak memory 1.06MB -> 4.76MB, bounded by the spill buffer size. - `sum(v) over (partition by k % 8)`: spill blocks 6128 -> 192, spill file bytes 35.5MB -> 20.3MB, query 0.90s -> 0.76s. ### Release note None ### Check List (For Author) - Test: Regression test / Unit Test / Manual test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (34 passed), including new cases for coalescing small appends, the final partial buffer, forced flush on revoke, and replay slicing by batch size - `./run-regression-test.sh --run -d spill_p0` passed - Manual benchmark above; scaled-down TPC-DS validation passed with a 1MB spill threshold - Behavior changed: No - Does this need documentation: No Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
Issue Number: None Related PR: apache#68500 Problem Summary: Three gaps in the window function spill path: 1. The analytic source had no reservation for reading spilled data. Before a replay reads a new spilled record it relied on the generic previous-call estimate (at least 4MB), while opening a batch reader allocates a buffer for the largest serialized record and reading deserializes a whole record, each up to about `spill_buffer_size_bytes` after small Blocks are coalesced. The source now adds one spill buffer to its reservation when it is about to read a record of a spilled batch, and two when it is about to open the next batch (reader buffer plus deserialized Block). In-memory batches and partially consumed Blocks add nothing. 2. The peer group end buffers of the batch store and of the sealed batch used `std::vector`, which bypasses the query MemTracker, although they hold up to `spill_buffer_size_bytes` per task. They now use `DorisVector`. 3. After a batch was spilled the store no longer forced a spill on every append, so no test created the peer group sidecar file any more. New tests cover sidecar creation on revoke with two metadata Blocks, sidecar flushing at the 1MB spill buffer threshold while the rows stay in memory, and the replay reservation. None - Test: Regression test / Unit Test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (36 passed) - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold - Behavior changed: No - Does this need documentation: No Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Before publishing an in-memory batch to the source, the window function sink spilled it whenever its revocable memory exceeded `spill_min_revocable_mem`, regardless of memory pressure. FE sends 4MB for that option by default, and with high-cardinality partitions every input Block that ends a partition is sealed as a batch, so rows with a wide column (for example a ~1KB STRING, about 16MB per 8160-row Block) wrote and read back every Block, turning an in-memory query into a disk-bound one. `spill_min_revocable_mem` is only a "worth revoking" threshold elsewhere; the per-batch in-memory limit of this operator is `spill_analytic_sink_mem_limit_bytes` (64MB by default). A sealed batch is now spilled only when it reaches `spill_analytic_sink_mem_limit_bytes`, the same limit that triggers the proactive spill of the open partition, so memory that cannot be revoked after publication stays bounded by that limit plus one Block. Also: - add UTs that keep a sealed batch above `min_revocable_mem` but below the sink limit in memory, and that trigger the proactive spill through `spill_analytic_sink_mem_limit_bytes` without forced spill or revoke; - assert the scheduler-facing `get_reserve_mem_size` of the analytic source in the replay reservation test; - let `~SpillFileWriter` close writers left open by a failed or cancelled query so that close failures are logged instead of discarded. ### Release note None ### Check List (For Author) - Test: Regression test / Unit Test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed) - `./run-regression-test.sh --run -d spill_p0` passed; scaled-down TPC-DS window spill validation passed with a 1MB spill threshold - Behavior changed: No - Does this need documentation: No Claude-Session: https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt
… peer group sidecar ### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Two gaps in the replay memory reservation of the analytic source: 1. At every batch boundary the source reserved two spill buffers before it knew whether the next batch was spilled. A batch sealed below `spill_analytic_sink_mem_limit_bytes` is published in memory, and its bytes are no longer revocable by the sink; if a workload group rejects that reservation at its high watermark while the query has no revocable task, the only consumer stays paused until the paused-queue timeout although replaying the batch would open no file. The source now inspects the batch at the head of the queue under `buffer_mutex` and reserves two spill buffers for each of its spilled files (`data_file`, `peer_group_file`), nothing for an in-memory batch and nothing when the queue is empty. 2. The reservation ignored the peer group sidecar. Opening a batch opens the sidecar reader and reads a sidecar Block before the first data record, and the sidecar is read again during replay; each record holds up to `spill_buffer_size_bytes` of group ends, so a replay could allocate well beyond its reservation. The sidecar now gets the same two buffers when the batch is opened, and during replay one buffer is reserved exactly when the next `batch_size` slice contains a row beyond the last group end read so far. UTs assert the reservation for a data-only spilled batch, a batch with both files, an in-memory batch, and the slice that reads the second sidecar record while the rows stay in memory. The design document describes the reservation rule. ### Release note None ### Check List (For Author) - Test: Unit Test - `ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'` (38 passed) - `build-support/check-format.sh`, `build-support/check-build-hygiene.sh`, `git diff --check` passed - Behavior changed: No - Does this need documentation: No
|
/review-light |
There was a problem hiding this comment.
Review complete at head 389d015ff948a26109e6ab5a02fcef935f55c2cc after two convergence rounds, a final 23-file sweep, and live-thread deduplication. One new P2 is inline below: a stale sink spill-write peak can keep an empty EOS call from publishing its final batch under sustained workload-group pressure. Seven still-applicable P2 admission issues already have inline threads (4226675241, 4226675247, 4226675253, 4226675257, 4226675264, 4226675269, 4226675278), so I have not reposted them. Earlier coalescing and initial queued-batch/sidecar reservation threads (4120354529, 4139868859, 4139868867) are addressed at this head. No existing P0/P1 inline finding remains.
Critical checkpoints:
- Goal and tests: The code adds capability-gated spill for eligible non-streaming window partitions. Static inspection of the BE cases and all 12 regression result groups supports the covered sum/count/avg/min/max, NTILE, peer-function, fallback, and streaming cases; each
.outgroup matches across spill-disabled, in-memory, and forced-spill modes. Those artifacts do not exercise the reported workload-group pause or prove runtime memory bounds. - Scope and reuse: The changes are focused on analytic sink/source, a separate batch store, aggregate capabilities, one query option, and tests/docs. They reuse the existing SpillFile, pipeline dependency, and aggregate interfaces; no unrelated product path was found in the authoritative diff.
- Concurrency and locks: Sink and source run in separate pipeline tasks. Sealed batches are published and dequeued under
buffer_mutex; EOS usessink_eos_lockwithout a reverse acquisition path found. The source is signalled after publication, and the sink is signalled when the queue empties. The new P2 concerns admission before that publication, not a lock inversion. - Lifecycle and static initialization: The sink closes writers before publishing each immutable batch; source completion/close releases readers and batch ownership, and unfinished writer destruction attempts close. No new cross-translation-unit static initialization dependency was found. Failure/cancellation cleanup was inspected statically; no fault-injection run was performed.
- Configuration and FE/BE compatibility: The session variable reaches ordinary SQL, INSERT SELECT, and CTAS through
toThrift; BE clamps the optional field and uses the same 64 MiB default when it is absent. Broker-load coordinator defaults also leave query spill disabled. No persisted format or function symbol changed; the optional Thrift addition has a default for mixed versions. This session option is evaluated for each query, so no running-query dynamic refresh is expected. - Parallel paths and conditions: Spill eligibility is gated by every function's capability and the supported frame. Streaming and unsupported nodes retain the prior path; no-ORDER-BY RANGE CURRENT ROW is treated as a whole-partition frame. Partition, peer, nullable, NTILE, and aggregate reset paths were rechecked without a distinct semantic defect.
- Results and coverage: The checked-in
.outdata is internally consistent across modes but its generation and correctness against an independent oracle were not verified here. BE tests cover in-memory/disk replay, multi-partition batches, nullable results, peer sidecars, revoke, and slicing. Workload-group admission, fault injection, and large-memory behavior need executed validation; the PR description's test and benchmark claims are author reports. - Observability and performance: Spill mode, partition, peer metadata, and common spill counters support diagnosis. Coalescing and batch-size slicing address obvious write/read amplification; existing inline threads and the new P2 identify the remaining admission risks. No benchmark was run in this review.
- Persistence and writes: This change writes temporary spill files, not table data or EditLog state; transaction, visible-version, delete-bitmap, and failover checkpoints are not applicable. The temporary-file writer/reader lifecycle and error propagation were reviewed as above. No separate data-visibility issue was substantiated.
- Other issues: No additional substantiated finding remained after deduplication and the final changed-file sweep.
The user focus file contained only -light and named no additional code concern; the complete PR was reviewed. Review work was static only, except the text-only check-build-hygiene.sh gate, which passed. No build, BE unit test, or regression test was run in this invocation.
| } | ||
|
|
||
| Status AnalyticSinkLocalState::_finish_spill_sink_call(RuntimeState* state, bool eos) { | ||
| if (eos) { |
There was a problem hiding this comment.
[P2] Recompute admission for the empty EOS call. A preceding non-EOS call can spill the open partition and leave a large PBlock/compression peak in _reserve_mem_size even after its buffers are flushed and revocable_mem_size() is zero. PipelineTask asks for that stale peak before entering this branch; if the workload group stays above its high watermark, the sink remains paused while the source waits for this unpublished batch, and the pause handler can cancel the query on timeout. Size the pending seal/close work from the current state instead of carrying the prior write peak into EOS.
|
run buildall |
Cloud UT Coverage ReportIncrement line coverage Increment coverage report
|
TPC-H: Total hot run time: 29366 ms |
TPC-DS: Total hot run time: 152378 ms |
ClickBench: Total hot run time: 24.45 s |
FE UT Coverage ReportIncrement line coverage |
|
run check_coverage |
| } | ||
| assert_cast<ColumnFloat64&>(*computed_result_columns[i]) | ||
| .get_data() | ||
| .push_back(_current_peer_group_result(i)); |
There was a problem hiding this comment.
[P2] Restore peer-range result filling in spill replay
This replaces the existing peer-range evaluation/fill pattern with per-row evaluation. In the original path, _get_next_for_unbounded_range() passes a peer/output-Block range to _insert_result_info(), and WindowFunctionPercentRank::insert_result_into_range() / WindowFunctionCumeDist::insert_result_into_range() compute the value once before filling that range. Here _current_peer_group_result(i) is called inside the row loop, repeating the function-kind branch and ratio calculation for every row, followed by an individual push_back().
Within a partition and peer group, _peer_group_start, _peer_group_end, _partition_start and _partition_end are unchanged, so both results are constant. With one million rows in one peer, output Blocks of 4096 rows, and both functions, the old path performs about 490 formula evaluations across 245 Blocks; this path performs two million source-level evaluations. These are code-level counts, not a measured query-time slowdown. The extra work also affects eligible queries that enable spill but never actually write to disk.
Please process each intersection of the current peer and output slice as a range, compute each peer-function result once, and reuse ColumnFloat64::insert_many_vals(value, count). _append_spill_results() already bounds this call by the partition. Inside this helper the next range length can be:
const int64_t pos = _batch_output_position + offset;
// Advance peer metadata while pos >= _peer_group_end.
const size_t count =
std::min<size_t>(rows - offset, _peer_group_end - pos);Fill that range and advance the local offset; keep _batch_output_position advancement in the existing caller. This preserves the current spill metadata and requires no new cross-call cache. Retain cancellation checks and validate groups crossing output Blocks and sidecar records. A focused benchmark with few large peers versus one-row peers would quantify the CPU/query-time effect.
Original batch-fill implementations:
### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Window spill replay estimated a retained input slice from serialized row bytes. A NULL fixed-width nullable value serializes without its nested value, but ColumnNullable::cut copies both the null map and nested storage, so a wide all-NULL slice could be admitted with too little memory. Use the in-memory type width for non-constant fixed-width columns while preserving the existing variable-width and constant paths. Replay also evaluated PERCENT_RANK and CUME_DIST once per row even though each peer/output intersection has a single result; fill that intersection in one operation while retaining cancellation checks. Add focused storage-budget and cross-Block/sidecar peer tests. ### Release note Window spill replay accounts for fixed-width nullable input slices and fills peer results by range. ### Check List (For Author) - Test: Regression test and compile checks - Regression test: spill_p0/window_function_spill passed with an isolated ASAN BE and FE cluster. - Unit Test: focused test source compiled; execution was blocked by pre-existing function_string_test.cpp compile errors in the standard BE UT runner. - Manual test: None. - Behavior changed: Yes (spill replay reservation and peer-result fill strategy; SQL results unchanged). - Does this need documentation: No.
|
/review |
There was a problem hiding this comment.
Reviewed all 27 changed files at af6a803 with two general-scope reviewers and a separate admission/concurrency reviewer. One new P2 is reported inline: a revocation test's input allocation reaches the direct-write threshold, leaving zero revocable bytes and invalidating its expectation. This is a static code-path proof; no build or runtime test was executed.
Four previously reported concerns still apply and are not reposted: retained NTILE parameter accounting, combined coalesced-write admission, the separate first-slice reservation test expectation, and a residual case of initial replay scheduling. The scheduler change covers the sink and later source submissions, but an initial source submission can occur before sink open sets spill_enabled; publication before that source reaches its dependency check can still permit synchronous replay I/O in the simple pool. There are no existing P0/P1 comments to carry forward. The current head addresses the previous adaptive-boundary-search, peer-range-fill, fixed-width nullable-cut, fixture-manager-lifetime and other concrete replay-admission reports.
Critical checkpoint conclusions:
- Goal and proof: eligible full-partition reductions, NTILE and peer functions use collection/sealed-batch/replay; streaming and unsupported windows retain the existing path. Positive tests support the intended behavior, subject to the inline test defect and existing concerns above.
- Scope and reuse: changes are focused on analytic spill, shared writer measurements, one query option, tests and design documentation; aggregate update/result and spill-file infrastructure are reused.
- Concurrency and locks: sink/source publication is protected by buffer_mutex, EOS by sink_eos_lock, and eligibility is atomic. I/O runs outside queue locks; no new lock inversion was verified. Per-task revoke gating prevents simultaneous store mutation. Initial scheduler classification retains the existing concern described above.
- Lifecycle and initialization: seal closes writers before publication; source owns immutable result descriptors and closes readers before releasing files. Store writers are destroyed before their files; the fixture manager now outlives fixture-owned state. No new ownership cycle or cross-TU initializer dependency was identified. NTILE metadata accounting remains an existing issue.
- Configuration: the session threshold is forwarded per query, defaults to 64 MiB and is clamped in BE to 1 MiB through 4 GiB; a newly submitted query observes its session value without a process restart.
- Compatibility: no durable storage or existing spill-record format changes; record/footer maxima are process-local descriptor metadata and existing block serialization remains in use.
- Thrift/protobuf: read TQueryOptions on HEAD and the current branch-3.1, branch-4.0, branch-4.1 and branch-4.2 refs. New field 237 is optional and unused on those releases; it introduces no field-id collision.
- Parallel paths: supported capabilities and both nullable adapters are covered; variable-length MIN/MAX, unsupported mixtures and streaming/offset frames fall back. Local and remote spill share the writer/reader implementation; remote coalescing is accounted separately.
- Conditions: frame selection, complete-key boundary search, spill thresholds and peer-range filling have concrete invariants/comments. Required upstream ordering makes complete-key equality contiguous, including composite and nullable keys.
- Test coverage: reviewed eligibility, reductions/NULLs, scalar parameters, cross-Block partitions/peers, coalescing, oversize records, revoke, replay slicing and admission cases, plus 15 regression queries in three modes. Runtime fault injection, remote/early-close cleanup, tracked-memory-drop and adversarial numeric/performance validation remain unexecuted or absent from the added tests.
- Expected results: all 45 regression output sections agree across modes; independent arithmetic checks agree with the displayed results. The inline staged-buffer test mismatch and already-reported replay-cut expectation need correction. Output-generation provenance was not independently established.
- Observability: mode/reason, partition counts/rows/buffered bytes, peer metadata and replay timing supplement existing spill counters and contextual error logs. Profile counters do not remedy missing MemTracker accounting.
- Transactions and persistence: no EditLog, commit, visible-version, delete-bitmap or master-failover protocol changes.
- Data writes and failures: writes are temporary query spill files; failed writes/reads propagate failure and unsuccessful seal prevents publication. Cleanup reuses existing SpillFile ownership/GC; committed table data is unchanged.
- FE/BE propagation: SessionVariable forwarding/toThrift, the optional query option and RuntimeState consumption are connected; point-query and constant-folding paths do not execute analytic windows.
- Performance: adaptive key search, range result filling, small-block coalescing and sliced replay were traced; no benchmark or runtime memory bound was measured. Combined-write admission remains the existing concern above.
- Additional review and user focus: rechecked boundary/NULL/scalar, snapshot/EOS, cancellation/LIMIT and ownership cases. No additional user focus was supplied and no other distinct issue was verified.
Static coverage is complete for this invocation. Runtime behavior, sanitizer results and performance are not certified by this review.
Review converged after 2 round(s).
| Block second = ColumnHelper::create_block<DataTypeInt64>(values); | ||
| status = sink->sink(state.get(), &second, false); | ||
| ASSERT_TRUE(status.ok()) << status.to_string(); | ||
| EXPECT_GE(sink->revocable_mem_size(state.get()), SpillFile::MIN_SPILL_WRITE_BATCH_MEM); |
There was a problem hiding this comment.
[P2] Keep the staged test block below the spill-buffer threshold
ColumnHelper::create_column inserts these 100,000 Int64 values into a doubling PaddedPODArray, so the second Block allocates 1,048,576 bytes, exactly the configured 1 MiB spill buffer. The first call has already created the writer, so _write_data_block() takes its allocated_bytes() >= spill_buffer_bytes direct-write branch for this second Block. No data or peer buffer remains, revocable_mem_size() returns zero, and this assertion fails instead of exercising the intended 512 KiB revocation case. Use a 2 MiB spill buffer here, or a second Block whose allocation is at least 512 KiB and strictly below 1 MiB. This is separate from the already-reported first-slice reservation expectation at line 1832; the failure follows statically from the allocation and write branches.
|
run buildall |
1 similar comment
|
run buildall |
FE UT Coverage ReportIncrement line coverage |
TPC-H: Total hot run time: 28344 ms |
### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Window spill peer replay fills a constant result for each peer/output intersection, but retained row-based cancellation checks by splitting fills at 4096-row boundaries. Check cancellation before each fill instead and let the peer and output boundaries determine the fill size. Replay output is already bounded by the runtime batch size. Add unit coverage for cancellation before filling, a large peer filled without artificial row splits, and cancellation between uneven peers. SQL results are unchanged. ### Release note None ### Check List (For Author) - Test: Unit test compilation and ASAN BE build - Unit Test: Added two focused peer-fill tests. The modified source and test translation units compiled through run-be-ut.sh, but test execution was blocked by existing function_string_test.cpp compilation errors. - Manual test: ASAN ./build.sh --be -j16, clang-format v16, build hygiene, and git diff --check passed. clang-tidy was attempted but blocked by the toolchain's missing stddef.h resource header. - Regression test: Not run for this cancellation-check refactoring. - Behavior changed: Yes (check cancellation before each peer/output fill instead of every 4096 output rows; SQL results unchanged). - Does this need documentation: No.
TPC-DS: Total hot run time: 150505 ms |
ClickBench: Total hot run time: 25.14 s |
### What problem does this PR solve? Issue Number: None Related PR: apache#68500 Problem Summary: Window spill retained one identical NTILE bucket argument for every partition even though FE requires a positive integer literal per function. The nested standard vectors used O(functions * partitions) untracked parameter storage and required per-partition append and lookup. Cache each constant once on the first nonempty input, copy one allocator-aware scalar array into each batch, and replay by function index. Remove the two partition-parameter states and make the NTILE admission allowance independent of the partition count. For 32 functions and 65,535 partitions, the parameter payload per descriptor falls from approximately 16 MiB to 256 bytes; no query-level performance benchmark is claimed. Partition boundaries and per-partition reduction results are unchanged. ### Release note None ### Check List (For Author) - Test: ASAN BE build; 6 NTILE unit tests pass; window_function_spill regression generated and rerun on isolated ports, with 16 queries matching in no-spill, in-memory replay, and forced-spill modes; clang-format v16, build hygiene, and changed-line clang-tidy pass. - Broader analytic unit run: 62/65 pass; all three memory assertion failures reproduce with identical values on parent a225ec4. Standard full UT compilation is blocked by existing function_string_test.cpp API errors; analytic UTs use a temporary source filter with run-be-ut.sh, restored after testing. - Behavior changed: No SQL result changes; remove redundant parameter storage. - Does this need documentation: No external documentation; internal window spill design documentation updated.
### What problem does this PR solve? Issue Number: N/A Related PR: apache#68500 Problem Summary: The PARTITION_CARDINALITY name suggests support for any window function that needs a partition's row count, but this replay path implements NTILE only. Rename the strategy to NTILE and name its cached parameters ntile_bucket_counts across sink, batch metadata, source, tests, and design documentation. Preserve the enum values and replay behavior. ### Release note None ### Check List (For Author) - Test: ASAN BE build; focused unit tests; window_function_spill regression - Unit Test: 74/74 pass together with the following BE UT admission fix; rename alone reproduced the same three pre-existing admission failures - Regression test: all 48 existing query tags pass - Behavior changed: No - Does this need documentation: Yes (design document updated in this commit)
### What problem does this PR solve? Issue Number: N/A Related PR: apache#68500 Problem Summary: BE UT build 1081123 reported three window spill admission failures. Fixed-width output reservations counted row payloads but missed PaddedPODArray padding and power-of-two allocation rounding. For 65535 rows, a null map needs 131072 bytes, exceeding even twice the row count. Reuse the PODArray allocation-size calculation for fixed-width cuts, result columns, and added null maps. Keep allocation behavior unchanged and test the size calculation against real allocations around power-of-two boundaries. Correct two stale test setups: use a 512 KiB second block to actually leave staged data below the 1 MiB writer threshold, and account for both input cuts as well as the result column during in-memory replay. Do not change spill thresholds or relax the actual-allocation lower-bound assertions. ### Release note Improve memory reservations for fixed-width window spill output buffers. ### Check List (For Author) - Test: ASAN BE build; Unit Test; Regression test - Unit Test: 74/74 focused analytic, boundary and PODArray tests pass - Regression test: window_function_spill passes all 48 existing query tags - Full local BE UT source set not run; used a restored temporary focused source list due to pre-existing unrelated string-test API build errors - Behavior changed: Yes (reserve padded allocation capacity before output) - Does this need documentation: No
|
run beut |
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
|
/review |
FE Regression Coverage ReportIncrement line coverage |
There was a problem hiding this comment.
Static review of PR #68500 at bef288e. One new P2 finding: replay under-admits cuts of arrays with nullable elements. Existing scalar fixed-width sizing is repaired, but the recursive complex-column fallback still uses NULL-eliding serialization size as a bound on copied storage.
Critical checkpoint conclusions:
- Purpose and evidence: the capability-gated collector/replayer implements the intended supported non-streaming windows; the added tests exercise in-memory and spilled execution. Memory admission remains incomplete as described inline and in the known threads below.
- Scope and reuse: the change is substantial but focused on analytic spill, shared aggregate capabilities, and its query option; it reuses Block and the existing spill infrastructure.
- Concurrency and locks: queue publication/dequeue use buffer_mutex, EOS uses sink_eos_lock, and execution mode is atomic. The reviewed buffer-to-EOS lock order is consistent. I/O occurs outside the handoff locks; task execution and revocation exclude concurrent mutation of sink-owned state. No additional race or deadlock was substantiated.
- Lifecycle and initialization: writers close before immutable batch publication; readers close on source cleanup. File ownership is acyclic, writer/file destruction order is sound, and fixture teardown now keeps SpillDataDir alive. No new cross-translation-unit static initialization dependency was found.
- Configuration: the forwardable per-query option defaults to 64 MiB and BE clamps it to 1 MiB through 4 GiB. A new query observes its session setting without a process restart.
- Compatibility: no persistent storage or spill-record format change was found. Older receivers can ignore the new optional query option, and absent values use the BE default. Final ELF/glibc compatibility was not assessed in this static-only review.
- Thrift/protobuf: complete TQueryOptions definitions were compared with the live branch-3.1, branch-4.0, branch-4.1 and branch-4.2 refs. Optional i64 field 237 is unused on those releases; no new collision was found. Historical mismatches in other fields predate this PR.
- Parallel paths: disabled, streaming and unsupported nodes retain the existing executor. Eligible in-memory batches and local/remote data/peer files were traced. The source snapshot pins the batch admitted before the later EOF transition; newly published unadmitted batches are deferred.
- Conditions and invariants: frame gates, full composite-key boundaries, final peer sealing and NTILE positive-literal assumptions have concrete upstream guarantees. I/O errors propagate instead of publishing partial batches.
- Tests: BE cases cover eligibility, NULL reductions, distinct NTILE constants across partitions/batches, adaptive boundaries, replay slicing, sidecars, revoke and cancellation boundaries. Regression coverage spans 16 query families in three modes. Direct remote analytic spill, fault-injected write/close/read/GC failures and actual tracked-memory decline after revoke remain test gaps.
- Expected results: all 48 checked-in regression sections were inspected; matching query families have identical outputs across modes. Prior fixture/threshold/reservation-expectation defects are addressed on this head. Output regeneration and passing runtime results were not verified here.
- Observability: spill counters plus mode/reason, partition counts, peak buffering, peer metadata and replay timing cover the new paths; no new hot per-row logging was found.
- Transactions and persistence: no EditLog, failover, committed-data visibility, visible-version or delete-bitmap path changes apply.
- Writes and crash behavior: only disposable query spill files are added; publication follows successful close, and cleanup uses the existing spill infrastructure. No new transactional data-write behavior applies.
- FE/BE propagation: SessionVariable.toThrift and RuntimeState are connected. Constant-folding and point-query paths do not execute this analytic operator.
- Performance and memory: adaptive complete-key search, coalesced writes and peer-range fills avoid the previous repeated work; scalar NTILE storage is compact. The new array-cut finding and already-known admission concerns remain; no measured performance claim is made by this review.
- Other concerns: nullable adapters, decimal/floating accumulation order, bucket counts exceeding partition size, no ORDER BY, multiple partitions per Block and early termination were rechecked without another verified correctness finding.
Existing review context: coalesced-write scratch (4348511978), sink partition-result capacity (4347489392), and the broader constant-argument materialization concern (4348010167) remain duplicate-fenced and are not reposted. The specific NTILE expansion/vector, fixed-width nullable slice, peer-range filling, adaptive boundary search, scheduler classification, fixture teardown and test-expectation examples have been addressed. No existing P0/P1 inline finding was present to carry forward.
User focus: no additional focus was supplied; all 29 changed paths and applicable skill checkpoints were reviewed. The review converged after three rounds, including an independent pass over the corrected frozen payload. The concrete inline trigger explicitly sets minimum_operator_memory_required_kb=4096; FE normally forwards 32000 KiB, whereas 4 MiB is only the BE fallback when the option is absent. No unresolved review candidate remains. Validation is static only: no builds, tests, ASAN execution, remote spill execution or final-binary ABI audit was performed.
Review converged after 3 round(s).
| bytes += padded_rows * column.type->get_size_of_value_in_memory(); | ||
| } else { | ||
| for (size_t row = cut_start; row < cut_start + rows; ++row) { | ||
| bytes += 2 * column.column->serialize_size_at(row); |
There was a problem hiding this comment.
[P2] Size array replay cuts from their nested storage
The complex-column fallback still omits storage for NULL array elements. DataTypeArray has no fixed maximum width, so an array of 4096 NULL BIGINTs is estimated as 2 * (4096 + 8) bytes per row. But IColumn::cut() calls ColumnArray::insert_range_from() and then ColumnNullable::insert_range_from(), which copy every nested Int64 as well as the null map.
This is reachable with ordinary 512-row input Blocks, a 64 MiB spill buffer, a 16 MiB analytic sink limit, minimum_operator_memory_required_kb=4096, and a stored nonconstant array payload selected alongside count(*) over (). The first Block proactively spills; two subsequent roughly 36 MiB allocated Blocks coalesce into a 1024-row record. On the second 512-row replay slice, no file-read reservation is added: this fallback plus the configured 4 MiB minimum and COUNT result admits about 8 MiB, while the copied input alone needs at least 18 MiB before capacity rounding. Under memory pressure the cut can exceed admission before another revoke opportunity.
Please size complex cuts recursively from their actual nested ranges/null maps and allocation capacities, with a retained ARRAY cut budget test. This is the recursive ARRAY case explicitly left outside the fixed-width-only fix in discussion 4358224133; that repaired scalar-width branch does not cover this fallback.
TPC-H: Total hot run time: 29215 ms |
TPC-DS: Total hot run time: 152235 ms |
ClickBench: Total hot run time: 24.59 s |
HappenLee
left a comment
There was a problem hiding this comment.
Reviewed commit bef288e685e5d940a92a9ef7a1877a67ea16190a.
The supported scope covers full-partition reductions, NTILE, and percent_rank/cume_dist with sliced replay. Existing discussions retain the P2 edge cases around replay-cut admission, coalesced-write admission, and initial replay scheduling for follow-up.
Validation: GitHub currently reports successful compile, BE UT, FE UT, and P0 regression checks. No additional local build or Doris runtime test was run during this review.
…dows (#69686) ### What problem does this PR solve? Issue Number: None Related PR: #68500 Problem Summary: The non-spill analytic sink expanded each NTILE literal bucket argument to every input row and appended it to a buffered argument column. NTILE reads only the first value, so the extra materialization and storage grew with the number of retained rows. Store one bucket value per NTILE function from the first nonempty input Block and retain it when old input rows are pruned. Other window functions continue to use their row-wise argument columns. Query results are unchanged. ### Release note None ### Check List (For Author) - Test: - Unit Test: ASAN `AnalyticSinkOperatorTest.*` (65 tests), including the new multi-Block, multi-partition pruning case. - Regression test: `spill_p0/window_function_spill` and `query_p0/sql_functions/window_functions/test_ntile_function` (one suite each, zero failures). - Build and static checks: ASAN BE/FE build (including FE Checkstyle), clang-format, check-format, build hygiene, clang-tidy on changed lines, and `git diff --check`. - Behavior changed: No (SQL results are unchanged; NTILE argument storage in the non-spill path is reduced). - Does this need documentation: No
### What problem does this PR solve? Issue Number: None Related PR: apache#69658, apache#68500, apache#61325, apache#65553 Problem Summary: Release branches have been allocating thrift ids and enum values on their own, and those ids later collide with master. Comparing branch-4.0/4.1/4.2/master shows, for example: - TQueryOptions 237 is paimon_blob_max_size on branch-4.1 (apache#69658, merged directly to branch-4.1) and spill_analytic_sink_mem_limit_bytes on master (apache#68500); both are i64, so a mixed cluster silently reads one as the other. - TRLTaskTxnCommitAttachment 12 is firstErrorMsg on 4.0/4.1/4.2 but kinesisRLTaskProgress on master, because the backport of apache#65553 renumbered firstErrorMsg into the gap left by the unpicked apache#61325. - TSchemaTableType values 62-72 differ between 4.0, 4.1/4.2 and master. The existing rule only says an id must mean the same thing everywhere; it does not say where an id is allocated, so each branch picks the next free number. This adds the rule to gensrc/AGENTS.md (summarized in the root AGENTS.md and the code-review skill): a new id or enum value is added on master first, a release-only feature still reserves its id on master, picks keep master's numbers, enum values are written explicitly, and a release-branch PR that adds an id master does not have is not mergeable. ### Release note None ### Check List (For Author) - Test: No need to test (documentation-only change) - Behavior changed: No - Does this need documentation: No Co-Authored-By: Claude Opus 5.5 <[email protected]>
What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
Large non-streaming window partitions currently retain all input blocks until the partition closes. A sufficiently large partition can therefore exhaust BE memory even when query spill is enabled.
This change adds a capability-gated spill path for eligible non-streaming window functions:
percent_rankandcume_dist, with independent metadata spill;AnalyticSpillBatch(per-partition row ends, reduce results and ntile parameters; batch-relative peer group ends), so high-cardinality partition keys do not create per-partition descriptors, aggregate-state rebuilds or 1-row output blocks;spill_buffer_size_bytes, flushing it on revoke and seal;batch_sizeslices and tracks peer-group metadata with the query MemTracker; the source reserves replay memory only for the spilled files it is about to read (data file and peer-group sidecar of the queued batch, and the next data or sidecar record during replay), so in-memory batches are consumed without a disk-read reservation;spill_analytic_sink_mem_limit_bytes, so batches are not written to disk without memory pressure;spill_analytic_sink_mem_limit_bytesand window-spill profile counters.The initial supported set is:
sum/sum0/count/avgand fixed-length-statemin/max, includingOVER ()/OVER (PARTITION BY ...)without ORDER BY, which Nereids plans asRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWwith no order-by expressions (every row is a peer, so the frame is the whole partition);ntilewithROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW;percent_rankandcume_distwithRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.The new source file shifts the BE unity batches so that
async_result_writer.cppshares a translation unit with a file that includeslocal_exchanger.h. Both declareclear_blockswith a default argument, which fails with "redefinition of default argument"; the redeclaration inasync_result_writer.cppno longer repeats the default argument (its only call passes both arguments).Performance (5M unique partition keys,
sum(v) over (partition by k), SQL/query cache off): 0.14s with spill enabled vs 0.26s on the in-memory path; the source emits ~4K rows per block.Coalescing spill writes (50M narrow rows,
spill_analytic_sink_mem_limit_bytes=1MB,sum(v) over ()): spill blocks 6152 -> 96, serialize time 958ms -> 388ms, spill file 38MB -> 11.8MB, query 1.81s -> 1.26s.TPC-DS SF10000 (single BE,
exec_mem_limit=8GB): full-partition aggregates, ntile, percent_rank/cume_dist overstore_sales(28.8B rows) and a 930M-row single partition all completed with the window sink peak at about 64MB per instance; 2.4B ticket-level partitions ran entirely through in-memory batches.Release note
Eligible non-streaming window functions can spill partitions to local storage when query spill is enabled.
Check List (For Author)
spill_p0/window_function_spill: 12 queries (full-partition aggregates with an all-NULL partition, skew, no partition, high cardinality, ntile, percent_rank/cume_dist, unsupported fallback, streaming) each run with spill disabled, spill enabled and forced spill; all three outputs are identical.ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.*:BoundaryPoseTest.*'— 38/38 passed under ASAN, including write coalescing, replay slicing, revoke flush, peer-group sidecar files, replay reservation for in-memory / data-only / data-plus-sidecar batches and sidecar records read during replay, and the proactive spill limit../build.sh --be --fe— passed.build-support/check-format.sh,build-support/check-build-hygiene.sh,git diff --check— passed.sum(v) over ()spilling 5M rows andpartition by kusing in-memory batches.Validation notes:
Latest follow-up (
a225ec4bd70):Peer replay checks cancellation before each peer/output fill; output is already bounded by
batch_size, so there is no extra 4096-row split.Added focused unit cases for cancellation before filling a large peer, filling a large peer in one iteration, and cancellation between uneven peers.
BUILD_TYPE=ASAN ENABLE_UNITY_BUILD=ON ./build.sh --be -j16— passed.ENABLE_UNITY_BUILD=ON ./run-be-ut.sh --run --filter='AnalyticSinkOperatorTest.Spill*' -j16— the changed production and test translation units compiled, but the test executable could not be rebuilt/run because of existingbe/test/exprs/function/function_string_test.cpp:2971-2987compilation errors (Fieldstring construction and removedFunctionContextscope values). The earlier 38/38 result above does not cover this follow-up.build-support/clang-format.sh,build-support/check-format.sh,build-support/check-build-hygiene.sh, andgit diff --check— passed.SQL regression was not rerun for this cancellation-check refactoring.
Clang-tidy cannot fully run on the local toolchain (
stddef.hnot found); reported diagnostics on the touched files are pre-existing cognitive-complexity warnings or false positives from the failed parse.Behavior changed:
Does this need documentation?
Check List (For Reviewer who merge this PR)
https://claude.ai/code/session_01J4WRvR5E8wQd6ew3LsJoAt