morningman opened a new pull request, #69355:
URL: https://github.com/apache/doris/pull/69355
### What problem does this PR solve?
Issue Number: None
Related PR: #67087
Problem Summary:
**In short.** This ports branch-4.1 #67087 to master. The normal and the
spill Iceberg table sinks stop being async writer sinks: they own the Iceberg
table writer and open, write and close it on the pipeline task, scheduled on
the blocking scheduler, the same way the Paimon sink already works on master.
Master's per-partition memory reservation and spill revocation stay as they are.
**Background.** On master both Iceberg data file sinks
(`IcebergTableSinkLocalState`, and `SpillIcebergTableSinkLocalState` for tables
with a sort order) are `AsyncWriterSink`s. Every block is moved into a queue,
and a thread taken from the fragment manager's thread pool opens the files,
writes every block and closes the writer for the whole statement, while the
pipeline task only enqueues. To keep memory admission correct with that
hand-off, master moves each pipeline reservation into the queue together with
its block, holds the EOS reservation in the writer thread until `close()`
finishes, and makes the spill sink admit a block only after the previous one
has been fully processed, because the reservation it computes samples sorter
state that the writer thread updates.
**The problem, and what it cost.**
| | Master today | After this PR |
|---|---|---|
| Who writes the files | A fragment-pool thread per sink instance, for the
whole statement | The pipeline task, on the blocking scheduler |
| Blocks in flight per instance | Up to 3 blocks parked in the async queue |
None, each block is written in `sink()` |
| Spill sink pipelining | None: each block waits until the writer thread has
processed the previous one | Same work, without the thread hop |
| Memory reservation | Transferred through the queue; the EOS reservation is
held by the writer thread through `finish()`/`close()` | The EOS `sink()` call
itself closes the writer under its reservation |
| Final spill merge output block | `batch_size()` rows, which for wide rows
is far larger than the spill buffer the merge reservation charges for it | At
most one spill batch of rows (≈ spill buffer size) |
**How this PR fixes it.**
- `VIcebergTableWriter` no longer derives from `AsyncResultWriter`; it keeps
the output expressions and operator profile itself. `open`, `write`, `close`
and `finish_deferred_file_cleanup` are virtual so unit tests can replace the
writer.
- `IcebergTableSinkLocalState` and `SpillIcebergTableSinkLocalState` derive
from `PipelineXSinkLocalState<FakeSharedState>`, are blockable, create the
writer in `init()`, clone the output expressions and open it in `open()`, and
write each non-empty block in `sink()`. Their `close()` folds a cancellation
seen before or during the writer close into the final status, and calls
`finish_deferred_file_cleanup()` with that status, so a statement cancelled
after the files were closed deletes them instead of handing them to the final
report.
- The spill sink closes the writer in the EOS `sink()` call (idempotently
with `close()`), so the final sort and spill merge run under the EOS
reservation. That reservation already includes the merge workspace of the
largest partition (`iceberg_spill_merge_workspace`); it now also covers the
work that runs inside `sink()`. Master's per-partition reservation, cold-writer
reservation and largest-partition-first revocation are kept. A mutex guards the
writer pointer so a memory callback that arrives after `close()` sees the
released writer.
- The final spill merge of `VIcebergSortWriter` emits `min(batch_size, spill
batch rows)` rows per block, matching the per-block size the merge reservation
assumes.
- `AsyncResultWriter::wait_for_processing_before_next_sink()` and the
waiting mode of the async queue admission only existed for the Iceberg spill
sink and are removed. The other async writers (Hive, MaxCompute, JDBC, TVF,
OLAP, result file) always used the plain bounded queue, whose behavior does not
change.
- DELETE and MERGE sinks stay asynchronous. The MERGE sink keeps using
`VIcebergTableWriter` as its nested data file writer.
**Results.**
- Unit tests: 122 tests pass across the new `IcebergTableSinkOperatorTest`
(13 cases), `SpillIcebergTableSinkOperatorTest`, `FullSorterTest`, the Iceberg
table and sort writer suites, the MERGE / DELETE sink suites and the async
writer suites. Four mutations (the final merge batch back at `batch_size()`
rows, no writer close in the spill sink's EOS `sink()`, and two of the
cancellation checks around the writer close removed) make 7 of the 13 new cases
fail; the other 6 still pass.
- Regression test: the new `test_iceberg_write_sorted_spill` writes 60000
rows into a REST catalog table sorted by `id desc nulls last` and partitioned
by `region`, with forced spilling and `spill_min_revocable_mem = 1`. On a local
cluster the BE paused the INSERT three times to revoke
`SPILL_ICEBERG_TABLE_SINK_OPERATOR`; each partition was still committed as one
data file of 20000 rows, and Spark reads back 60000 rows, the expected sum, and
no row out of order inside any file (`lag(id) over (partition by _file order by
_pos)`). All 43 suites of `external_table_p0/iceberg/write` pass on the same
cluster.
- Manual test: on a local `file://` Iceberg catalog the same forced-spill
INSERT wrote 12 spill files (4 per partition) before the final merge, every
data file read back in descending `id` order, and UPDATE / DELETE / MERGE on
the table returned the expected rows.
<details>
<summary>Classes and call graph</summary>
```
PipelineTask::execute (blocking scheduler, sink is_blockable() == true)
├─ IcebergTableSinkOperatorX::sink_impl
│ └─ IcebergTableSinkLocalState::sink ──> VIcebergTableWriter::write
├─ SpillIcebergTableSinkOperatorX::get_reserve_mem_size (master model,
unchanged)
├─ SpillIcebergTableSinkOperatorX::sink_impl
│ └─ SpillIcebergTableSinkLocalState::sink
│ ├─ VIcebergTableWriter::write ──> VIcebergSortWriter::write (per
partition)
│ └─ eos: _close_writer ──> VIcebergTableWriter::close ──>
VIcebergSortWriter::close
│ (final sort / spill merge, capped
output batch)
└─ PipelineTask::close ──> *LocalState::close
└─ cancellation check ─ writer close (if not yet) ─ cancellation
check
─ VIcebergTableWriter::finish_deferred_file_cleanup(final status)
RevokableTask (while the task is paused)
└─ SpillIcebergTableSinkLocalState::revoke_memory
└─ under _writer_mutex: active_writers() snapshot ──> largest
VIcebergSortWriter::trigger_spill
```
</details>
Original author: @hubgeter
### Release note
Iceberg data file writes (INSERT, INSERT OVERWRITE, CTAS) run synchronously
on the blocking pipeline scheduler instead of a separate async writer thread.
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [x] Regression test
- [x] Unit Test
- [x] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [x] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR should
merge into -->
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]