This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 9177f0cbd6d [refactor](be) Separate S3 spill file I/O from common
readers and writers (#69781)
9177f0cbd6d is described below
commit 9177f0cbd6d0faa0ea809cc1c0afe375a47965f0
Author: Jerry Hu <[email protected]>
AuthorDate: Mon Nov 2 09:54:30 2026 +0800
[refactor](be) Separate S3 spill file I/O from common readers and writers
(#69781)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: `SpillFileWriter` and `SpillFileReader` mixed their
common spill-part lifecycle and serialization/deserialization with
object-storage-specific policies. The writer included S3 upload queue
admission, cancellation checks, timing, and remote accounting; the
reader included S3 footer prefetch, adjacent-block coalescing, and
remote byte/request accounting.
This change introduces `SpillS3FileWriter` and `SpillS3FileReader`,
selected by the spill file factory for remote files. The common classes
retain part lifecycle, footer format and validation, block
serialization/deserialization, part rotation/traversal, and seek
handling. Narrow virtual hooks preserve the S3 policies, failed-part
reconciliation, query-buffer cap, and local/remote metric separation.
The derived writer closes before base destruction so implicit close
still dispatches its S3 hooks; reader cleanup remains entirely in the
base class and does not require derived hooks. Public writer/reader APIs
and the spill file format remain unchanged.
### Release note
None
### Check List (For Author)
- Test: Unit Test / Build and static checks
- `BUILD_TYPE=ASAN ./build.sh --be -j16` passed on the updated
reader/writer head.
- `./run-be-ut.sh -j16 --run
--filter='SpillFileS3Test.*:SpillFileTest.*:SpillRepartitionerTest.*:SpillSortSourceOperatorTest.*'`
passed (109 tests from 4 suites).
- New coverage checks S3 reader factory selection and empty-file
handling, the query-buffer cap on coalesced GET lengths, and local
reader type/accounting isolation. Existing suites exercise multi-part
roundtrips, seeking, footer overflow, oversized blocks, and disabled
coalescing.
- BE formatting/check-format, build hygiene, and `git diff --check`
passed.
- Changed-line clang-tidy passed for all 7 files in the reader follow-up
using the UT compilation database and the Clang resource directory.
- S3 unit tests use an in-memory object-storage mock. SQL regression and
real-object-store E2E were not rerun for the reader follow-up; these
results are not remote CI results.
- Behavior changed: No; this is an internal responsibility split
preserving the existing local and S3 spill behavior.
- Does this need documentation: No
---
be/src/exec/spill/spill_file.cpp | 15 +++-
be/src/exec/spill/spill_file.h | 2 +
be/src/exec/spill/spill_file_reader.cpp | 61 ++++----------
be/src/exec/spill/spill_file_reader.h | 31 ++++---
be/src/exec/spill/spill_file_writer.cpp | 106 ++++--------------------
be/src/exec/spill/spill_file_writer.h | 57 ++++++-------
be/src/exec/spill/spill_s3_file_reader.cpp | 82 +++++++++++++++++++
be/src/exec/spill/spill_s3_file_reader.h | 46 +++++++++++
be/src/exec/spill/spill_s3_file_writer.cpp | 127 +++++++++++++++++++++++++++++
be/src/exec/spill/spill_s3_file_writer.h | 47 +++++++++++
be/test/vec/spill/spill_file_s3_test.cpp | 95 +++++++++++++++++++++
be/test/vec/spill/spill_file_test.cpp | 11 +++
12 files changed, 496 insertions(+), 184 deletions(-)
diff --git a/be/src/exec/spill/spill_file.cpp b/be/src/exec/spill/spill_file.cpp
index 9cda254ec98..9849c8561fe 100644
--- a/be/src/exec/spill/spill_file.cpp
+++ b/be/src/exec/spill/spill_file.cpp
@@ -27,6 +27,8 @@
#include "exec/spill/spill_file_manager.h"
#include "exec/spill/spill_file_reader.h"
#include "exec/spill/spill_file_writer.h"
+#include "exec/spill/spill_s3_file_reader.h"
+#include "exec/spill/spill_s3_file_writer.h"
#include "io/fs/local_file_system.h"
#include "io/fs/path.h"
#include "io/fs/s3_file_system.h"
@@ -137,8 +139,13 @@ std::string SpillFile::_log_context() const {
Status SpillFile::create_writer(RuntimeState* state, RuntimeProfile* profile,
SpillFileWriterSPtr& writer) {
- writer = std::make_shared<SpillFileWriter>(shared_from_this(), state,
profile, _data_dir, _fs,
- _spill_dir);
+ if (_data_dir->is_remote()) {
+ writer = std::make_shared<SpillS3FileWriter>(shared_from_this(),
state, profile, _data_dir,
+ _fs, _spill_dir);
+ } else {
+ writer = std::make_shared<SpillFileWriter>(shared_from_this(), state,
profile, _data_dir,
+ _fs, _spill_dir);
+ }
// _active_writer is set in SpillFileWriter constructor via the shared_ptr
return Status::OK();
}
@@ -146,6 +153,10 @@ Status SpillFile::create_writer(RuntimeState* state,
RuntimeProfile* profile,
SpillFileReaderSPtr SpillFile::create_reader(RuntimeState* state,
RuntimeProfile* profile) const {
// It's a programming error to create a reader while a writer is still
active.
DCHECK(_active_writer == nullptr) << "create_reader() called while writer
still active";
+ if (_data_dir->is_remote()) {
+ return std::make_shared<SpillS3FileReader>(state, profile, _data_dir,
_fs, _spill_dir,
+ _part_sizes);
+ }
return std::make_shared<SpillFileReader>(state, profile, _data_dir, _fs,
_spill_dir,
_part_sizes);
}
diff --git a/be/src/exec/spill/spill_file.h b/be/src/exec/spill/spill_file.h
index 6665e1ae585..bc3d77714a8 100644
--- a/be/src/exec/spill/spill_file.h
+++ b/be/src/exec/spill/spill_file.h
@@ -30,6 +30,7 @@ class RuntimeState;
class Block;
class SpillDataDir;
class SpillFileWriter;
+class SpillS3FileWriter;
class SpillFileReader;
using SpillFileWriterSPtr = std::shared_ptr<SpillFileWriter>;
using SpillFileReaderSPtr = std::shared_ptr<SpillFileReader>;
@@ -91,6 +92,7 @@ public:
private:
friend class SpillFileWriter;
+ friend class SpillS3FileWriter;
friend class SpillFileManager;
/// Called by SpillFileWriter::close() to mark writing as complete.
diff --git a/be/src/exec/spill/spill_file_reader.cpp
b/be/src/exec/spill/spill_file_reader.cpp
index 7e8082b41e1..287f8924c9a 100644
--- a/be/src/exec/spill/spill_file_reader.cpp
+++ b/be/src/exec/spill/spill_file_reader.cpp
@@ -19,11 +19,10 @@
#include <glog/logging.h>
-#include <algorithm>
#include <cstring>
+#include <utility>
#include "common/cast_set.h"
-#include "common/config.h"
#include "common/exception.h"
#include "core/block/block.h"
#include "exec/spill/spill_file_manager.h"
@@ -34,6 +33,7 @@
#include "runtime/query_context.h"
#include "runtime/runtime_profile_counter_names.h"
#include "runtime/runtime_state.h"
+#include "runtime/workload_management/io_context.h"
#include "util/debug_points.h"
#include "util/slice.h"
namespace doris {
@@ -49,7 +49,6 @@ SpillFileReader::SpillFileReader(RuntimeState* state,
RuntimeProfile* profile,
_spill_dir(std::move(spill_dir)),
_part_sizes(std::move(part_sizes)),
_part_count(_part_sizes.size()),
- _is_remote(data_dir != nullptr && data_dir->is_remote()),
_resource_ctx(state->get_query_ctx()->resource_ctx()) {
// Internalize counter setup. The counters themselves are registered by
the owning
// operator (SpillReadCounters::init), so look them up by the shared name
constants:
@@ -69,34 +68,28 @@ SpillFileReader::SpillFileReader(RuntimeState* state,
RuntimeProfile* profile,
_read_file_size = get_counter(profile::SPILL_READ_FILE_BYTES);
_read_rows_count = get_counter(profile::SPILL_READ_ROWS);
_read_file_count = get_counter(profile::SPILL_READ_FILE_COUNT);
- // Optional: older profiles may not register it.
- _remote_read_requests =
custom_profile->get_counter(profile::SPILL_REMOTE_READ_REQUESTS);
- if (_is_remote) {
- _coalesce_bytes = static_cast<size_t>(std::max<int64_t>(
- 0,
- std::min(config::spill_s3_read_coalesce_bytes,
state->spill_buffer_size_bytes())));
- }
}
void SpillFileReader::_record_read(size_t bytes_read) {
COUNTER_UPDATE(_read_file_size, bytes_read);
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_read_bytes(bytes_read);
- if (_is_remote) {
- // One successful read_at() is one logical GET. Retries inside the
object storage client
- // and reads that finally failed are not counted.
- if (_remote_read_requests != nullptr) {
- COUNTER_UPDATE(_remote_read_requests, 1);
- }
- if (_resource_ctx) {
-
_resource_ctx->io_context()->update_spill_read_bytes_from_remote_storage(bytes_read);
- _resource_ctx->io_context()->update_spill_remote_read_requests(1);
- }
-
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_remote_read(bytes_read,
1);
- } else if (_resource_ctx) {
+ _record_spill_read(bytes_read);
+}
+
+void SpillFileReader::_record_spill_read(size_t bytes_read) {
+ if (_resource_ctx) {
_resource_ctx->io_context()->update_spill_read_bytes_from_local_storage(bytes_read);
}
}
+size_t SpillFileReader::_footer_probe_size(size_t, bool) const {
+ return 2 * sizeof(size_t);
+}
+
+size_t SpillFileReader::_read_window_end(size_t index) const {
+ return _block_start_offsets[index + 1];
+}
+
Status SpillFileReader::open() {
if (_is_open || _part_count == 0) {
return Status::OK();
@@ -120,7 +113,7 @@ Status SpillFileReader::_open_part(size_t part_index, bool
fetch_small_part) {
}
io::FileReaderOptions opts;
opts.cache_type = io::FileCachePolicy::NO_CACHE;
- // The writer recorded the part size; on object storage this saves a HEAD
request.
+ // The writer recorded the part size, so opening needs no size lookup.
opts.file_size = _part_sizes[part_index];
{
SCOPED_TIMER(_read_file_timer);
@@ -138,23 +131,12 @@ Status SpillFileReader::_read_footer(size_t file_size,
bool fetch_small_part) {
// [block start offsets array (_part_block_count * size_t)]
// [serialized blocks]
constexpr size_t kFooterTailBytes = 2 * sizeof(size_t);
- // Enough for the offsets of a few thousand blocks, so the footer usually
takes one GET.
- constexpr size_t kRemoteFooterProbeBytes = 64 * 1024;
if (file_size < kFooterTailBytes) {
return Status::InternalError("spill part {} is too small: {} bytes",
_file_reader->path().native(), file_size);
}
- // Without coalescing (local disk) the footer is read exactly. Otherwise a
larger tail, bounded
- // by the coalesced read size like every other read, is read in one
request, or the whole
- // part when it fits in one coalesced read.
- size_t probe_size = kFooterTailBytes;
- if (_coalesce_bytes > 0) {
- probe_size = fetch_small_part && file_size <= _coalesce_bytes
- ? file_size
- : std::min({file_size, kRemoteFooterProbeBytes,
- std::max(_coalesce_bytes,
kFooterTailBytes)});
- }
+ const size_t probe_size = _footer_probe_size(file_size, fetch_small_part);
_ensure_read_buff(probe_size);
RETURN_IF_ERROR(_read_exact(file_size - probe_size, _read_buff.data(),
probe_size));
@@ -217,14 +199,7 @@ Status SpillFileReader::_block_slice(size_t index, Slice*
out) {
const size_t begin = _block_start_offsets[index];
const size_t end = _block_start_offsets[index + 1];
if (begin < _window_begin || end > _window_end) {
- // Take this block and the following ones as long as the read stays
within
- // _coalesce_bytes. A block larger than that is still read whole.
- size_t last = index + 1;
- while (last < _part_block_count &&
- _block_start_offsets[last + 1] - begin <= _coalesce_bytes) {
- ++last;
- }
- const size_t window_end = _block_start_offsets[last];
+ const size_t window_end = _read_window_end(index);
// A failed read may leave the buffer partly overwritten.
_window_begin = _window_end = 0;
_ensure_read_buff(window_end - begin);
diff --git a/be/src/exec/spill/spill_file_reader.h
b/be/src/exec/spill/spill_file_reader.h
index bbc5f4e3dfb..2c8468dd56c 100644
--- a/be/src/exec/spill/spill_file_reader.h
+++ b/be/src/exec/spill/spill_file_reader.h
@@ -50,18 +50,12 @@ class SpillDataDir;
///
/// Parts are opened on the file system pinned in the SpillFile (local disk or
object storage).
/// Part sizes are known from the writer, so no size lookup is needed on open.
-///
-/// On object storage every read is a GET, so the reader keeps the request
count low:
-/// the part footer is fetched with one tail read of at most `_coalesce_bytes`
(plus one more
-/// read for the rest of a block offset array that does not fit), adjacent
blocks are coalesced
-/// into one read of at most `_coalesce_bytes`, and a part no larger than that
is fetched whole.
-/// A single block, or the rest of the offset array, larger than
`_coalesce_bytes` is read whole.
class SpillFileReader {
public:
SpillFileReader(RuntimeState* state, RuntimeProfile* profile,
SpillDataDir* data_dir,
io::FileSystemSPtr fs, std::string spill_dir,
std::vector<int64_t> part_sizes);
- ~SpillFileReader() { (void)close(); }
+ virtual ~SpillFileReader() { (void)close(); }
/// Open the first part and read its footer metadata.
Status open();
@@ -77,16 +71,24 @@ public:
Status close();
+protected:
+ /// Storage-specific read sizing. Local files read the footer tail and one
block at a time.
+ virtual size_t _footer_probe_size(size_t file_size, bool fetch_small_part)
const;
+ virtual size_t _read_window_end(size_t index) const;
+
+ /// Account a successful store read in the storage-specific counters.
+ virtual void _record_spill_read(size_t bytes_read);
+
private:
- /// Open a specific part file and read its footer. With
`fetch_small_part`, a part that
- /// fits in one coalesced read is fetched whole, so its blocks need no
further reads.
+ /// Open a specific part file and read its footer. `fetch_small_part`
allows the storage
+ /// policy to fetch the whole part when it fits in one read.
Status _open_part(size_t part_index, bool fetch_small_part);
/// Read the footer (block offsets, max sub block size, block count) of
the current part.
Status _read_footer(size_t file_size, bool fetch_small_part);
/// Point `out` at the serialized bytes of block `index` of the current
part, reading
- /// them (and the following blocks that fit in the coalesce window) if not
buffered.
+ /// them (and any following blocks selected by the storage policy) if not
buffered.
Status _block_slice(size_t index, Slice* out);
/// Read exactly `len` bytes at `offset` of the current part into `data`.
@@ -110,9 +112,6 @@ private:
std::string _spill_dir;
std::vector<int64_t> _part_sizes;
size_t _part_count;
- bool _is_remote = false;
- // Upper bound of one coalesced read; 0 reads block by block (local disk).
- size_t _coalesce_bytes = 0;
// ── Current part state ──
size_t _current_part_index = 0;
@@ -126,7 +125,6 @@ private:
PaddedPODArray<char> _read_buff;
size_t _window_begin = 0;
size_t _window_end = 0;
- std::vector<size_t> _block_start_offsets;
PBlock _pb_block;
@@ -138,9 +136,10 @@ private:
RuntimeProfile::Counter* _read_file_size = nullptr;
RuntimeProfile::Counter* _read_rows_count = nullptr;
RuntimeProfile::Counter* _read_file_count = nullptr;
- // Remote only, may be null when the profile does not register it.
- RuntimeProfile::Counter* _remote_read_requests = nullptr;
+protected:
+ // Includes the footer start as the final offset, so every block has an
end offset.
+ std::vector<size_t> _block_start_offsets;
std::shared_ptr<ResourceContext> _resource_ctx = nullptr;
};
diff --git a/be/src/exec/spill/spill_file_writer.cpp
b/be/src/exec/spill/spill_file_writer.cpp
index d780123eb1e..abb9b61a1e4 100644
--- a/be/src/exec/spill/spill_file_writer.cpp
+++ b/be/src/exec/spill/spill_file_writer.cpp
@@ -18,8 +18,6 @@
#include "exec/spill/spill_file_writer.h"
#include <algorithm>
-#include <chrono>
-#include <thread>
#include "agent/be_exec_version_manager.h"
#include "common/config.h"
@@ -31,12 +29,10 @@
#include "io/fs/local_file_writer.h"
#include "runtime/exec_env.h"
#include "runtime/query_context.h"
-#include "runtime/runtime_profile_counter_names.h"
#include "runtime/runtime_state.h"
#include "runtime/thread_context.h"
#include "util/defer_op.h"
#include "util/stopwatch.hpp"
-#include "util/threadpool.h"
namespace doris {
@@ -67,9 +63,6 @@ SpillFileWriter::SpillFileWriter(const
std::shared_ptr<SpillFile>& spill_file, R
_write_file_current_size =
custom_profile->get_counter("SpillWriteFileCurrentBytes");
_write_rows_counter = custom_profile->get_counter("SpillWriteRows");
_total_file_count =
custom_profile->get_counter("SpillWriteFileTotalCount");
- _remote_upload_bytes =
custom_profile->get_counter(profile::SPILL_REMOTE_UPLOAD_BYTES);
- _remote_upload_timer =
custom_profile->get_counter(profile::SPILL_REMOTE_UPLOAD_TIME);
- _remote_upload_wait_timer =
custom_profile->get_counter(profile::SPILL_REMOTE_UPLOAD_WAIT_TIME);
}
SpillFileWriter::~SpillFileWriter() {
@@ -100,10 +93,8 @@ void SpillFileWriter::_discard(SpillFile* spill_file) {
// without publishing the object, before the SpillFile deletes its prefix.
Incomplete
// multipart uploads are left to the bucket lifecycle rule.
std::unique_ptr<io::FileWriter> writer = std::move(_file_writer);
- if (_data_dir->is_remote()) {
- _account_remote_part(/*closed=*/false);
- }
writer.reset();
+ _on_part_closed(nullptr, /*closed=*/false, /*close_ns=*/0);
_advance_part();
}
@@ -157,13 +148,10 @@ Status SpillFileWriter::_close_current_part(const
std::shared_ptr<SpillFile>& sp
if (status.ok()) {
_part_written_bytes += meta_size;
COUNTER_UPDATE(_write_file_total_size, meta_size);
- if (_resource_ctx) {
- if (_data_dir->is_remote()) {
-
_resource_ctx->io_context()->update_spill_write_bytes_to_remote_storage(meta_size);
- } else {
-
_resource_ctx->io_context()->update_spill_write_bytes_to_local_storage(meta_size);
- }
- }
+ // The S3 destructor closes before base destruction; a standalone base
writer
+ // dispatches to its local implementation during its own destructor.
+ // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall)
+ _record_spill_write_bytes(meta_size);
if (_write_file_current_size) {
COUNTER_UPDATE(_write_file_current_size, meta_size);
}
@@ -181,16 +169,12 @@ Status SpillFileWriter::_close_current_part(const
std::shared_ptr<SpillFile>& sp
// tail of the uploads plus one CompleteMultipartUpload) is negligible
against the part
// itself, so overlapping it with the next part is not worth an async
close pipeline.
std::unique_ptr<io::FileWriter> writer = std::move(_file_writer);
+ int64_t close_ns = 0;
if (status.ok()) {
MonotonicStopWatch close_watch;
close_watch.start();
status = writer->close();
- if (_data_dir->is_remote()) {
- _part_upload_ns += close_watch.elapsed_time();
- }
- }
- if (_data_dir->is_remote()) {
- _account_remote_part(status.ok());
+ close_ns = close_watch.elapsed_time();
}
if (!status.ok()) {
// A part that failed before close() (an append failed or the query
was cancelled) must
@@ -203,13 +187,11 @@ Status SpillFileWriter::_close_current_part(const
std::shared_ptr<SpillFile>& sp
if (!status.ok()) {
LOG(WARNING) << "failed to close spill part " << _current_part_path <<
": " << status;
- // A failed append or close can still leave an object in S3. The
writer has drained
- // in-flight uploads above, so list the prefix after accounting a
conservative upper
- // bound rather than relying on the close status to prove whether PUT
committed.
- if (spill_file && _data_dir->is_remote()) {
- spill_file->account_potential_part(_part_written_bytes);
- }
- } else if (spill_file) {
+ }
+ // S3 destruction closes while derived hooks still exist; base-only
writers use this no-op.
+ // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall)
+ _on_part_closed(spill_file, status.ok(), close_ns);
+ if (status.ok() && spill_file) {
spill_file->add_part(_part_written_bytes);
}
@@ -217,62 +199,17 @@ Status SpillFileWriter::_close_current_part(const
std::shared_ptr<SpillFile>& sp
return status;
}
-void SpillFileWriter::_account_remote_part(bool closed) {
- // A successful close() publishes the whole part, including its footer.
Failed uploads
- // can still be in flight, so their bytes are not counted as successfully
uploaded.
- const int64_t uploaded_bytes = closed ? _part_written_bytes : 0;
- if (_remote_upload_bytes != nullptr) {
- COUNTER_UPDATE(_remote_upload_bytes, uploaded_bytes);
- COUNTER_UPDATE(_remote_upload_timer, _part_upload_ns);
+void SpillFileWriter::_record_spill_write_bytes(int64_t bytes) {
+ if (_resource_ctx) {
+
_resource_ctx->io_context()->update_spill_write_bytes_to_local_storage(bytes);
}
-
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_remote_write(uploaded_bytes);
}
-Status SpillFileWriter::_wait_for_upload_queue() {
- auto is_cancelled = [this]() {
- return _resource_ctx != nullptr &&
_resource_ctx->task_controller()->is_cancelled();
- };
- // A cancelled query must not start another (billed) upload.
- if (is_cancelled()) {
- return Status::Cancelled("query cancelled before uploading spill
data");
- }
- auto* upload_pool = ExecEnv::GetInstance()->s3_file_upload_thread_pool();
- auto queue_full = [upload_pool]() {
- return static_cast<int64_t>(upload_pool->get_queue_size()) *
config::s3_write_buffer_size >=
- config::spill_s3_max_inflight_upload_bytes;
- };
- if (!queue_full()) {
- return Status::OK();
- }
- MonotonicStopWatch wait_watch;
- wait_watch.start();
- Defer record_wait {
- [&]() { COUNTER_UPDATE(_remote_upload_wait_timer,
wait_watch.elapsed_time()); }};
- // Queued uploads always finish, so the wait ends without help from other
writers.
- while (queue_full()) {
- if (is_cancelled()) {
- return Status::Cancelled("query cancelled while waiting for the
spill upload queue");
- }
- std::this_thread::sleep_for(std::chrono::milliseconds(5));
- }
- return Status::OK();
-}
+void SpillFileWriter::_on_part_closed(const std::shared_ptr<SpillFile>&, bool,
int64_t) {}
Status SpillFileWriter::_append_to_part(const Slice& data) {
RETURN_IF_ERROR(_part_append_status);
- if (!_data_dir->is_remote()) {
- _part_append_status = _file_writer->append(data);
- return _part_append_status;
- }
- // S3FileWriter splits `data` into upload buffers and queues every full
one without waiting,
- // so the queue is checked once per append: `data` is already in memory,
and the wait keeps
- // the next append from producing faster than object storage takes the
data.
- _part_append_status = _wait_for_upload_queue();
- RETURN_IF_ERROR(_part_append_status);
- MonotonicStopWatch append_watch;
- append_watch.start();
_part_append_status = _file_writer->append(data);
- _part_upload_ns += append_watch.elapsed_time();
return _part_append_status;
}
@@ -282,7 +219,6 @@ void SpillFileWriter::_advance_part() {
_part_written_bytes = 0;
_part_max_sub_block_size = 0;
_part_meta.clear();
- _part_upload_ns = 0;
_part_append_status = Status::OK();
}
@@ -427,15 +363,7 @@ Status SpillFileWriter::_write_internal(const Block& block,
_part_meta.append((const char*)&_part_written_bytes,
sizeof(size_t));
COUNTER_UPDATE(_write_file_total_size, buff_size);
- if (_resource_ctx) {
- if (_data_dir->is_remote()) {
-
_resource_ctx->io_context()->update_spill_write_bytes_to_remote_storage(
- buff_size);
- } else {
-
_resource_ctx->io_context()->update_spill_write_bytes_to_local_storage(
- buff_size);
- }
- }
+ _record_spill_write_bytes(buff_size);
if (_write_file_current_size) {
COUNTER_UPDATE(_write_file_current_size, buff_size);
}
diff --git a/be/src/exec/spill/spill_file_writer.h
b/be/src/exec/spill/spill_file_writer.h
index ac60b5f67c0..dd17e5480c8 100644
--- a/be/src/exec/spill/spill_file_writer.h
+++ b/be/src/exec/spill/spill_file_writer.h
@@ -47,19 +47,15 @@ class SpillFile;
/// directory. A part is closed synchronously when it is rotated out or when
close() is called.
/// If the SpillFile is destroyed before the writer is closed, the unfinished
part is discarded.
///
-/// Files are created on the file system pinned in the SpillFile, which is
either a local disk or
-/// the object storage of a cloud storage vault. For object storage the writer
additionally
-/// pauses before every append while the S3 upload queue holds at least
-/// spill_s3_max_inflight_upload_bytes of buffers, which slows down a spill
that
-/// produces faster than object storage takes it, and reports request
statistics derived from
-/// the closed part.
+/// Files are created on the file system pinned in the SpillFile. This base
class handles
+/// common part lifecycle and local writes; SpillS3FileWriter provides
object-storage writes.
class SpillFileWriter {
public:
SpillFileWriter(const std::shared_ptr<SpillFile>& spill_file,
RuntimeState* state,
RuntimeProfile* profile, SpillDataDir* data_dir,
io::FileSystemSPtr fs,
std::string spill_dir);
- ~SpillFileWriter();
+ virtual ~SpillFileWriter();
/// Write a block. Automatically opens the first part, splits large blocks,
/// and rotates to a new part when the current one exceeds max_part_size.
@@ -76,6 +72,19 @@ public:
size_t max_block_bytes() const { return _max_block_bytes; }
size_t max_footer_bytes() const { return _max_footer_bytes; }
+protected:
+ /// Preserve the first append failure for the whole part, including its
footer.
+ virtual Status _append_to_part(const Slice& data);
+
+ /// Record bytes in the storage-specific query I/O context.
+ virtual void _record_spill_write_bytes(int64_t bytes);
+
+ /// Account a closed or discarded part after its writer has drained on
failure.
+ virtual void _on_part_closed(const std::shared_ptr<SpillFile>& spill_file,
bool closed,
+ int64_t close_ns);
+
+ const std::string& _spill_directory() const { return _spill_dir; }
+
private:
Status _close_impl(const std::shared_ptr<SpillFile>& spill_file);
@@ -87,26 +96,11 @@ private:
Status _open_next_part(const std::shared_ptr<SpillFile>& spill_file);
/// Close the current part: write the footer, close the file writer
synchronously
- /// (draining in-flight uploads on failure), account its remote
statistics, register
+ /// (draining in-flight uploads on failure), account its statistics,
register
/// the successful part with the SpillFile, and advance to the next part
index.
/// Statistics are accounted whether it succeeds or not.
Status _close_current_part(const std::shared_ptr<SpillFile>& spill_file);
- /// Append to the current part. For object storage `data` is appended after
- /// _wait_for_upload_queue(). After a failed append the part is
- /// broken: every later append, the footer included, returns that failure
without reaching
- /// the file writer (S3FileWriter must not be appended to after a failed
append).
- Status _append_to_part(const Slice& data);
-
- /// Object storage only: refuse a cancelled query, and wait while the S3
upload queue holds
- /// at least spill_s3_max_inflight_upload_bytes of buffers. Buffers that
are still being
- /// filled are not queued, so idle writers cannot block each other.
- Status _wait_for_upload_queue();
-
- /// Object storage only: account the upload time and, after a successful
close(), the bytes
- /// of the whole part including its footer. Does not inspect S3 upload
state.
- void _account_remote_part(bool closed);
-
/// Reset the per-part state and move on to the next part index.
void _advance_part();
@@ -134,16 +128,17 @@ private:
// ── Current part state (reset on rotation) ──
size_t _current_part_index = 0;
std::string _current_part_path;
- std::unique_ptr<doris::io::FileWriter> _file_writer;
size_t _part_written_blocks = 0;
- int64_t _part_written_bytes = 0;
size_t _part_max_sub_block_size = 0;
std::string _part_meta;
- // Object storage only: time spent in append() and close() of the current
part.
- int64_t _part_upload_ns = 0;
- // First failed append of the current part.
+
+protected:
+ std::unique_ptr<doris::io::FileWriter> _file_writer;
+ int64_t _part_written_bytes = 0;
Status _part_append_status;
+ std::shared_ptr<ResourceContext> _resource_ctx = nullptr;
+private:
bool _closed = false;
// First failed write_block(); every later write and close() returns it.
Status _write_status;
@@ -161,12 +156,6 @@ private:
RuntimeProfile::Counter* _write_rows_counter = nullptr;
RuntimeProfile::Counter* _memory_used_counter = nullptr;
RuntimeProfile::Counter* _total_file_count = nullptr;
- // Remote only, may be null when the profile does not register them.
- RuntimeProfile::Counter* _remote_upload_bytes = nullptr;
- RuntimeProfile::Counter* _remote_upload_timer = nullptr;
- RuntimeProfile::Counter* _remote_upload_wait_timer = nullptr;
-
- std::shared_ptr<ResourceContext> _resource_ctx = nullptr;
};
using SpillFileWriterSPtr = std::shared_ptr<SpillFileWriter>;
} // namespace doris
diff --git a/be/src/exec/spill/spill_s3_file_reader.cpp
b/be/src/exec/spill/spill_s3_file_reader.cpp
new file mode 100644
index 00000000000..bdb943529c2
--- /dev/null
+++ b/be/src/exec/spill/spill_s3_file_reader.cpp
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "exec/spill/spill_s3_file_reader.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "common/config.h"
+#include "exec/spill/spill_file_manager.h"
+#include "runtime/exec_env.h"
+#include "runtime/runtime_profile_counter_names.h"
+#include "runtime/runtime_state.h"
+#include "runtime/workload_management/io_context.h"
+
+namespace doris {
+
+SpillS3FileReader::SpillS3FileReader(RuntimeState* state, RuntimeProfile*
profile,
+ SpillDataDir* data_dir,
io::FileSystemSPtr fs,
+ std::string spill_dir,
std::vector<int64_t> part_sizes)
+ : SpillFileReader(state, profile, data_dir, std::move(fs),
std::move(spill_dir),
+ std::move(part_sizes)),
+ _coalesce_bytes(static_cast<size_t>(
+ std::max<int64_t>(0,
std::min(config::spill_s3_read_coalesce_bytes,
+
state->spill_buffer_size_bytes())))) {
+ RuntimeProfile* custom_profile =
profile->get_child(profile::CUSTOM_COUNTERS);
+ _remote_read_requests =
custom_profile->get_counter(profile::SPILL_REMOTE_READ_REQUESTS);
+}
+
+size_t SpillS3FileReader::_footer_probe_size(size_t file_size, bool
fetch_small_part) const {
+ constexpr size_t kFooterTailBytes = 2 * sizeof(size_t);
+ // Enough for the offsets of a few thousand blocks, so the footer usually
takes one GET.
+ constexpr size_t kRemoteFooterProbeBytes = 64 * 1024;
+ if (_coalesce_bytes == 0) {
+ return kFooterTailBytes;
+ }
+ return fetch_small_part && file_size <= _coalesce_bytes
+ ? file_size
+ : std::min({file_size, kRemoteFooterProbeBytes,
+ std::max(_coalesce_bytes, kFooterTailBytes)});
+}
+
+size_t SpillS3FileReader::_read_window_end(size_t index) const {
+ // Take this block and the following ones as long as the read stays within
+ // _coalesce_bytes. A block larger than that is still read whole.
+ const size_t begin = _block_start_offsets[index];
+ size_t last = index + 1;
+ while (last + 1 < _block_start_offsets.size() &&
+ _block_start_offsets[last + 1] - begin <= _coalesce_bytes) {
+ ++last;
+ }
+ return _block_start_offsets[last];
+}
+
+void SpillS3FileReader::_record_spill_read(size_t bytes_read) {
+ // One successful read_at() is one logical GET. Retries inside the object
storage client
+ // and reads that finally failed are not counted.
+ if (_remote_read_requests != nullptr) {
+ COUNTER_UPDATE(_remote_read_requests, 1);
+ }
+ if (_resource_ctx) {
+
_resource_ctx->io_context()->update_spill_read_bytes_from_remote_storage(bytes_read);
+ _resource_ctx->io_context()->update_spill_remote_read_requests(1);
+ }
+
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_remote_read(bytes_read,
1);
+}
+
+} // namespace doris
diff --git a/be/src/exec/spill/spill_s3_file_reader.h
b/be/src/exec/spill/spill_s3_file_reader.h
new file mode 100644
index 00000000000..46e1c7d7e80
--- /dev/null
+++ b/be/src/exec/spill/spill_s3_file_reader.h
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include "exec/spill/spill_file_reader.h"
+
+namespace doris {
+
+/// S3-specific coalesced reads and request accounting for spill parts.
+/// The footer is fetched with one tail read (plus one more for offsets that
do not fit),
+/// adjacent blocks are coalesced, and a part that fits in the read window is
fetched whole.
+/// A single block or the rest of the offset array larger than the window is
read whole.
+class SpillS3FileReader final : public SpillFileReader {
+public:
+ SpillS3FileReader(RuntimeState* state, RuntimeProfile* profile,
SpillDataDir* data_dir,
+ io::FileSystemSPtr fs, std::string spill_dir,
+ std::vector<int64_t> part_sizes);
+
+protected:
+ size_t _footer_probe_size(size_t file_size, bool fetch_small_part) const
override;
+ size_t _read_window_end(size_t index) const override;
+ void _record_spill_read(size_t bytes_read) override;
+
+private:
+ // Upper bound of one coalesced read; 0 reads block by block.
+ size_t _coalesce_bytes = 0;
+ // Optional: older profiles may not register it.
+ RuntimeProfile::Counter* _remote_read_requests = nullptr;
+};
+
+} // namespace doris
diff --git a/be/src/exec/spill/spill_s3_file_writer.cpp
b/be/src/exec/spill/spill_s3_file_writer.cpp
new file mode 100644
index 00000000000..053761cbc52
--- /dev/null
+++ b/be/src/exec/spill/spill_s3_file_writer.cpp
@@ -0,0 +1,127 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "exec/spill/spill_s3_file_writer.h"
+
+#include <chrono>
+#include <thread>
+#include <utility>
+
+#include "common/config.h"
+#include "exec/spill/spill_file.h"
+#include "exec/spill/spill_file_manager.h"
+#include "runtime/exec_env.h"
+#include "runtime/runtime_profile_counter_names.h"
+#include "runtime/workload_management/io_context.h"
+#include "util/defer_op.h"
+#include "util/stopwatch.hpp"
+#include "util/threadpool.h"
+
+namespace doris {
+
+SpillS3FileWriter::SpillS3FileWriter(const std::shared_ptr<SpillFile>&
spill_file,
+ RuntimeState* state, RuntimeProfile*
profile,
+ SpillDataDir* data_dir,
io::FileSystemSPtr fs,
+ std::string spill_dir)
+ : SpillFileWriter(spill_file, state, profile, data_dir, std::move(fs),
+ std::move(spill_dir)) {
+ RuntimeProfile* custom_profile = profile->get_child("CustomCounters");
+ _remote_upload_bytes =
custom_profile->get_counter(profile::SPILL_REMOTE_UPLOAD_BYTES);
+ _remote_upload_timer =
custom_profile->get_counter(profile::SPILL_REMOTE_UPLOAD_TIME);
+ _remote_upload_wait_timer =
custom_profile->get_counter(profile::SPILL_REMOTE_UPLOAD_WAIT_TIME);
+}
+
+SpillS3FileWriter::~SpillS3FileWriter() {
+ // Base destruction cannot dispatch virtual hooks to this class. Close
while the S3
+ // counters and queue admission hook are still alive, then let the base
clear registration.
+ Status st = close();
+ if (!st.ok()) {
+ LOG(WARNING) << "SpillS3FileWriter::~SpillS3FileWriter() failed: " <<
st.to_string()
+ << ", spill_dir=" << _spill_directory();
+ }
+}
+
+Status SpillS3FileWriter::_wait_for_upload_queue() {
+ auto is_cancelled = [this]() {
+ return _resource_ctx != nullptr &&
_resource_ctx->task_controller()->is_cancelled();
+ };
+ // A cancelled query must not start another (billed) upload.
+ if (is_cancelled()) {
+ return Status::Cancelled("query cancelled before uploading spill
data");
+ }
+ auto* upload_pool = ExecEnv::GetInstance()->s3_file_upload_thread_pool();
+ auto queue_full = [upload_pool]() {
+ return static_cast<int64_t>(upload_pool->get_queue_size()) *
config::s3_write_buffer_size >=
+ config::spill_s3_max_inflight_upload_bytes;
+ };
+ if (!queue_full()) {
+ return Status::OK();
+ }
+ MonotonicStopWatch wait_watch;
+ wait_watch.start();
+ Defer record_wait {
+ [&]() { COUNTER_UPDATE(_remote_upload_wait_timer,
wait_watch.elapsed_time()); }};
+ // Queued uploads always finish, so the wait ends without help from other
writers.
+ while (queue_full()) {
+ if (is_cancelled()) {
+ return Status::Cancelled("query cancelled while waiting for the
spill upload queue");
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(5));
+ }
+ return Status::OK();
+}
+
+Status SpillS3FileWriter::_append_to_part(const Slice& data) {
+ RETURN_IF_ERROR(_part_append_status);
+ // S3FileWriter splits `data` into upload buffers and queues every full
one without waiting,
+ // so the queue is checked once per append: `data` is already in memory,
and the wait keeps
+ // the next append from producing faster than object storage takes the
data.
+ _part_append_status = _wait_for_upload_queue();
+ RETURN_IF_ERROR(_part_append_status);
+ MonotonicStopWatch append_watch;
+ append_watch.start();
+ _part_append_status = _file_writer->append(data);
+ _part_upload_ns += append_watch.elapsed_time();
+ return _part_append_status;
+}
+
+void SpillS3FileWriter::_record_spill_write_bytes(int64_t bytes) {
+ if (_resource_ctx) {
+
_resource_ctx->io_context()->update_spill_write_bytes_to_remote_storage(bytes);
+ }
+}
+
+void SpillS3FileWriter::_on_part_closed(const std::shared_ptr<SpillFile>&
spill_file, bool closed,
+ int64_t close_ns) {
+ _part_upload_ns += close_ns;
+ // A successful close publishes the whole part, including its footer.
Failed uploads
+ // can still be in flight, so their bytes are not counted as successfully
uploaded.
+ const int64_t uploaded_bytes = closed ? _part_written_bytes : 0;
+ if (_remote_upload_bytes != nullptr) {
+ COUNTER_UPDATE(_remote_upload_bytes, uploaded_bytes);
+ COUNTER_UPDATE(_remote_upload_timer, _part_upload_ns);
+ }
+
ExecEnv::GetInstance()->spill_file_mgr()->update_spill_remote_write(uploaded_bytes);
+ // A failed close may have published an object despite returning an error.
The caller
+ // has already drained its writer, so reconcile the prefix before charging
the file.
+ if (!closed && spill_file) {
+ spill_file->account_potential_part(_part_written_bytes);
+ }
+ _part_upload_ns = 0;
+}
+
+} // namespace doris
diff --git a/be/src/exec/spill/spill_s3_file_writer.h
b/be/src/exec/spill/spill_s3_file_writer.h
new file mode 100644
index 00000000000..7bea2e771d4
--- /dev/null
+++ b/be/src/exec/spill/spill_s3_file_writer.h
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include "exec/spill/spill_file_writer.h"
+
+namespace doris {
+
+/// S3-specific upload admission and accounting for a spill part.
+class SpillS3FileWriter final : public SpillFileWriter {
+public:
+ SpillS3FileWriter(const std::shared_ptr<SpillFile>& spill_file,
RuntimeState* state,
+ RuntimeProfile* profile, SpillDataDir* data_dir,
io::FileSystemSPtr fs,
+ std::string spill_dir);
+ ~SpillS3FileWriter() override;
+
+protected:
+ Status _append_to_part(const Slice& data) override;
+ void _record_spill_write_bytes(int64_t bytes) override;
+ void _on_part_closed(const std::shared_ptr<SpillFile>& spill_file, bool
closed,
+ int64_t close_ns) override;
+
+private:
+ Status _wait_for_upload_queue();
+
+ int64_t _part_upload_ns = 0;
+ RuntimeProfile::Counter* _remote_upload_bytes = nullptr;
+ RuntimeProfile::Counter* _remote_upload_timer = nullptr;
+ RuntimeProfile::Counter* _remote_upload_wait_timer = nullptr;
+};
+
+} // namespace doris
diff --git a/be/test/vec/spill/spill_file_s3_test.cpp
b/be/test/vec/spill/spill_file_s3_test.cpp
index fef54874ae4..c4496a5cc4e 100644
--- a/be/test/vec/spill/spill_file_s3_test.cpp
+++ b/be/test/vec/spill/spill_file_s3_test.cpp
@@ -51,6 +51,8 @@
#include "exec/spill/spill_file_manager.h"
#include "exec/spill/spill_file_reader.h"
#include "exec/spill/spill_file_writer.h"
+#include "exec/spill/spill_s3_file_reader.h"
+#include "exec/spill/spill_s3_file_writer.h"
#include "io/fs/file_system.h"
#include "io/fs/file_writer.h"
#include "io/fs/local_file_system.h"
@@ -464,6 +466,7 @@ protected:
_saved_inflight = config::spill_s3_max_inflight_upload_bytes;
_saved_check_after_upload =
config::enable_s3_object_check_after_upload;
_saved_read_coalesce = config::spill_s3_read_coalesce_bytes;
+ _saved_enable_file_cache = config::enable_file_cache;
// The manager talks to meta-service through the cloud storage engine
only in cloud
// mode; there is no engine in this test, so pin the mode regardless
of earlier tests.
_saved_deploy_mode = config::deploy_mode;
@@ -477,6 +480,8 @@ protected:
config::s3_write_buffer_size = 8 * 1024;
config::spill_file_part_size_bytes = 32 * 1024;
config::spill_storage_limit_bytes = 0;
+ // The mock S3 fixture has no FileCacheFactory; S3 spill explicitly
bypasses file cache.
+ config::enable_file_cache = false;
_runtime_state = std::make_unique<MockRuntimeState>();
_profile = std::make_unique<RuntimeProfile>("test");
@@ -513,6 +518,7 @@ protected:
config::spill_s3_max_inflight_upload_bytes = _saved_inflight;
config::enable_s3_object_check_after_upload =
_saved_check_after_upload;
config::spill_s3_read_coalesce_bytes = _saved_read_coalesce;
+ config::enable_file_cache = _saved_enable_file_cache;
config::deploy_mode = _saved_deploy_mode;
config::cloud_unique_id = _saved_cloud_unique_id;
BackendOptions::set_localhost(_saved_localhost);
@@ -793,11 +799,67 @@ protected:
int64_t _saved_inflight = 0;
bool _saved_check_after_upload = true;
int64_t _saved_read_coalesce = 0;
+ bool _saved_enable_file_cache = true;
std::string _saved_deploy_mode;
std::string _saved_cloud_unique_id;
std::string _saved_localhost;
};
+TEST_F(SpillFileS3Test, WriterFactoryUsesS3Subclass) {
+ _create_manager();
+ SpillFileSPtr spill_file;
+ ASSERT_TRUE(_manager->create_spill_file("query_factory/sort-1",
spill_file).ok());
+ SpillFileWriterSPtr writer;
+ ASSERT_TRUE(spill_file->create_writer(_runtime_state.get(),
_profile.get(), writer).ok());
+ ASSERT_NE(dynamic_cast<SpillS3FileWriter*>(writer.get()), nullptr);
+ ASSERT_TRUE(writer->close().ok());
+ writer.reset();
+ spill_file.reset();
+}
+
+TEST_F(SpillFileS3Test, ReaderFactoryUsesS3Subclass) {
+ _create_manager();
+ Status st;
+ auto spill_file = _write_blocks("query_reader_factory/sort-1", {}, &st);
+ ASSERT_TRUE(st.ok()) << st;
+ auto reader = spill_file->create_reader(_runtime_state.get(),
_profile.get());
+ ASSERT_NE(dynamic_cast<SpillS3FileReader*>(reader.get()), nullptr);
+ ASSERT_TRUE(reader->open().ok());
+ Block block;
+ bool eos = false;
+ ASSERT_TRUE(reader->read(&block, &eos).ok());
+ ASSERT_TRUE(eos);
+ ASSERT_TRUE(reader->close().ok());
+ ASSERT_EQ(mock_store().get_requests, 0);
+ ASSERT_EQ(_counter(profile::SPILL_REMOTE_READ_REQUESTS), 0);
+}
+
+TEST_F(SpillFileS3Test, ImplicitClosePreservesRemoteAccounting) {
+ _create_manager();
+ SpillFileSPtr spill_file;
+ ASSERT_TRUE(_manager->create_spill_file("query_implicit/sort-1",
spill_file).ok());
+ SpillFileWriterSPtr writer;
+ ASSERT_TRUE(spill_file->create_writer(_runtime_state.get(),
_profile.get(), writer).ok());
+ std::mt19937 rng(42);
+ auto block = _random_string_block(rng, 8, 100);
+ ASSERT_TRUE(writer->write_block(_runtime_state.get(), block).ok());
+
+ // Destruction must dispatch the S3 footer, upload and accounting hooks
before the
+ // base destructor runs; no explicit close is issued here.
+ writer.reset();
+ ASSERT_TRUE(spill_file->ready_for_reading());
+ const auto object_bytes =
+ static_cast<int64_t>(_object(spill_root() +
"/query_implicit/sort-1/0").size());
+ ASSERT_GT(object_bytes, 0);
+ ASSERT_EQ(_counter(profile::SPILL_REMOTE_UPLOAD_BYTES), object_bytes);
+ ASSERT_EQ(_manager->remote_spill_data_bytes(), object_bytes);
+ ASSERT_EQ(_data_dir->get_spill_data_bytes(), object_bytes);
+ ASSERT_EQ(_read_all(spill_file), _column_values(block));
+ spill_file.reset();
+ ASSERT_EQ(_manager->remote_spill_data_bytes(), 0);
+ ASSERT_EQ(_data_dir->get_spill_data_bytes(), 0);
+}
+
TEST_F(SpillFileS3Test, RemoteStoreLayout) {
_create_manager();
ASSERT_TRUE(_data_dir->is_remote());
@@ -1423,6 +1485,39 @@ TEST_F(SpillFileS3Test, ReadCoalescesAdjacentBlocks) {
}
}
+TEST_F(SpillFileS3Test, ReadCoalesceClampedBySpillBufferSize) {
+ constexpr int64_t kWindow = 1024 * 1024;
+ config::spill_file_part_size_bytes = 8 * kWindow;
+ config::spill_s3_read_coalesce_bytes = 8 * kWindow;
+ auto query_options = _runtime_state->query_options();
+ query_options.__set_spill_buffer_size_bytes(kWindow);
+ _runtime_state->set_query_options(query_options);
+ _create_manager();
+ std::mt19937 rng(61);
+ std::vector<Block> blocks;
+ for (int i = 0; i < 8; ++i) {
+ blocks.push_back(_random_string_block(rng, 256, 1024));
+ }
+ Status st;
+ auto spill_file = _write_blocks("query_read_buffer_limit/sort-1", blocks,
&st);
+ ASSERT_TRUE(st.ok()) << st;
+ auto keys = mock_store().keys_with_prefix(kBucket, spill_root() +
"/query_read_buffer_limit/");
+ ASSERT_EQ(keys.size(), 1);
+ const std::string object = _object(keys[0]);
+ ASSERT_GT(object.size(), kWindow);
+ const auto offsets = _block_offsets(object);
+ std::vector<size_t> expected_gets {64 * 1024};
+ auto reads = _coalesced_reads(offsets, kWindow);
+ expected_gets.insert(expected_gets.end(), reads.begin(), reads.end());
+
+ _reset_get_stats();
+ ASSERT_EQ(_read_all(spill_file), _values_of(blocks));
+ ASSERT_EQ(_get_sizes(), expected_gets);
+ ASSERT_EQ(_counter(profile::SPILL_REMOTE_READ_REQUESTS),
expected_gets.size());
+ auto* io_ctx =
_runtime_state->get_query_ctx()->resource_ctx()->io_context();
+ ASSERT_EQ(io_ctx->spill_read_bytes_from_local_storage(), 0);
+}
+
// A read size smaller than the block offset array: the probe reads only its
tail and one
// more GET fetches the rest of the footer, then every block is read alone.
TEST_F(SpillFileS3Test, FooterLargerThanProbe) {
diff --git a/be/test/vec/spill/spill_file_test.cpp
b/be/test/vec/spill/spill_file_test.cpp
index be0c5375648..003aba1baf4 100644
--- a/be/test/vec/spill/spill_file_test.cpp
+++ b/be/test/vec/spill/spill_file_test.cpp
@@ -27,6 +27,7 @@
#include <numeric>
#include <set>
#include <string>
+#include <typeinfo>
#include <vector>
#include "common/config.h"
@@ -45,6 +46,7 @@
#include "runtime/exec_env.h"
#include "runtime/fragment_mgr.h"
#include "runtime/runtime_profile.h"
+#include "runtime/workload_management/io_context.h"
#include "storage/olap_define.h"
#include "testutil/column_helper.h"
#include "testutil/mock/mock_query_context.h"
@@ -213,6 +215,8 @@ TEST_F(SpillFileTest, CreateWriterAndReader) {
st = spill_file->create_writer(_runtime_state.get(), _profile.get(),
writer);
ASSERT_TRUE(st.ok()) << st.to_string();
ASSERT_TRUE(writer != nullptr);
+ SpillFileWriter* raw_writer = writer.get();
+ ASSERT_EQ(typeid(*raw_writer), typeid(SpillFileWriter));
// Close writer with no data written
st = writer->close();
@@ -222,6 +226,8 @@ TEST_F(SpillFileTest, CreateWriterAndReader) {
// Create reader on empty file (0 parts)
auto reader = spill_file->create_reader(_runtime_state.get(),
_profile.get());
ASSERT_TRUE(reader != nullptr);
+ SpillFileReader* raw_reader = reader.get();
+ ASSERT_EQ(typeid(*raw_reader), typeid(SpillFileReader));
st = reader->open();
ASSERT_TRUE(st.ok()) << st.to_string();
@@ -1894,6 +1900,11 @@ TEST_F(SpillFileTest, ReadCounters) {
auto* read_file_size = _custom_profile->get_counter("SpillReadFileBytes");
ASSERT_TRUE(read_file_size != nullptr);
ASSERT_GT(read_file_size->value(), 0);
+ auto* io_ctx =
_runtime_state->get_query_ctx()->resource_ctx()->io_context();
+ ASSERT_EQ(io_ctx->spill_read_bytes_from_local_storage(),
read_file_size->value());
+ ASSERT_EQ(io_ctx->spill_read_bytes_from_remote_storage(), 0);
+ ASSERT_EQ(io_ctx->spill_remote_read_requests(), 0);
+
ASSERT_EQ(_custom_profile->get_counter(profile::SPILL_REMOTE_READ_REQUESTS)->value(),
0);
}
// Regression test: SpillFileReader used to look up the deserialize timer
under a
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]