github-actions[bot] commented on code in PR #65658:
URL: https://github.com/apache/doris/pull/65658#discussion_r3709208902
##########
be/src/io/cache/block_file_cache.cpp:
##########
@@ -1370,6 +1503,8 @@ void BlockFileCache::try_evict_in_advance(size_t size,
std::lock_guard<std::mute
// remove specific cache synchronously, for critical operations
// if in use, cache meta will be deleted after use and the block file is then
deleted asynchronously
void BlockFileCache::remove_if_cached(const UInt128Wrapper& file_key) {
+ DORIS_CHECK(_async_write_service != nullptr);
+ _async_write_service->invalidate_pending_writes();
Review Comment:
[P1] Keep per-file invalidation scoped to this key. Both per-file removal
APIs advance the service-wide disk epoch, while workers discard every accepted
task carrying the old epoch regardless of its hash. Ordinary rowset/cache
recycling invokes this path once per segment and index, and the new test even
expects `queued_hash` to remain a gap after only `active_hash` is removed. A
recycle burst can therefore continuously cancel unrelated cache fills and
effectively disable async caching on that disk. Please use a per-key generation
for these APIs and reserve the global epoch for whole-cache invalidation.
##########
be/src/io/cache/async_cache_write_service.cpp:
##########
@@ -0,0 +1,731 @@
+// 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 "io/cache/async_cache_write_service.h"
+
+#include <algorithm>
+#include <exception>
+#include <limits>
+#include <optional>
+#include <thread>
+#include <type_traits>
+#include <utility>
+
+#include "common/logging.h"
+#include "core/allocator.h"
+#include "cpp/sync_point.h"
+#include "io/cache/block_file_cache.h"
+#include "runtime/thread_context.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
+#include "util/time.h"
+
+namespace doris::io {
+
+using AsyncCacheWriteAllocator = Allocator<false, false, false,
DefaultMemoryAllocator, true>;
+
+namespace {
+
+static_assert(std::is_nothrow_move_constructible_v<AsyncCacheWriteTask>);
+static_assert(std::is_nothrow_move_assignable_v<AsyncCacheWriteTask>);
+
+/// Keep an in-progress phase gauge balanced across every return path.
+class ScopedActiveCounter {
+public:
+ explicit ScopedActiveCounter(std::atomic<size_t>& counter) :
_counter(counter) {
+ _counter.fetch_add(1, std::memory_order_relaxed);
+ }
+
+ ~ScopedActiveCounter() { _counter.fetch_sub(1, std::memory_order_relaxed);
}
+
+private:
+ std::atomic<size_t>& _counter;
+};
+
+/// Acquire the FIFO mutex while measuring only the actual lock wait and
critical-section hold.
+class TimedQueueLock {
+public:
+ TimedQueueLock(std::mutex& mutex, bvar::LatencyRecorder& wait_latency,
+ bvar::LatencyRecorder& hold_latency)
+ : _lock(mutex, std::defer_lock),
+ _wait_latency(wait_latency),
+ _hold_latency(hold_latency) {
+ const int64_t wait_start_us = MonotonicMicros();
+ _lock.lock();
+ _acquired_at_us = MonotonicMicros();
+ _wait_us = _acquired_at_us - wait_start_us;
+ }
+
+ ~TimedQueueLock() {
+ const int64_t hold_us = MonotonicMicros() - _acquired_at_us;
+ _lock.unlock();
+ _wait_latency << _wait_us;
+ _hold_latency << hold_us;
+ }
+
+private:
+ std::unique_lock<std::mutex> _lock;
+ bvar::LatencyRecorder& _wait_latency;
+ bvar::LatencyRecorder& _hold_latency;
+ int64_t _acquired_at_us {0};
+ int64_t _wait_us {0};
+};
+
+} // namespace
+
+Status resolve_async_file_cache_write_max_pending_bytes_per_disk(int64_t
configured_bytes,
+ int64_t
be_mem_limit,
+ size_t*
resolved_bytes) {
+ DORIS_CHECK(resolved_bytes != nullptr);
+ if (configured_bytes > 0) {
+ *resolved_bytes = static_cast<size_t>(configured_bytes);
+ return Status::OK();
+ }
+ if (configured_bytes != -1) {
+ return Status::InvalidArgument(
+ "async file cache write pending byte limit must be positive or
-1");
+ }
+
+ DORIS_CHECK(be_mem_limit > 0);
+ constexpr int64_t kMinimumAutoPendingBytes = 512LL * 1024 * 1024;
+ *resolved_bytes = static_cast<size_t>(std::max(kMinimumAutoPendingBytes,
be_mem_limit / 100));
+ return Status::OK();
+}
+
+class AsyncCacheWriteService::Worker : public
std::enable_shared_from_this<Worker> {
+public:
+ explicit Worker(AsyncCacheWriteService& service) : _service(service) {}
+
+ Status start() {
+ auto self = shared_from_this();
+ return _service._worker_pool->submit_func([self = std::move(self)]() {
self->_run(); });
+ }
+
+ void request_stop() { _stop_requested.store(true,
std::memory_order_release); }
+
+ void wait_until_stopped() { _stopped.wait(); }
+
+private:
+ void _run() {
+ _service._running_worker_count.fetch_add(1, std::memory_order_relaxed);
+ Defer mark_finished {[this]() {
+ const size_t old_running =
+ _service._running_worker_count.fetch_sub(1,
std::memory_order_relaxed);
+ DCHECK_GT(old_running, 0);
+ _stopped.count_down();
+ }};
+
+ while (!_stop_requested.load(std::memory_order_acquire)) {
+ AsyncCacheWriteTask task;
+ if (_service._try_take_task(&task)) {
+ _service._process_task(std::move(task));
+ continue;
+ }
+
+ if (_service._shutdown_requested.load(std::memory_order_acquire) &&
+ _service._pending_count.load(std::memory_order_acquire) == 0) {
+ return;
+ }
+ std::unique_lock lock(_service._queue_mutex);
+ _service._queue_cv.wait(lock, [this]() {
+ const bool shutdown_requested =
+
_service._shutdown_requested.load(std::memory_order_acquire);
+ return !_service._queue.empty() ||
+ _stop_requested.load(std::memory_order_acquire) ||
+ (shutdown_requested &&
+
_service._pending_count.load(std::memory_order_relaxed) == 0);
+ });
+ }
+ }
+
+ AsyncCacheWriteService& _service;
+ std::atomic<bool> _stop_requested {false};
+ CountDownLatch _stopped {1};
+};
+
+AsyncCacheWriteBuffer::AsyncCacheWriteBuffer(size_t size,
+
std::shared_ptr<MemTrackerLimiter> tracker)
+ : _size(size), _tracker(std::move(tracker)) {
+ AsyncCacheWriteAllocator allocator;
+ _data = reinterpret_cast<char*>(allocator.alloc(_size));
+}
+
+AsyncCacheWriteBuffer::~AsyncCacheWriteBuffer() {
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_tracker);
+ AsyncCacheWriteAllocator allocator;
+ allocator.free(_data, _size);
+}
+
+AsyncCacheWriteService::AsyncCacheWriteService(BlockFileCache* cache,
+ AsyncCacheWriteServiceOptions
options)
+ : _cache(cache),
+ _options(std::make_shared<const
AsyncCacheWriteServiceOptions>(options)),
+ _configured_worker_count(options.worker_count) {
+ DORIS_CHECK(_cache != nullptr);
+ DORIS_CHECK(options.worker_count > 0);
+ DORIS_CHECK(options.max_pending_bytes > 0);
+
+ const char* prefix = _cache->get_base_path().c_str();
+ _mem_tracker = MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::CACHE,
+ fmt::format("AsyncFileCacheWrite:{}", _cache->get_base_path()));
+ _pending_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_pending_count",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->pending_count();
+ },
+ this);
+ _pending_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_pending_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->pending_bytes();
+ },
+ this);
+ _queued_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_queue_size",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->queued_count();
+ },
+ this);
+ _queued_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_queued_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->queued_bytes();
+ },
+ this);
+ _active_task_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_tasks",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->active_task_count();
+ },
+ this);
+ _active_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->active_bytes();
+ },
+ this);
+ _running_worker_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_running_workers",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->running_worker_count();
+ },
+ this);
+ _configured_worker_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_configured_workers",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_configured_worker_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _max_pending_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_max_pending_bytes",
+ [](void* service) {
+ return static_cast<AsyncCacheWriteService*>(service)
+ ->_options.load(std::memory_order_acquire)
+ ->max_pending_bytes;
+ },
+ this);
+ _active_get_or_set_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_get_or_set",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_get_or_set_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _active_append_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_append",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_append_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _active_finalize_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_finalize",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_finalize_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _buffer_memory_metric = std::make_shared<bvar::PassiveStatus<int64_t>>(
+ prefix, "async_cache_write_buffer_memory_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->buffer_memory_bytes();
+ },
+ this);
+ _submitted_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_submitted_total");
+ _submitted_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_submitted_bytes_total");
+ _finished_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_finished_total");
+ _finished_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_finished_bytes_total");
+ _worker_finished_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_worker_finished_total");
+ _worker_finished_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_worker_finished_bytes_total");
+ _evicted_oldest_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_evicted_oldest_total");
+ _evicted_oldest_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_evicted_oldest_bytes_total");
+ _evicted_oldest_age_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_evicted_oldest_age_us");
+ _rejected_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_rejected_total");
+ _reject_not_running_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_reject_not_running_total");
+ _reject_backpressure_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_reject_backpressure_total");
+ _buffer_alloc_fail_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_buffer_alloc_fail_total");
+ _submit_latency_metric =
+ std::make_shared<bvar::LatencyRecorder>(prefix,
"async_cache_write_submit_latency_us");
+ _buffer_alloc_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_buffer_alloc_latency_us");
+ _queue_wait_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_wait_latency_us");
+ _queue_lock_wait_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_lock_wait_latency_us");
+ _queue_lock_hold_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_lock_hold_latency_us");
+ _worker_task_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_worker_task_latency_us");
+ _get_or_set_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_get_or_set_latency_us");
+ _append_latency_metric =
+ std::make_shared<bvar::LatencyRecorder>(prefix,
"async_cache_write_append_latency_us");
+ _finalize_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_finalize_latency_us");
+ _skip_downloaded_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_downloaded_total");
+ _skip_downloading_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_downloading_total");
+ _skip_partial_overlap_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_partial_overlap_total");
+ _drop_stale_epoch_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_drop_stale_epoch_total");
+ _skip_deleting_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_deleting_total");
+ _append_fail_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_append_fail_total");
+ _finalize_fail_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_finalize_fail_total");
+ _persisted_blocks_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_persisted_blocks_total");
+ _persisted_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_persisted_bytes_total");
+}
+
+AsyncCacheWriteService::~AsyncCacheWriteService() {
+ shutdown();
+}
+
+Status AsyncCacheWriteService::start() {
+ std::lock_guard resize_lock(_resize_mutex);
+ if (_shutdown_requested.load(std::memory_order_acquire) ||
+ !_accepting.load(std::memory_order_acquire)) {
+ return Status::InternalError("async file cache write service is
shutting down");
+ }
+ if (_started.load(std::memory_order_acquire)) {
+ return Status::OK();
+ }
+
+ const size_t worker_count =
_configured_worker_count.load(std::memory_order_acquire);
+ if (_worker_pool == nullptr) {
+ RETURN_IF_ERROR(
+ ThreadPoolBuilder(fmt::format("AsyncFileCacheWrite-{}",
+ std::hash<std::string>
{}(_cache->get_base_path())))
+ .set_min_threads(0)
+ .set_max_threads(static_cast<int>(worker_count))
+ .set_max_queue_size(128)
+ .build(&_worker_pool));
+ }
+ // A failed earlier start may have left a partial worker set. Reconcile
the owned workers with
+ // the latest configured count before publishing readiness.
+ RETURN_IF_ERROR(_resize_workers_locked(worker_count));
+ // Publish readiness only after every configured worker loop has been
accepted by the pool.
+ _started.store(true, std::memory_order_release);
+ return Status::OK();
+}
+
+bool AsyncCacheWriteService::try_submit(AsyncCacheWriteTask task) {
+ DORIS_CHECK(task.buffer != nullptr);
+ DORIS_CHECK(task.write_size > 0);
+ DORIS_CHECK(task.write_size <= task.buffer->size());
+ DORIS_CHECK(task.write_size <= std::numeric_limits<size_t>::max() -
task.file_offset);
+ const int64_t submit_start_us = MonotonicMicros();
+ Defer record_submit_latency {
+ [&]() { *_submit_latency_metric << (MonotonicMicros() -
submit_start_us); }};
+ _active_submitters.fetch_add(1, std::memory_order_acq_rel);
+ Defer submitter_done {[&]() { _active_submitters.fetch_sub(1,
std::memory_order_acq_rel); }};
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::try_submit:after_register",
&task);
+ if (!_started.load(std::memory_order_acquire) ||
!_accepting.load(std::memory_order_acquire)) {
+ *_rejected_metric << 1;
+ *_reject_not_running_metric << 1;
+ return false;
+ }
+
+ const size_t task_buffer_bytes = task.buffer->size();
+ std::optional<AsyncCacheWriteTask> victim;
+ {
+ TimedQueueLock lock(_queue_mutex, *_queue_lock_wait_latency_metric,
+ *_queue_lock_hold_latency_metric);
+ const auto options = _options.load(std::memory_order_acquire);
+ const size_t max_pending_bytes = options->max_pending_bytes;
+ const size_t pending_bytes =
_pending_bytes.load(std::memory_order_relaxed);
+ if (_task_buffer_size == 0) {
+ _task_buffer_size = task_buffer_bytes;
+ }
+ DORIS_CHECK(task_buffer_bytes == _task_buffer_size);
+
+ if (task_buffer_bytes > max_pending_bytes) {
+ *_rejected_metric << 1;
+ *_reject_backpressure_metric << 1;
+ return false;
+ }
+
+ const bool has_capacity = pending_bytes <= max_pending_bytes -
task_buffer_bytes;
+ if (!has_capacity && _queue.empty()) {
+ *_rejected_metric << 1;
+ *_reject_backpressure_metric << 1;
+ return false;
+ }
+
+ _queue.push_back(std::move(task));
+ if (has_capacity) {
+ _queued_bytes.fetch_add(task_buffer_bytes,
std::memory_order_relaxed);
+ _pending_count.fetch_add(1, std::memory_order_relaxed);
+ _pending_bytes.fetch_add(task_buffer_bytes,
std::memory_order_relaxed);
+ } else {
+ victim.emplace(std::move(_queue.front()));
+ _queue.pop_front();
+ }
+ }
+
+ *_submitted_metric << 1;
+ *_submitted_bytes_metric << task_buffer_bytes;
+ _queue_cv.notify_one();
+ if (victim) {
+ _finalize_task(std::move(*victim),
TaskFinalizationReason::EVICTED_OLDEST);
+ }
+ return true;
+}
+
+Status AsyncCacheWriteService::allocate_tracked_buffer(size_t size,
+
AsyncCacheWriteBufferPtr* buffer) {
+ DORIS_CHECK(buffer != nullptr);
+ DORIS_CHECK(size > 0);
+ const int64_t allocation_start_us = MonotonicMicros();
+ Defer record_allocation_latency {
+ [&]() { *_buffer_alloc_latency_metric << (MonotonicMicros() -
allocation_start_us); }};
+ Status injected_status;
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::allocate_tracked_buffer:inject_failure",
+ &injected_status);
+ if (!injected_status.ok()) {
+ *_buffer_alloc_fail_metric << 1;
+ return injected_status;
+ }
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_mem_tracker);
+ Status status = Status::OK();
+ try {
+ *buffer = AsyncCacheWriteBufferPtr(new AsyncCacheWriteBuffer(size,
_mem_tracker));
+ } catch (const std::exception& e) {
+ status = Status::MemoryAllocFailed("allocate async file cache write
buffer failed: {}",
+ e.what());
+ }
+ if (!status.ok()) {
+ *_buffer_alloc_fail_metric << 1;
+ }
+ return status;
+}
+
+void AsyncCacheWriteService::_process_task(AsyncCacheWriteTask task) {
+ Defer finish {[&]() { _finish_active_task(std::move(task)); }};
+
+ const int64_t age_us = MonotonicMicros() - task.submit_ts_us;
+ *_queue_wait_latency_metric << age_us;
+ if (!is_current_write_epoch(task.write_epoch)) {
+ *_drop_stale_epoch_metric << 1;
+ return;
+ }
+
+ const int64_t start_us = MonotonicMicros();
+ Status status = _write_one(task);
Review Comment:
[P1] Contain allocation failures inside each worker task. `_write_one()`
reaches the allocating `get_or_set()` miss path, but this call chain has no
exception boundary; neither `ThreadPool::dispatch_thread()` nor
`Thread::supervise_thread()` catches an exception escaping the runnable. An
actual Doris allocator failure or `std::bad_alloc` here therefore crosses the
pthread entry and terminates the BE instead of dropping one best-effort cache
write. Please convert exceptions to a logged non-OK `Status` around each task
and verify a later task and shutdown still complete after an injected failure.
##########
regression-test/suites/cloud_p0/cache/test_async_file_cache_write.groovy:
##########
@@ -0,0 +1,140 @@
+// 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.
+
+import org.apache.doris.regression.suite.ClusterOptions
+
+suite("test_async_file_cache_write", "docker") {
+ def options = new ClusterOptions()
+ options.cloudMode = true
+ options.setFeNum(1)
+ options.setBeNum(1)
+ options.msNum = 1
+ options.beConfigs += [
Review Comment:
[P1] Disable the S3-writer cache path for this cold-read test. Its new
test-only switch defaults to true, and with asynchronous flush
`UploadFileBuffer::on_upload()` signals writer completion before it calls
`upload_to_local_file_cache()`. Thus `INSERT`/`SYNC` can finish, this suite can
clear the cache, and the delayed load-side fill can then repopulate it before
the first SELECT. The `submittedAfter > submittedBefore` assertion becomes
timing-dependent and no longer proves query-side async population. Add
`enable_file_cache_write_from_s3_file_writer=false` here (or explicitly fence
that writer) before relying on the cold-query metrics.
##########
be/src/io/cache/async_cache_write_service.cpp:
##########
@@ -0,0 +1,731 @@
+// 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 "io/cache/async_cache_write_service.h"
+
+#include <algorithm>
+#include <exception>
+#include <limits>
+#include <optional>
+#include <thread>
+#include <type_traits>
+#include <utility>
+
+#include "common/logging.h"
+#include "core/allocator.h"
+#include "cpp/sync_point.h"
+#include "io/cache/block_file_cache.h"
+#include "runtime/thread_context.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
+#include "util/time.h"
+
+namespace doris::io {
+
+using AsyncCacheWriteAllocator = Allocator<false, false, false,
DefaultMemoryAllocator, true>;
+
+namespace {
+
+static_assert(std::is_nothrow_move_constructible_v<AsyncCacheWriteTask>);
+static_assert(std::is_nothrow_move_assignable_v<AsyncCacheWriteTask>);
+
+/// Keep an in-progress phase gauge balanced across every return path.
+class ScopedActiveCounter {
+public:
+ explicit ScopedActiveCounter(std::atomic<size_t>& counter) :
_counter(counter) {
+ _counter.fetch_add(1, std::memory_order_relaxed);
+ }
+
+ ~ScopedActiveCounter() { _counter.fetch_sub(1, std::memory_order_relaxed);
}
+
+private:
+ std::atomic<size_t>& _counter;
+};
+
+/// Acquire the FIFO mutex while measuring only the actual lock wait and
critical-section hold.
+class TimedQueueLock {
+public:
+ TimedQueueLock(std::mutex& mutex, bvar::LatencyRecorder& wait_latency,
+ bvar::LatencyRecorder& hold_latency)
+ : _lock(mutex, std::defer_lock),
+ _wait_latency(wait_latency),
+ _hold_latency(hold_latency) {
+ const int64_t wait_start_us = MonotonicMicros();
+ _lock.lock();
+ _acquired_at_us = MonotonicMicros();
+ _wait_us = _acquired_at_us - wait_start_us;
+ }
+
+ ~TimedQueueLock() {
+ const int64_t hold_us = MonotonicMicros() - _acquired_at_us;
+ _lock.unlock();
+ _wait_latency << _wait_us;
+ _hold_latency << hold_us;
+ }
+
+private:
+ std::unique_lock<std::mutex> _lock;
+ bvar::LatencyRecorder& _wait_latency;
+ bvar::LatencyRecorder& _hold_latency;
+ int64_t _acquired_at_us {0};
+ int64_t _wait_us {0};
+};
+
+} // namespace
+
+Status resolve_async_file_cache_write_max_pending_bytes_per_disk(int64_t
configured_bytes,
+ int64_t
be_mem_limit,
+ size_t*
resolved_bytes) {
+ DORIS_CHECK(resolved_bytes != nullptr);
+ if (configured_bytes > 0) {
+ *resolved_bytes = static_cast<size_t>(configured_bytes);
+ return Status::OK();
+ }
+ if (configured_bytes != -1) {
+ return Status::InvalidArgument(
+ "async file cache write pending byte limit must be positive or
-1");
+ }
+
+ DORIS_CHECK(be_mem_limit > 0);
+ constexpr int64_t kMinimumAutoPendingBytes = 512LL * 1024 * 1024;
+ *resolved_bytes = static_cast<size_t>(std::max(kMinimumAutoPendingBytes,
be_mem_limit / 100));
+ return Status::OK();
+}
+
+class AsyncCacheWriteService::Worker : public
std::enable_shared_from_this<Worker> {
+public:
+ explicit Worker(AsyncCacheWriteService& service) : _service(service) {}
+
+ Status start() {
+ auto self = shared_from_this();
+ return _service._worker_pool->submit_func([self = std::move(self)]() {
self->_run(); });
+ }
+
+ void request_stop() { _stop_requested.store(true,
std::memory_order_release); }
+
+ void wait_until_stopped() { _stopped.wait(); }
+
+private:
+ void _run() {
+ _service._running_worker_count.fetch_add(1, std::memory_order_relaxed);
+ Defer mark_finished {[this]() {
+ const size_t old_running =
+ _service._running_worker_count.fetch_sub(1,
std::memory_order_relaxed);
+ DCHECK_GT(old_running, 0);
+ _stopped.count_down();
+ }};
+
+ while (!_stop_requested.load(std::memory_order_acquire)) {
+ AsyncCacheWriteTask task;
+ if (_service._try_take_task(&task)) {
+ _service._process_task(std::move(task));
+ continue;
+ }
+
+ if (_service._shutdown_requested.load(std::memory_order_acquire) &&
+ _service._pending_count.load(std::memory_order_acquire) == 0) {
+ return;
+ }
+ std::unique_lock lock(_service._queue_mutex);
+ _service._queue_cv.wait(lock, [this]() {
+ const bool shutdown_requested =
+
_service._shutdown_requested.load(std::memory_order_acquire);
+ return !_service._queue.empty() ||
+ _stop_requested.load(std::memory_order_acquire) ||
+ (shutdown_requested &&
+
_service._pending_count.load(std::memory_order_relaxed) == 0);
+ });
+ }
+ }
+
+ AsyncCacheWriteService& _service;
+ std::atomic<bool> _stop_requested {false};
+ CountDownLatch _stopped {1};
+};
+
+AsyncCacheWriteBuffer::AsyncCacheWriteBuffer(size_t size,
+
std::shared_ptr<MemTrackerLimiter> tracker)
+ : _size(size), _tracker(std::move(tracker)) {
+ AsyncCacheWriteAllocator allocator;
+ _data = reinterpret_cast<char*>(allocator.alloc(_size));
+}
+
+AsyncCacheWriteBuffer::~AsyncCacheWriteBuffer() {
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_tracker);
+ AsyncCacheWriteAllocator allocator;
+ allocator.free(_data, _size);
+}
+
+AsyncCacheWriteService::AsyncCacheWriteService(BlockFileCache* cache,
+ AsyncCacheWriteServiceOptions
options)
+ : _cache(cache),
+ _options(std::make_shared<const
AsyncCacheWriteServiceOptions>(options)),
+ _configured_worker_count(options.worker_count) {
+ DORIS_CHECK(_cache != nullptr);
+ DORIS_CHECK(options.worker_count > 0);
+ DORIS_CHECK(options.max_pending_bytes > 0);
+
+ const char* prefix = _cache->get_base_path().c_str();
+ _mem_tracker = MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::CACHE,
+ fmt::format("AsyncFileCacheWrite:{}", _cache->get_base_path()));
+ _pending_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_pending_count",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->pending_count();
+ },
+ this);
+ _pending_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_pending_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->pending_bytes();
+ },
+ this);
+ _queued_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_queue_size",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->queued_count();
+ },
+ this);
+ _queued_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_queued_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->queued_bytes();
+ },
+ this);
+ _active_task_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_tasks",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->active_task_count();
+ },
+ this);
+ _active_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->active_bytes();
+ },
+ this);
+ _running_worker_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_running_workers",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->running_worker_count();
+ },
+ this);
+ _configured_worker_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_configured_workers",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_configured_worker_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _max_pending_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_max_pending_bytes",
+ [](void* service) {
+ return static_cast<AsyncCacheWriteService*>(service)
+ ->_options.load(std::memory_order_acquire)
+ ->max_pending_bytes;
+ },
+ this);
+ _active_get_or_set_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_get_or_set",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_get_or_set_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _active_append_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_append",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_append_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _active_finalize_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_finalize",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_finalize_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _buffer_memory_metric = std::make_shared<bvar::PassiveStatus<int64_t>>(
+ prefix, "async_cache_write_buffer_memory_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->buffer_memory_bytes();
+ },
+ this);
+ _submitted_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_submitted_total");
+ _submitted_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_submitted_bytes_total");
+ _finished_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_finished_total");
+ _finished_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_finished_bytes_total");
+ _worker_finished_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_worker_finished_total");
+ _worker_finished_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_worker_finished_bytes_total");
+ _evicted_oldest_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_evicted_oldest_total");
+ _evicted_oldest_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_evicted_oldest_bytes_total");
+ _evicted_oldest_age_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_evicted_oldest_age_us");
+ _rejected_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_rejected_total");
+ _reject_not_running_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_reject_not_running_total");
+ _reject_backpressure_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_reject_backpressure_total");
+ _buffer_alloc_fail_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_buffer_alloc_fail_total");
+ _submit_latency_metric =
+ std::make_shared<bvar::LatencyRecorder>(prefix,
"async_cache_write_submit_latency_us");
+ _buffer_alloc_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_buffer_alloc_latency_us");
+ _queue_wait_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_wait_latency_us");
+ _queue_lock_wait_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_lock_wait_latency_us");
+ _queue_lock_hold_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_lock_hold_latency_us");
+ _worker_task_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_worker_task_latency_us");
+ _get_or_set_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_get_or_set_latency_us");
+ _append_latency_metric =
+ std::make_shared<bvar::LatencyRecorder>(prefix,
"async_cache_write_append_latency_us");
+ _finalize_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_finalize_latency_us");
+ _skip_downloaded_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_downloaded_total");
+ _skip_downloading_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_downloading_total");
+ _skip_partial_overlap_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_partial_overlap_total");
+ _drop_stale_epoch_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_drop_stale_epoch_total");
+ _skip_deleting_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_deleting_total");
+ _append_fail_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_append_fail_total");
+ _finalize_fail_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_finalize_fail_total");
+ _persisted_blocks_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_persisted_blocks_total");
+ _persisted_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_persisted_bytes_total");
+}
+
+AsyncCacheWriteService::~AsyncCacheWriteService() {
+ shutdown();
+}
+
+Status AsyncCacheWriteService::start() {
+ std::lock_guard resize_lock(_resize_mutex);
+ if (_shutdown_requested.load(std::memory_order_acquire) ||
+ !_accepting.load(std::memory_order_acquire)) {
+ return Status::InternalError("async file cache write service is
shutting down");
+ }
+ if (_started.load(std::memory_order_acquire)) {
+ return Status::OK();
+ }
+
+ const size_t worker_count =
_configured_worker_count.load(std::memory_order_acquire);
+ if (_worker_pool == nullptr) {
+ RETURN_IF_ERROR(
+ ThreadPoolBuilder(fmt::format("AsyncFileCacheWrite-{}",
+ std::hash<std::string>
{}(_cache->get_base_path())))
+ .set_min_threads(0)
+ .set_max_threads(static_cast<int>(worker_count))
+ .set_max_queue_size(128)
+ .build(&_worker_pool));
+ }
+ // A failed earlier start may have left a partial worker set. Reconcile
the owned workers with
+ // the latest configured count before publishing readiness.
+ RETURN_IF_ERROR(_resize_workers_locked(worker_count));
+ // Publish readiness only after every configured worker loop has been
accepted by the pool.
+ _started.store(true, std::memory_order_release);
+ return Status::OK();
+}
+
+bool AsyncCacheWriteService::try_submit(AsyncCacheWriteTask task) {
+ DORIS_CHECK(task.buffer != nullptr);
+ DORIS_CHECK(task.write_size > 0);
+ DORIS_CHECK(task.write_size <= task.buffer->size());
+ DORIS_CHECK(task.write_size <= std::numeric_limits<size_t>::max() -
task.file_offset);
+ const int64_t submit_start_us = MonotonicMicros();
+ Defer record_submit_latency {
+ [&]() { *_submit_latency_metric << (MonotonicMicros() -
submit_start_us); }};
+ _active_submitters.fetch_add(1, std::memory_order_acq_rel);
+ Defer submitter_done {[&]() { _active_submitters.fetch_sub(1,
std::memory_order_acq_rel); }};
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::try_submit:after_register",
&task);
+ if (!_started.load(std::memory_order_acquire) ||
!_accepting.load(std::memory_order_acquire)) {
+ *_rejected_metric << 1;
+ *_reject_not_running_metric << 1;
+ return false;
+ }
+
+ const size_t task_buffer_bytes = task.buffer->size();
+ std::optional<AsyncCacheWriteTask> victim;
+ {
+ TimedQueueLock lock(_queue_mutex, *_queue_lock_wait_latency_metric,
+ *_queue_lock_hold_latency_metric);
+ const auto options = _options.load(std::memory_order_acquire);
+ const size_t max_pending_bytes = options->max_pending_bytes;
+ const size_t pending_bytes =
_pending_bytes.load(std::memory_order_relaxed);
+ if (_task_buffer_size == 0) {
+ _task_buffer_size = task_buffer_bytes;
+ }
+ DORIS_CHECK(task_buffer_bytes == _task_buffer_size);
+
+ if (task_buffer_bytes > max_pending_bytes) {
+ *_rejected_metric << 1;
+ *_reject_backpressure_metric << 1;
+ return false;
+ }
+
+ const bool has_capacity = pending_bytes <= max_pending_bytes -
task_buffer_bytes;
+ if (!has_capacity && _queue.empty()) {
+ *_rejected_metric << 1;
+ *_reject_backpressure_metric << 1;
+ return false;
+ }
+
+ _queue.push_back(std::move(task));
+ if (has_capacity) {
+ _queued_bytes.fetch_add(task_buffer_bytes,
std::memory_order_relaxed);
+ _pending_count.fetch_add(1, std::memory_order_relaxed);
+ _pending_bytes.fetch_add(task_buffer_bytes,
std::memory_order_relaxed);
+ } else {
+ victim.emplace(std::move(_queue.front()));
+ _queue.pop_front();
+ }
+ }
+
+ *_submitted_metric << 1;
+ *_submitted_bytes_metric << task_buffer_bytes;
+ _queue_cv.notify_one();
+ if (victim) {
+ _finalize_task(std::move(*victim),
TaskFinalizationReason::EVICTED_OLDEST);
+ }
+ return true;
+}
+
+Status AsyncCacheWriteService::allocate_tracked_buffer(size_t size,
+
AsyncCacheWriteBufferPtr* buffer) {
+ DORIS_CHECK(buffer != nullptr);
+ DORIS_CHECK(size > 0);
+ const int64_t allocation_start_us = MonotonicMicros();
+ Defer record_allocation_latency {
+ [&]() { *_buffer_alloc_latency_metric << (MonotonicMicros() -
allocation_start_us); }};
+ Status injected_status;
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::allocate_tracked_buffer:inject_failure",
+ &injected_status);
+ if (!injected_status.ok()) {
+ *_buffer_alloc_fail_metric << 1;
+ return injected_status;
+ }
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_mem_tracker);
+ Status status = Status::OK();
+ try {
+ *buffer = AsyncCacheWriteBufferPtr(new AsyncCacheWriteBuffer(size,
_mem_tracker));
+ } catch (const std::exception& e) {
+ status = Status::MemoryAllocFailed("allocate async file cache write
buffer failed: {}",
+ e.what());
+ }
+ if (!status.ok()) {
+ *_buffer_alloc_fail_metric << 1;
+ }
+ return status;
+}
+
+void AsyncCacheWriteService::_process_task(AsyncCacheWriteTask task) {
+ Defer finish {[&]() { _finish_active_task(std::move(task)); }};
+
+ const int64_t age_us = MonotonicMicros() - task.submit_ts_us;
+ *_queue_wait_latency_metric << age_us;
+ if (!is_current_write_epoch(task.write_epoch)) {
+ *_drop_stale_epoch_metric << 1;
+ return;
+ }
+
+ const int64_t start_us = MonotonicMicros();
+ Status status = _write_one(task);
+ *_worker_task_latency_metric << (MonotonicMicros() - start_us);
+ if (!status.ok()) {
+ LOG(WARNING) << "Async file cache write failed, cache=" <<
_cache->get_base_path()
+ << ", hash=" << task.cache_hash.to_string() << ",
offset=" << task.file_offset
+ << ", size=" << task.write_size << ", status=" << status;
+ }
+}
+
+bool AsyncCacheWriteService::_try_take_task(AsyncCacheWriteTask* task) {
+ TimedQueueLock lock(_queue_mutex, *_queue_lock_wait_latency_metric,
+ *_queue_lock_hold_latency_metric);
+ if (_queue.empty()) {
+ return false;
+ }
+
+ *task = std::move(_queue.front());
+ _queue.pop_front();
+ const size_t task_buffer_bytes = task->buffer->size();
+ _queued_bytes.fetch_sub(task_buffer_bytes, std::memory_order_relaxed);
+ _active_task_count.fetch_add(1, std::memory_order_relaxed);
+ _active_bytes.fetch_add(task_buffer_bytes, std::memory_order_relaxed);
+ return true;
+}
+
+Status AsyncCacheWriteService::_write_one(const AsyncCacheWriteTask& task) {
+ if (!is_current_write_epoch(task.write_epoch)) {
+ *_drop_stale_epoch_metric << 1;
+ return Status::OK();
+ }
+
+ ReadStatistics dummy_stats;
+ CacheContext context;
+ context.query_id = task.admission_ctx.query_id;
+ context.cache_type = task.admission_ctx.cache_type;
+ context.expiration_time = task.admission_ctx.expiration_time;
+ context.tablet_id = task.admission_ctx.tablet_id;
+ context.is_warmup = task.admission_ctx.is_warmup;
+ context.stats = &dummy_stats;
+ auto holder = [&]() {
+ ScopedActiveCounter active_get_or_set(_active_get_or_set_count);
+ const int64_t start_us = MonotonicMicros();
+ Defer record_latency {
+ [&]() { *_get_or_set_latency_metric << (MonotonicMicros() -
start_us); }};
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::_write_one:before_get_or_set",
&task);
+ auto result =
+ _cache->get_or_set(task.cache_hash, task.file_offset,
task.write_size, context);
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::_write_one:after_get_or_set",
&task);
+ return result;
+ }();
+
+ if (!is_current_write_epoch(task.write_epoch)) {
+ *_drop_stale_epoch_metric << 1;
+ return Status::OK();
+ }
+
+ const size_t task_end = task.file_offset + task.write_size;
+ for (const auto& block : holder.file_blocks) {
+ if (block->range().left < task.file_offset || block->range().right >=
task_end) {
+ *_skip_partial_overlap_metric << 1;
+ continue;
+ }
+ if (!is_current_write_epoch(task.write_epoch)) {
+ *_drop_stale_epoch_metric << 1;
+ return Status::OK();
+ }
+ if (_cache->is_block_deleting(block)) {
+ *_skip_deleting_metric << 1;
+ continue;
+ }
+
+ switch (block->state()) {
+ case FileBlock::State::DOWNLOADED:
+ *_skip_downloaded_metric << 1;
+ continue;
+ case FileBlock::State::DOWNLOADING:
+ *_skip_downloading_metric << 1;
+ continue;
+ case FileBlock::State::SKIP_CACHE:
+ continue;
+ case FileBlock::State::EMPTY:
+ break;
+ }
+
+ if (block->get_or_set_downloader() != FileBlock::get_caller_id()) {
+ *_skip_downloading_metric << 1;
+ continue;
+ }
+ const size_t buffer_offset = block->range().left - task.file_offset;
+ DORIS_CHECK(buffer_offset <= task.write_size);
+ DORIS_CHECK(block->range().size() <= task.write_size - buffer_offset);
+ Status status;
+ {
+ ScopedActiveCounter active_append(_active_append_count);
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::_write_one:before_append",
&task);
+ const int64_t start_us = MonotonicMicros();
+ status = block->append(
+ Slice(task.buffer->data() + buffer_offset,
block->range().size()));
+ *_append_latency_metric << (MonotonicMicros() - start_us);
+ }
+ if (!status.ok()) {
+ *_append_fail_metric << 1;
+ LOG(WARNING) << "Append async file cache block failed, cache="
+ << _cache->get_base_path() << ", hash=" <<
task.cache_hash.to_string()
+ << ", offset=" << block->offset() << ", size=" <<
block->range().size()
+ << ", status=" << status;
+ continue;
+ }
+ {
+ ScopedActiveCounter active_finalize(_active_finalize_count);
+ const int64_t start_us = MonotonicMicros();
+ status = block->finalize();
+ *_finalize_latency_metric << (MonotonicMicros() - start_us);
+ }
+ if (!status.ok()) {
+ *_finalize_fail_metric << 1;
+ LOG(WARNING) << "Finalize async file cache block failed, cache="
+ << _cache->get_base_path() << ", hash=" <<
task.cache_hash.to_string()
+ << ", offset=" << block->offset() << ", size=" <<
block->range().size()
+ << ", status=" << status;
+ continue;
+ }
+ *_persisted_blocks_metric << 1;
+ *_persisted_bytes_metric << block->range().size();
+ }
+ return Status::OK();
+}
+
+void AsyncCacheWriteService::_finish_active_task(AsyncCacheWriteTask task) {
+ const size_t task_buffer_bytes = task.buffer->size();
+ bool became_empty = false;
+ {
+ TimedQueueLock lock(_queue_mutex, *_queue_lock_wait_latency_metric,
+ *_queue_lock_hold_latency_metric);
+ const size_t old_active = _active_task_count.fetch_sub(1,
std::memory_order_relaxed);
+ DCHECK_GT(old_active, 0);
+ _active_bytes.fetch_sub(task_buffer_bytes, std::memory_order_relaxed);
+ const size_t old_pending = _pending_count.fetch_sub(1,
std::memory_order_relaxed);
Review Comment:
[P1] Keep finalizing buffers inside the pending quota. This subtracts the
task before `_finalize_task()` runs the callback that removes its inflight
entry and before the buffer is released. The full-queue path has the same gap:
it transfers a counted slot to the new task, then finalizes the displaced
victim outside the lock, so concurrent producers can each retain an additional
uncounted full-block buffer while the configured limit appears satisfied.
`pending_count` can also reach zero while an inflight entry remains, making the
new regression's pending-only drain race its disk-probe assertion. Account
reserved/finalizing bytes through callback and buffer release, and make drain
users wait on that same completion invariant.
##########
be/src/io/cache/async_cache_write_service.cpp:
##########
@@ -0,0 +1,731 @@
+// 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 "io/cache/async_cache_write_service.h"
+
+#include <algorithm>
+#include <exception>
+#include <limits>
+#include <optional>
+#include <thread>
+#include <type_traits>
+#include <utility>
+
+#include "common/logging.h"
+#include "core/allocator.h"
+#include "cpp/sync_point.h"
+#include "io/cache/block_file_cache.h"
+#include "runtime/thread_context.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
+#include "util/time.h"
+
+namespace doris::io {
+
+using AsyncCacheWriteAllocator = Allocator<false, false, false,
DefaultMemoryAllocator, true>;
+
+namespace {
+
+static_assert(std::is_nothrow_move_constructible_v<AsyncCacheWriteTask>);
+static_assert(std::is_nothrow_move_assignable_v<AsyncCacheWriteTask>);
+
+/// Keep an in-progress phase gauge balanced across every return path.
+class ScopedActiveCounter {
+public:
+ explicit ScopedActiveCounter(std::atomic<size_t>& counter) :
_counter(counter) {
+ _counter.fetch_add(1, std::memory_order_relaxed);
+ }
+
+ ~ScopedActiveCounter() { _counter.fetch_sub(1, std::memory_order_relaxed);
}
+
+private:
+ std::atomic<size_t>& _counter;
+};
+
+/// Acquire the FIFO mutex while measuring only the actual lock wait and
critical-section hold.
+class TimedQueueLock {
+public:
+ TimedQueueLock(std::mutex& mutex, bvar::LatencyRecorder& wait_latency,
+ bvar::LatencyRecorder& hold_latency)
+ : _lock(mutex, std::defer_lock),
+ _wait_latency(wait_latency),
+ _hold_latency(hold_latency) {
+ const int64_t wait_start_us = MonotonicMicros();
+ _lock.lock();
+ _acquired_at_us = MonotonicMicros();
+ _wait_us = _acquired_at_us - wait_start_us;
+ }
+
+ ~TimedQueueLock() {
+ const int64_t hold_us = MonotonicMicros() - _acquired_at_us;
+ _lock.unlock();
+ _wait_latency << _wait_us;
+ _hold_latency << hold_us;
+ }
+
+private:
+ std::unique_lock<std::mutex> _lock;
+ bvar::LatencyRecorder& _wait_latency;
+ bvar::LatencyRecorder& _hold_latency;
+ int64_t _acquired_at_us {0};
+ int64_t _wait_us {0};
+};
+
+} // namespace
+
+Status resolve_async_file_cache_write_max_pending_bytes_per_disk(int64_t
configured_bytes,
+ int64_t
be_mem_limit,
+ size_t*
resolved_bytes) {
+ DORIS_CHECK(resolved_bytes != nullptr);
+ if (configured_bytes > 0) {
+ *resolved_bytes = static_cast<size_t>(configured_bytes);
+ return Status::OK();
+ }
+ if (configured_bytes != -1) {
+ return Status::InvalidArgument(
+ "async file cache write pending byte limit must be positive or
-1");
+ }
+
+ DORIS_CHECK(be_mem_limit > 0);
+ constexpr int64_t kMinimumAutoPendingBytes = 512LL * 1024 * 1024;
+ *resolved_bytes = static_cast<size_t>(std::max(kMinimumAutoPendingBytes,
be_mem_limit / 100));
+ return Status::OK();
+}
+
+class AsyncCacheWriteService::Worker : public
std::enable_shared_from_this<Worker> {
+public:
+ explicit Worker(AsyncCacheWriteService& service) : _service(service) {}
+
+ Status start() {
+ auto self = shared_from_this();
+ return _service._worker_pool->submit_func([self = std::move(self)]() {
self->_run(); });
+ }
+
+ void request_stop() { _stop_requested.store(true,
std::memory_order_release); }
+
+ void wait_until_stopped() { _stopped.wait(); }
+
+private:
+ void _run() {
+ _service._running_worker_count.fetch_add(1, std::memory_order_relaxed);
+ Defer mark_finished {[this]() {
+ const size_t old_running =
+ _service._running_worker_count.fetch_sub(1,
std::memory_order_relaxed);
+ DCHECK_GT(old_running, 0);
+ _stopped.count_down();
+ }};
+
+ while (!_stop_requested.load(std::memory_order_acquire)) {
+ AsyncCacheWriteTask task;
+ if (_service._try_take_task(&task)) {
+ _service._process_task(std::move(task));
+ continue;
+ }
+
+ if (_service._shutdown_requested.load(std::memory_order_acquire) &&
+ _service._pending_count.load(std::memory_order_acquire) == 0) {
+ return;
+ }
+ std::unique_lock lock(_service._queue_mutex);
+ _service._queue_cv.wait(lock, [this]() {
+ const bool shutdown_requested =
+
_service._shutdown_requested.load(std::memory_order_acquire);
+ return !_service._queue.empty() ||
+ _stop_requested.load(std::memory_order_acquire) ||
+ (shutdown_requested &&
+
_service._pending_count.load(std::memory_order_relaxed) == 0);
+ });
+ }
+ }
+
+ AsyncCacheWriteService& _service;
+ std::atomic<bool> _stop_requested {false};
+ CountDownLatch _stopped {1};
+};
+
+AsyncCacheWriteBuffer::AsyncCacheWriteBuffer(size_t size,
+
std::shared_ptr<MemTrackerLimiter> tracker)
+ : _size(size), _tracker(std::move(tracker)) {
+ AsyncCacheWriteAllocator allocator;
+ _data = reinterpret_cast<char*>(allocator.alloc(_size));
+}
+
+AsyncCacheWriteBuffer::~AsyncCacheWriteBuffer() {
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_tracker);
+ AsyncCacheWriteAllocator allocator;
+ allocator.free(_data, _size);
+}
+
+AsyncCacheWriteService::AsyncCacheWriteService(BlockFileCache* cache,
+ AsyncCacheWriteServiceOptions
options)
+ : _cache(cache),
+ _options(std::make_shared<const
AsyncCacheWriteServiceOptions>(options)),
+ _configured_worker_count(options.worker_count) {
+ DORIS_CHECK(_cache != nullptr);
+ DORIS_CHECK(options.worker_count > 0);
+ DORIS_CHECK(options.max_pending_bytes > 0);
+
+ const char* prefix = _cache->get_base_path().c_str();
+ _mem_tracker = MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::CACHE,
+ fmt::format("AsyncFileCacheWrite:{}", _cache->get_base_path()));
+ _pending_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_pending_count",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->pending_count();
+ },
+ this);
+ _pending_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_pending_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->pending_bytes();
+ },
+ this);
+ _queued_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_queue_size",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->queued_count();
+ },
+ this);
+ _queued_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_queued_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->queued_bytes();
+ },
+ this);
+ _active_task_count_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_tasks",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->active_task_count();
+ },
+ this);
+ _active_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->active_bytes();
+ },
+ this);
+ _running_worker_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_running_workers",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->running_worker_count();
+ },
+ this);
+ _configured_worker_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_configured_workers",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_configured_worker_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _max_pending_bytes_metric = std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_max_pending_bytes",
+ [](void* service) {
+ return static_cast<AsyncCacheWriteService*>(service)
+ ->_options.load(std::memory_order_acquire)
+ ->max_pending_bytes;
+ },
+ this);
+ _active_get_or_set_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_get_or_set",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_get_or_set_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _active_append_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_append",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_append_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _active_finalize_count_metric =
std::make_shared<bvar::PassiveStatus<size_t>>(
+ prefix, "async_cache_write_active_finalize",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->_active_finalize_count.load(
+ std::memory_order_relaxed);
+ },
+ this);
+ _buffer_memory_metric = std::make_shared<bvar::PassiveStatus<int64_t>>(
+ prefix, "async_cache_write_buffer_memory_bytes",
+ [](void* service) {
+ return
static_cast<AsyncCacheWriteService*>(service)->buffer_memory_bytes();
+ },
+ this);
+ _submitted_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_submitted_total");
+ _submitted_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_submitted_bytes_total");
+ _finished_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_finished_total");
+ _finished_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_finished_bytes_total");
+ _worker_finished_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_worker_finished_total");
+ _worker_finished_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_worker_finished_bytes_total");
+ _evicted_oldest_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_evicted_oldest_total");
+ _evicted_oldest_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_evicted_oldest_bytes_total");
+ _evicted_oldest_age_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_evicted_oldest_age_us");
+ _rejected_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_rejected_total");
+ _reject_not_running_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_reject_not_running_total");
+ _reject_backpressure_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_reject_backpressure_total");
+ _buffer_alloc_fail_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_buffer_alloc_fail_total");
+ _submit_latency_metric =
+ std::make_shared<bvar::LatencyRecorder>(prefix,
"async_cache_write_submit_latency_us");
+ _buffer_alloc_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_buffer_alloc_latency_us");
+ _queue_wait_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_wait_latency_us");
+ _queue_lock_wait_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_lock_wait_latency_us");
+ _queue_lock_hold_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_queue_lock_hold_latency_us");
+ _worker_task_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_worker_task_latency_us");
+ _get_or_set_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_get_or_set_latency_us");
+ _append_latency_metric =
+ std::make_shared<bvar::LatencyRecorder>(prefix,
"async_cache_write_append_latency_us");
+ _finalize_latency_metric = std::make_shared<bvar::LatencyRecorder>(
+ prefix, "async_cache_write_finalize_latency_us");
+ _skip_downloaded_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_downloaded_total");
+ _skip_downloading_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_downloading_total");
+ _skip_partial_overlap_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_partial_overlap_total");
+ _drop_stale_epoch_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_drop_stale_epoch_total");
+ _skip_deleting_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_skip_deleting_total");
+ _append_fail_metric =
+ std::make_shared<bvar::Adder<uint64_t>>(prefix,
"async_cache_write_append_fail_total");
+ _finalize_fail_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_finalize_fail_total");
+ _persisted_blocks_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_persisted_blocks_total");
+ _persisted_bytes_metric = std::make_shared<bvar::Adder<uint64_t>>(
+ prefix, "async_cache_write_persisted_bytes_total");
+}
+
+AsyncCacheWriteService::~AsyncCacheWriteService() {
+ shutdown();
+}
+
+Status AsyncCacheWriteService::start() {
+ std::lock_guard resize_lock(_resize_mutex);
+ if (_shutdown_requested.load(std::memory_order_acquire) ||
+ !_accepting.load(std::memory_order_acquire)) {
+ return Status::InternalError("async file cache write service is
shutting down");
+ }
+ if (_started.load(std::memory_order_acquire)) {
+ return Status::OK();
+ }
+
+ const size_t worker_count =
_configured_worker_count.load(std::memory_order_acquire);
+ if (_worker_pool == nullptr) {
+ RETURN_IF_ERROR(
+ ThreadPoolBuilder(fmt::format("AsyncFileCacheWrite-{}",
+ std::hash<std::string>
{}(_cache->get_base_path())))
+ .set_min_threads(0)
+ .set_max_threads(static_cast<int>(worker_count))
+ .set_max_queue_size(128)
+ .build(&_worker_pool));
+ }
+ // A failed earlier start may have left a partial worker set. Reconcile
the owned workers with
+ // the latest configured count before publishing readiness.
+ RETURN_IF_ERROR(_resize_workers_locked(worker_count));
+ // Publish readiness only after every configured worker loop has been
accepted by the pool.
+ _started.store(true, std::memory_order_release);
+ return Status::OK();
+}
+
+bool AsyncCacheWriteService::try_submit(AsyncCacheWriteTask task) {
+ DORIS_CHECK(task.buffer != nullptr);
+ DORIS_CHECK(task.write_size > 0);
+ DORIS_CHECK(task.write_size <= task.buffer->size());
+ DORIS_CHECK(task.write_size <= std::numeric_limits<size_t>::max() -
task.file_offset);
+ const int64_t submit_start_us = MonotonicMicros();
+ Defer record_submit_latency {
+ [&]() { *_submit_latency_metric << (MonotonicMicros() -
submit_start_us); }};
+ _active_submitters.fetch_add(1, std::memory_order_acq_rel);
+ Defer submitter_done {[&]() { _active_submitters.fetch_sub(1,
std::memory_order_acq_rel); }};
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::try_submit:after_register",
&task);
+ if (!_started.load(std::memory_order_acquire) ||
!_accepting.load(std::memory_order_acquire)) {
+ *_rejected_metric << 1;
+ *_reject_not_running_metric << 1;
+ return false;
+ }
+
+ const size_t task_buffer_bytes = task.buffer->size();
+ std::optional<AsyncCacheWriteTask> victim;
+ {
+ TimedQueueLock lock(_queue_mutex, *_queue_lock_wait_latency_metric,
+ *_queue_lock_hold_latency_metric);
+ const auto options = _options.load(std::memory_order_acquire);
+ const size_t max_pending_bytes = options->max_pending_bytes;
+ const size_t pending_bytes =
_pending_bytes.load(std::memory_order_relaxed);
+ if (_task_buffer_size == 0) {
+ _task_buffer_size = task_buffer_bytes;
+ }
+ DORIS_CHECK(task_buffer_bytes == _task_buffer_size);
+
+ if (task_buffer_bytes > max_pending_bytes) {
+ *_rejected_metric << 1;
+ *_reject_backpressure_metric << 1;
+ return false;
+ }
+
+ const bool has_capacity = pending_bytes <= max_pending_bytes -
task_buffer_bytes;
+ if (!has_capacity && _queue.empty()) {
+ *_rejected_metric << 1;
+ *_reject_backpressure_metric << 1;
+ return false;
+ }
+
+ _queue.push_back(std::move(task));
+ if (has_capacity) {
+ _queued_bytes.fetch_add(task_buffer_bytes,
std::memory_order_relaxed);
+ _pending_count.fetch_add(1, std::memory_order_relaxed);
+ _pending_bytes.fetch_add(task_buffer_bytes,
std::memory_order_relaxed);
+ } else {
+ victim.emplace(std::move(_queue.front()));
+ _queue.pop_front();
+ }
+ }
+
+ *_submitted_metric << 1;
+ *_submitted_bytes_metric << task_buffer_bytes;
+ _queue_cv.notify_one();
+ if (victim) {
+ _finalize_task(std::move(*victim),
TaskFinalizationReason::EVICTED_OLDEST);
+ }
+ return true;
+}
+
+Status AsyncCacheWriteService::allocate_tracked_buffer(size_t size,
+
AsyncCacheWriteBufferPtr* buffer) {
+ DORIS_CHECK(buffer != nullptr);
+ DORIS_CHECK(size > 0);
+ const int64_t allocation_start_us = MonotonicMicros();
+ Defer record_allocation_latency {
+ [&]() { *_buffer_alloc_latency_metric << (MonotonicMicros() -
allocation_start_us); }};
+ Status injected_status;
+
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteService::allocate_tracked_buffer:inject_failure",
+ &injected_status);
+ if (!injected_status.ok()) {
+ *_buffer_alloc_fail_metric << 1;
+ return injected_status;
+ }
+ SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_mem_tracker);
Review Comment:
[P1] Enable the catchable allocation path here. Switching to `_mem_tracker`
does not set `enable_thread_catch_bad_alloc`; both allocator system-memory and
tracker-limit checks only throw `MEM_ALLOC_FAILED` when that flag is set, and
otherwise log that allocation will continue. Consequently this `try`/non-OK
fallback cannot apply backpressure at Doris's memory limits and a best-effort
cache fill can allocate past them until the OS allocation itself fails. Please
use the standard `RETURN_IF_CATCH_EXCEPTION` boundary around construction
(while preserving the failure metric) and test the real allocator-limit path.
##########
be/src/io/cache/benchmark/async_file_cache_write_microbench.cpp:
##########
@@ -0,0 +1,1099 @@
+// 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 "io/cache/file_cache_common.h"
+
+#if defined(BE_TEST) && defined(BUILD_FILE_CACHE_MICROBENCH_TOOL)
+
+#include <gflags/gflags.h>
+#include <glog/logging.h>
+
+#include <algorithm>
+#include <atomic>
+#include <barrier>
+#include <chrono>
+#include <cmath>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include "cloud/config.h"
+#include "common/config.h"
+#include "common/status.h"
+#include "io/cache/async_cache_write_service.h"
+#include "io/cache/block_file_cache.h"
+#include "io/cache/block_file_cache_factory.h"
+#include "io/cache/cached_remote_file_reader.h"
+#include "io/cache/fs_file_cache_storage.h"
+#include "io/cache/inflight_write_buffer_index.h"
+#include "io/fs/file_reader.h"
+#include "io/fs/path.h"
+#include "runtime/exec_env.h"
+#include "runtime/thread_context.h"
+#include "util/cpu_info.h"
+#include "util/disk_info.h"
+#include "util/mem_info.h"
+#include "util/slice.h"
+#include "util/time.h"
+
+DEFINE_string(benchmark_mode, "all",
+ "Comma-separated benchmark groups: reader, service, index, or
all");
+DEFINE_string(cache_path, "./output/async_file_cache_write_microbench",
+ "Directory used by the real filesystem-backed BlockFileCache");
+DEFINE_uint64(block_size, 1024 * 1024,
+ "File-cache block size and stride used by cold reader misses");
+DEFINE_uint64(request_size, 64 * 1024, "Bytes returned to the caller by each
reader operation");
+DEFINE_uint64(reader_operations, 128, "Cold read operations in each sync/async
reader case");
+DEFINE_uint64(service_task_size, 1024 * 1024, "Payload bytes in each direct
service task");
+DEFINE_uint64(service_operations, 256, "Attempted tasks in each direct service
case");
+DEFINE_uint64(service_key_count, 64, "Logical remote files spread across
direct service tasks");
+DEFINE_uint64(index_operations_per_thread, 100000,
+ "Inflight-index lookups performed by each producer thread");
+DEFINE_uint64(index_key_count, 4096, "Keys used by the representative sharded
index case");
+DEFINE_int32(producer_threads, 16, "Concurrent foreground readers or task
producers");
+DEFINE_int32(reader_workers, 16, "Async write workers used by the reader
comparison");
+DEFINE_string(worker_counts, "1,4,16",
+ "Comma-separated async write worker counts used by service
scaling cases");
+DEFINE_uint64(repetitions, 5, "Measured repetitions of every selected
benchmark case");
+DEFINE_uint64(backpressure_pending_bytes, 64 * 1024 * 1024,
+ "Pending-buffer byte limit used by the saturated service case");
+DEFINE_uint64(queue_sample_interval_us, 50,
+ "Sampling interval for pending, queued, and inflight peak
values");
+DEFINE_uint64(timeout_seconds, 120, "Maximum time to wait for one benchmark
case to drain");
+DEFINE_bool(keep_cache, false, "Keep benchmark cache files after the process
exits");
+
+namespace doris::io {
+namespace {
+
+using Clock = std::chrono::steady_clock;
+using Nanoseconds = std::chrono::nanoseconds;
+
+constexpr size_t kMiB = 1024 * 1024;
+
+/// Compact percentile summary for foreground operation latency.
+struct LatencySummary {
+ double average_us {0};
+ double p50_us {0};
+ double p95_us {0};
+ double p99_us {0};
+ double maximum_us {0};
+};
+
+/// Return one nearest-rank percentile from an already sorted nanosecond
sample set.
+/// @param sorted_ns Ascending latency samples in nanoseconds.
+/// @param percentile Requested percentile in the inclusive range [0, 1].
+double percentile_us(const std::vector<int64_t>& sorted_ns, double percentile)
{
+ DORIS_CHECK(!sorted_ns.empty());
+ DORIS_CHECK(percentile > 0 && percentile <= 1);
+ const size_t index =
+ static_cast<size_t>(std::ceil(percentile *
static_cast<double>(sorted_ns.size()))) - 1;
+ return static_cast<double>(sorted_ns[std::min(index, sorted_ns.size() -
1)]) / 1000.0;
+}
+
+/// Merge per-thread samples and calculate stable latency percentiles.
+/// @param per_thread_ns Independently owned samples, one vector per producer.
+LatencySummary summarize_latencies(const std::vector<std::vector<int64_t>>&
per_thread_ns) {
+ size_t sample_count = 0;
+ for (const auto& samples : per_thread_ns) {
+ sample_count += samples.size();
+ }
+ DORIS_CHECK(sample_count > 0);
+
+ std::vector<int64_t> sorted_ns;
+ sorted_ns.reserve(sample_count);
+ for (const auto& samples : per_thread_ns) {
+ sorted_ns.insert(sorted_ns.end(), samples.begin(), samples.end());
+ }
+ std::sort(sorted_ns.begin(), sorted_ns.end());
+ const int64_t total_ns = std::accumulate(sorted_ns.begin(),
sorted_ns.end(), int64_t {0});
+ return LatencySummary {
+ .average_us =
+ static_cast<double>(total_ns) /
static_cast<double>(sample_count) / 1000.0,
+ .p50_us = percentile_us(sorted_ns, 0.50),
+ .p95_us = percentile_us(sorted_ns, 0.95),
+ .p99_us = percentile_us(sorted_ns, 0.99),
+ .maximum_us = static_cast<double>(sorted_ns.back()) / 1000.0,
+ };
+}
+
+/// Parse a comma-separated positive integer list used for worker scaling.
+/// @param text Raw gflag value such as "1,4,16".
+/// @param values Parsed worker counts in input order.
+Status parse_positive_integer_list(std::string_view text, std::vector<size_t>*
values) {
+ DORIS_CHECK(values != nullptr);
+ values->clear();
+ std::stringstream stream {std::string(text)};
+ std::string token;
+ while (std::getline(stream, token, ',')) {
+ try {
+ const long long value = std::stoll(token);
+ if (value <= 0) {
+ return Status::InvalidArgument("worker count must be positive:
{}", token);
+ }
+ values->push_back(static_cast<size_t>(value));
+ } catch (const std::exception& error) {
+ return Status::InvalidArgument("invalid worker count '{}': {}",
token, error.what());
+ }
+ }
+ if (values->empty()) {
+ return Status::InvalidArgument("worker_counts cannot be empty");
+ }
+ return Status::OK();
+}
+
+/// Split the selected benchmark groups while preserving a small command-line
surface.
+/// @param text Raw mode string.
+std::vector<std::string> parse_modes(std::string_view text) {
+ std::stringstream stream {std::string(text)};
+ std::vector<std::string> modes;
+ std::string mode;
+ while (std::getline(stream, mode, ',')) {
+ if (!mode.empty()) {
+ modes.emplace_back(std::move(mode));
+ }
+ }
+ return modes;
+}
+
+/// Return whether a benchmark group was requested explicitly or through "all".
+/// @param modes Parsed benchmark groups.
+/// @param target Group to test.
+bool mode_enabled(const std::vector<std::string>& modes, std::string_view
target) {
+ return std::find(modes.begin(), modes.end(), "all") != modes.end() ||
+ std::find(modes.begin(), modes.end(), target) != modes.end();
+}
+
+/// Validate sizes and concurrency before allocating cache capacity or
starting threads.
+Status validate_flags(const std::vector<std::string>& modes,
+ const std::vector<size_t>& worker_counts) {
+ if (modes.empty()) {
+ return Status::InvalidArgument("benchmark_mode cannot be empty");
+ }
+ for (const auto& mode : modes) {
+ if (mode != "all" && mode != "reader" && mode != "service" && mode !=
"index") {
+ return Status::InvalidArgument("unsupported benchmark mode: {}",
mode);
+ }
+ }
+ DORIS_CHECK(!worker_counts.empty());
+ if (FLAGS_producer_threads <= 0 || FLAGS_reader_workers <= 0) {
+ return Status::InvalidArgument("producer_threads and reader_workers
must be positive");
+ }
+ if (FLAGS_block_size == 0 || FLAGS_request_size == 0 || FLAGS_request_size
> FLAGS_block_size) {
+ return Status::InvalidArgument("sizes must satisfy 0 < request_size <=
block_size");
+ }
+ if (FLAGS_reader_operations <
static_cast<uint64_t>(FLAGS_producer_threads) ||
+ FLAGS_service_operations <
static_cast<uint64_t>(FLAGS_producer_threads)) {
+ return Status::InvalidArgument(
+ "reader_operations and service_operations must be at least
producer_threads");
+ }
+ if (FLAGS_service_task_size != FLAGS_block_size || FLAGS_service_key_count
== 0 ||
+ FLAGS_index_operations_per_thread == 0 || FLAGS_index_key_count == 0 ||
+ FLAGS_backpressure_pending_bytes == 0 ||
FLAGS_queue_sample_interval_us == 0 ||
+ FLAGS_timeout_seconds == 0 || FLAGS_repetitions == 0) {
+ return Status::InvalidArgument(
+ "operation counts, timeouts, and service_task_size ==
block_size are required");
+ }
+ return Status::OK();
+}
+
+/// Produce deterministic data without adding network or object-store latency
to reader results.
+class SyntheticRemoteFileReader final : public FileReader {
+public:
+ /// @param path Stable logical path used to derive the file-cache hash.
+ /// @param file_size Virtual file length; no payload is allocated for it.
+ /// @param block_size Pattern granularity used to validate copied bytes.
+ SyntheticRemoteFileReader(Path path, size_t file_size, size_t block_size)
+ : _path(std::move(path)), _file_size(file_size),
_block_size(block_size) {}
+
+ Status close() override {
+ _closed.store(true, std::memory_order_release);
+ return Status::OK();
+ }
+
+ const Path& path() const override { return _path; }
+ size_t size() const override { return _file_size; }
+ bool closed() const override { return
_closed.load(std::memory_order_acquire); }
+ int64_t mtime() const override { return 0; }
+
+ /// Return the byte value expected at an aligned benchmark block.
+ /// @param offset File offset inside the virtual source.
+ char expected_byte(size_t offset) const {
+ return static_cast<char>((offset / _block_size) % 251);
+ }
+
+protected:
+ /// Fill the requested span from a deterministic virtual file.
+ Status read_at_impl(size_t offset, Slice result, size_t* bytes_read,
+ const IOContext* io_ctx) override {
+ DORIS_CHECK(bytes_read != nullptr);
+ if (offset > _file_size || result.size > _file_size - offset) {
+ return Status::InvalidArgument("synthetic read [{}, {}) exceeds
file size {}", offset,
+ offset + result.size, _file_size);
+ }
+ size_t copied = 0;
+ while (copied < result.size) {
+ const size_t current_offset = offset + copied;
+ const size_t block_end =
+ std::min(_file_size, (current_offset / _block_size + 1) *
_block_size);
+ const size_t bytes = std::min(result.size - copied, block_end -
current_offset);
+ std::memset(result.data + copied, expected_byte(current_offset),
bytes);
+ copied += bytes;
+ }
+ *bytes_read = result.size;
+ return Status::OK();
+ }
+
+private:
+ Path _path;
+ size_t _file_size;
+ size_t _block_size;
+ std::atomic<bool> _closed {false};
+};
+
+/// Record the first worker-thread error without obscuring the performance hot
path.
+class ConcurrentError {
+public:
+ /// Store the first non-OK status observed by any producer.
+ void set(Status status) {
+ if (status.ok()) {
+ return;
+ }
+ bool expected = false;
+ if (_failed.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
+ std::lock_guard lock(_mutex);
+ _status = std::move(status);
+ }
+ }
+
+ bool failed() const { return _failed.load(std::memory_order_acquire); }
+
+ /// Return the stored error after producer threads have joined.
+ Status status() const {
+ std::lock_guard lock(_mutex);
+ return _status;
+ }
+
+private:
+ std::atomic<bool> _failed {false};
+ mutable std::mutex _mutex;
+ Status _status;
+};
+
+/// Sample public queue gauges so short benchmark cases still report a
meaningful high-water mark.
+class QueuePeakSampler {
+public:
+ /// @param service Service whose pending and queued gauges are sampled.
+ /// @param index Inflight index paired with the service.
+ QueuePeakSampler(AsyncCacheWriteService* service,
InflightWriteBufferIndex* index)
+ : _service(service), _index(index) {
+ DORIS_CHECK(_service != nullptr);
+ DORIS_CHECK(_index != nullptr);
+ }
+
+ /// Start the sampling thread. The sampler does not mutate benchmark state.
+ void start() {
+ _running.store(true, std::memory_order_release);
+ _thread = std::thread([this]() {
+ while (_running.load(std::memory_order_acquire)) {
+ sample();
+ std::this_thread::sleep_for(
+
std::chrono::microseconds(FLAGS_queue_sample_interval_us));
+ }
+ sample();
+ });
+ }
+
+ /// Stop sampling and join before reading peak values.
+ void stop() {
+ _running.store(false, std::memory_order_release);
+ if (_thread.joinable()) {
+ _thread.join();
+ }
+ }
+
+ ~QueuePeakSampler() { stop(); }
+
+ size_t peak_pending() const { return
_peak_pending.load(std::memory_order_relaxed); }
+ size_t peak_queued() const { return
_peak_queued.load(std::memory_order_relaxed); }
+ size_t peak_inflight() const { return
_peak_inflight.load(std::memory_order_relaxed); }
+ size_t peak_buffer_bytes() const { return
_peak_buffer_bytes.load(std::memory_order_relaxed); }
+
+private:
+ /// Capture one internally consistent set of independently sampled public
gauges.
+ void sample() {
+ update_max(&_peak_pending, _service->pending_count());
+ update_max(&_peak_queued, _service->queued_count());
+ update_max(&_peak_inflight, _index->count());
+ const int64_t buffer_memory_bytes = _service->buffer_memory_bytes();
+ DORIS_CHECK(buffer_memory_bytes >= 0);
+ update_max(&_peak_buffer_bytes,
static_cast<size_t>(buffer_memory_bytes));
+ }
+
+ /// Atomically preserve the largest sampled gauge value.
+ static void update_max(std::atomic<size_t>* maximum, size_t value) {
+ size_t current = maximum->load(std::memory_order_relaxed);
+ while (current < value &&
+ !maximum->compare_exchange_weak(current, value,
std::memory_order_relaxed)) {
+ }
+ }
+
+ AsyncCacheWriteService* _service;
+ InflightWriteBufferIndex* _index;
+ std::atomic<bool> _running {false};
+ std::thread _thread;
+ std::atomic<size_t> _peak_pending {0};
+ std::atomic<size_t> _peak_queued {0};
+ std::atomic<size_t> _peak_inflight {0};
+ std::atomic<size_t> _peak_buffer_bytes {0};
+};
+
+/// Poll one asynchronous completion predicate with a bounded failure mode.
+/// @param predicate Returns true after the expected state is reached.
+/// @param description Included in timeout diagnostics.
+template <typename Predicate>
+Status wait_until(Predicate&& predicate, std::string_view description) {
+ const auto deadline = Clock::now() +
std::chrono::seconds(FLAGS_timeout_seconds);
+ while (!predicate()) {
+ if (Clock::now() >= deadline) {
+ return Status::TimedOut("timed out waiting for {}", description);
+ }
+ std::this_thread::sleep_for(std::chrono::microseconds(100));
+ }
+ return Status::OK();
+}
+
+/// Own one real filesystem cache and install its factory into the otherwise
minimal ExecEnv.
+class BenchmarkEnvironment {
+public:
+ BenchmarkEnvironment() = default;
+
+ ~BenchmarkEnvironment() {
+ if (_factory) {
+ ExecEnv::GetInstance()->set_file_cache_factory(nullptr);
+ _factory.reset();
+ }
+ if (_owns_cache_path && !FLAGS_keep_cache) {
+ std::error_code error;
+ std::filesystem::remove_all(FLAGS_cache_path, error);
+ }
+ }
+
+ /// Configure globals, create the cache, and wait for its asynchronous
metadata open.
+ /// @param cache_capacity Bytes reserved for the benchmark's normal queue.
+ Status initialize(size_t cache_capacity) {
+ std::error_code error;
+ const auto cache_path =
+ std::filesystem::absolute(FLAGS_cache_path,
error).lexically_normal();
+ if (error) {
+ return Status::IOError("failed to resolve cache path {}: {}",
FLAGS_cache_path,
+ error.message());
+ }
+ const auto current_path = std::filesystem::current_path(error);
+ if (error) {
+ return Status::IOError("failed to resolve current directory: {}",
error.message());
+ }
+ if (cache_path == cache_path.root_path() || cache_path ==
current_path) {
+ return Status::InvalidArgument("cache path must be a dedicated
subdirectory: {}",
+ cache_path.string());
+ }
+ const bool cache_path_exists = std::filesystem::exists(cache_path,
error);
+ if (error) {
+ return Status::IOError("failed to inspect cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ if (cache_path_exists) {
+ const bool cache_path_is_directory =
std::filesystem::is_directory(cache_path, error);
+ if (error) {
+ return Status::IOError("failed to inspect cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ if (!cache_path_is_directory) {
+ return Status::InvalidArgument("cache path is not a directory:
{}",
+ cache_path.string());
+ }
+ const bool cache_path_is_empty =
std::filesystem::is_empty(cache_path, error);
+ if (error) {
+ return Status::IOError("failed to inspect cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ if (!cache_path_is_empty) {
+ return Status::InvalidArgument(
+ "cache path must not exist or must be an empty
directory: {}",
+ cache_path.string());
+ }
+ } else {
+ std::filesystem::create_directories(cache_path, error);
+ if (error) {
+ return Status::IOError("failed to create cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ }
+ _owns_cache_path = true;
+
+ config::enable_async_file_cache_write = true;
+ config::enable_async_file_cache_write_inflight_write_buffer_index =
true;
+ config::enable_read_cache_file_directly = false;
+ config::enable_cache_read_from_peer = false;
+ config::clear_file_cache = true;
+ config::enable_evict_file_cache_in_advance = false;
+ config::file_cache_enter_disk_resource_limit_mode_percent = 99;
+ config::file_cache_each_block_size =
static_cast<int64_t>(FLAGS_block_size);
+ // Benchmark reads are external-table style with tablet_id=0, so they
do not register
+ // tablet-scoped TTL work. Short intervals only bound cache teardown
latency; production
+ // defaults let the otherwise idle TTL threads sleep for three minutes
before join().
+ config::file_cache_background_ttl_gc_interval_ms = 100;
+ config::file_cache_background_ttl_info_update_interval_ms = 100;
+ config::file_cache_background_tablet_id_flush_interval_ms = 100;
+ config::async_file_cache_write_workers_per_disk = FLAGS_reader_workers;
+ config::async_file_cache_write_max_pending_bytes_per_disk =
static_cast<int64_t>(
+ std::max(FLAGS_reader_operations, FLAGS_service_operations) *
FLAGS_block_size);
+
+ DORIS_CHECK(ExecEnv::GetInstance()->file_cache_factory() == nullptr);
+
ExecEnv::GetInstance()->set_file_cache_open_fd_cache(std::make_unique<FDCache>());
+ _factory = std::make_unique<FileCacheFactory>();
+ ExecEnv::GetInstance()->set_file_cache_factory(_factory.get());
+
+ FileCacheSettings settings;
+ settings.capacity = cache_capacity;
+ settings.max_file_block_size = FLAGS_block_size;
+ settings.max_query_cache_size = 0;
+ const size_t auxiliary_queue_size = FLAGS_block_size;
+ settings.disposable_queue_size = auxiliary_queue_size;
+ settings.disposable_queue_elements = 128;
+ settings.index_queue_size = auxiliary_queue_size;
+ settings.index_queue_elements = 128;
+ settings.ttl_queue_size = auxiliary_queue_size;
+ settings.ttl_queue_elements = 128;
+ settings.query_queue_size = cache_capacity - 3 * auxiliary_queue_size;
+ settings.query_queue_elements = std::max<size_t>(
+ 1024, 2 * std::max(FLAGS_reader_operations,
FLAGS_service_operations));
+
+ RETURN_IF_ERROR(_factory->create_file_cache(FLAGS_cache_path,
settings));
+ _cache = _factory->get_by_path(FLAGS_cache_path);
+ DORIS_CHECK(_cache != nullptr);
+ RETURN_IF_ERROR(wait_until([&]() { return
_cache->get_async_open_success(); },
+ "file cache initialization"));
+ return Status::OK();
+ }
+
+ /// Drain accepted writes, then synchronously invalidate and clear all
cached blocks.
+ Status clear_cache() {
+ RETURN_IF_ERROR(wait_for_idle());
+ std::string clear_result;
+ RETURN_IF_ERROR(_factory->clear_file_caches(true, &clear_result));
+ return wait_for_idle();
+ }
+
+ /// Apply one explicit worker/queue snapshot to the production service.
+ /// @param workers Active background writer count.
+ /// @param max_pending_bytes Maximum accepted buffer-capacity bytes,
including active workers.
+ Status configure_service(size_t workers, size_t max_pending_bytes) {
+ auto options = service()->options();
+ options.worker_count = workers;
+ options.max_pending_bytes = max_pending_bytes;
+ return service()->update_options(options);
+ }
+
+ /// Wait until both queue ownership and reader-visible inflight payloads
are gone.
+ Status wait_for_idle() {
+ return wait_until(
+ [&]() {
+ return service()->pending_count() == 0 &&
service()->queued_count() == 0 &&
+ index()->count() == 0;
+ },
+ "async write queue and inflight index to drain");
+ }
+
+ BlockFileCache* cache() const { return _cache; }
+ AsyncCacheWriteService* service() const { return
_cache->async_write_service(); }
+ InflightWriteBufferIndex* index() const { return
_cache->inflight_write_buffer_index(); }
+
+private:
+ std::unique_ptr<FileCacheFactory> _factory;
+ BlockFileCache* _cache {nullptr};
+ bool _owns_cache_path {false};
+};
+
+/// Verify that a complete range can be resolved from the final cache state.
+/// @param cache Target cache.
+/// @param hash Logical file hash.
+/// @param offset Range offset to verify.
+/// @param size Expected persisted bytes.
+Status verify_cached_range(BlockFileCache* cache, const UInt128Wrapper& hash,
size_t offset,
Review Comment:
[P2] Validate the persisted bytes, not only DOWNLOADED coverage. The direct
producers fill each task with a deterministic `operation % 251` pattern, but
the record drops that expectation and this helper never reads a block. The
reader case likewise validates the foreground remote bytes, then only checks
cache metadata after drain. Buffer-offset, append, or persisted-data corruption
would therefore still produce a successful benchmark sample despite the
README's validation guarantee. Preserve the expected pattern and read each
range back (or force a cache-only second read), rejecting byte mismatches.
##########
be/src/io/cache/benchmark/async_file_cache_write_microbench.cpp:
##########
@@ -0,0 +1,1099 @@
+// 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 "io/cache/file_cache_common.h"
+
+#if defined(BE_TEST) && defined(BUILD_FILE_CACHE_MICROBENCH_TOOL)
+
+#include <gflags/gflags.h>
+#include <glog/logging.h>
+
+#include <algorithm>
+#include <atomic>
+#include <barrier>
+#include <chrono>
+#include <cmath>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include "cloud/config.h"
+#include "common/config.h"
+#include "common/status.h"
+#include "io/cache/async_cache_write_service.h"
+#include "io/cache/block_file_cache.h"
+#include "io/cache/block_file_cache_factory.h"
+#include "io/cache/cached_remote_file_reader.h"
+#include "io/cache/fs_file_cache_storage.h"
+#include "io/cache/inflight_write_buffer_index.h"
+#include "io/fs/file_reader.h"
+#include "io/fs/path.h"
+#include "runtime/exec_env.h"
+#include "runtime/thread_context.h"
+#include "util/cpu_info.h"
+#include "util/disk_info.h"
+#include "util/mem_info.h"
+#include "util/slice.h"
+#include "util/time.h"
+
+DEFINE_string(benchmark_mode, "all",
+ "Comma-separated benchmark groups: reader, service, index, or
all");
+DEFINE_string(cache_path, "./output/async_file_cache_write_microbench",
+ "Directory used by the real filesystem-backed BlockFileCache");
+DEFINE_uint64(block_size, 1024 * 1024,
+ "File-cache block size and stride used by cold reader misses");
+DEFINE_uint64(request_size, 64 * 1024, "Bytes returned to the caller by each
reader operation");
+DEFINE_uint64(reader_operations, 128, "Cold read operations in each sync/async
reader case");
+DEFINE_uint64(service_task_size, 1024 * 1024, "Payload bytes in each direct
service task");
+DEFINE_uint64(service_operations, 256, "Attempted tasks in each direct service
case");
+DEFINE_uint64(service_key_count, 64, "Logical remote files spread across
direct service tasks");
+DEFINE_uint64(index_operations_per_thread, 100000,
+ "Inflight-index lookups performed by each producer thread");
+DEFINE_uint64(index_key_count, 4096, "Keys used by the representative sharded
index case");
+DEFINE_int32(producer_threads, 16, "Concurrent foreground readers or task
producers");
+DEFINE_int32(reader_workers, 16, "Async write workers used by the reader
comparison");
+DEFINE_string(worker_counts, "1,4,16",
+ "Comma-separated async write worker counts used by service
scaling cases");
+DEFINE_uint64(repetitions, 5, "Measured repetitions of every selected
benchmark case");
+DEFINE_uint64(backpressure_pending_bytes, 64 * 1024 * 1024,
+ "Pending-buffer byte limit used by the saturated service case");
+DEFINE_uint64(queue_sample_interval_us, 50,
+ "Sampling interval for pending, queued, and inflight peak
values");
+DEFINE_uint64(timeout_seconds, 120, "Maximum time to wait for one benchmark
case to drain");
+DEFINE_bool(keep_cache, false, "Keep benchmark cache files after the process
exits");
+
+namespace doris::io {
+namespace {
+
+using Clock = std::chrono::steady_clock;
+using Nanoseconds = std::chrono::nanoseconds;
+
+constexpr size_t kMiB = 1024 * 1024;
+
+/// Compact percentile summary for foreground operation latency.
+struct LatencySummary {
+ double average_us {0};
+ double p50_us {0};
+ double p95_us {0};
+ double p99_us {0};
+ double maximum_us {0};
+};
+
+/// Return one nearest-rank percentile from an already sorted nanosecond
sample set.
+/// @param sorted_ns Ascending latency samples in nanoseconds.
+/// @param percentile Requested percentile in the inclusive range [0, 1].
+double percentile_us(const std::vector<int64_t>& sorted_ns, double percentile)
{
+ DORIS_CHECK(!sorted_ns.empty());
+ DORIS_CHECK(percentile > 0 && percentile <= 1);
+ const size_t index =
+ static_cast<size_t>(std::ceil(percentile *
static_cast<double>(sorted_ns.size()))) - 1;
+ return static_cast<double>(sorted_ns[std::min(index, sorted_ns.size() -
1)]) / 1000.0;
+}
+
+/// Merge per-thread samples and calculate stable latency percentiles.
+/// @param per_thread_ns Independently owned samples, one vector per producer.
+LatencySummary summarize_latencies(const std::vector<std::vector<int64_t>>&
per_thread_ns) {
+ size_t sample_count = 0;
+ for (const auto& samples : per_thread_ns) {
+ sample_count += samples.size();
+ }
+ DORIS_CHECK(sample_count > 0);
+
+ std::vector<int64_t> sorted_ns;
+ sorted_ns.reserve(sample_count);
+ for (const auto& samples : per_thread_ns) {
+ sorted_ns.insert(sorted_ns.end(), samples.begin(), samples.end());
+ }
+ std::sort(sorted_ns.begin(), sorted_ns.end());
+ const int64_t total_ns = std::accumulate(sorted_ns.begin(),
sorted_ns.end(), int64_t {0});
+ return LatencySummary {
+ .average_us =
+ static_cast<double>(total_ns) /
static_cast<double>(sample_count) / 1000.0,
+ .p50_us = percentile_us(sorted_ns, 0.50),
+ .p95_us = percentile_us(sorted_ns, 0.95),
+ .p99_us = percentile_us(sorted_ns, 0.99),
+ .maximum_us = static_cast<double>(sorted_ns.back()) / 1000.0,
+ };
+}
+
+/// Parse a comma-separated positive integer list used for worker scaling.
+/// @param text Raw gflag value such as "1,4,16".
+/// @param values Parsed worker counts in input order.
+Status parse_positive_integer_list(std::string_view text, std::vector<size_t>*
values) {
+ DORIS_CHECK(values != nullptr);
+ values->clear();
+ std::stringstream stream {std::string(text)};
+ std::string token;
+ while (std::getline(stream, token, ',')) {
+ try {
+ const long long value = std::stoll(token);
+ if (value <= 0) {
+ return Status::InvalidArgument("worker count must be positive:
{}", token);
+ }
+ values->push_back(static_cast<size_t>(value));
+ } catch (const std::exception& error) {
+ return Status::InvalidArgument("invalid worker count '{}': {}",
token, error.what());
+ }
+ }
+ if (values->empty()) {
+ return Status::InvalidArgument("worker_counts cannot be empty");
+ }
+ return Status::OK();
+}
+
+/// Split the selected benchmark groups while preserving a small command-line
surface.
+/// @param text Raw mode string.
+std::vector<std::string> parse_modes(std::string_view text) {
+ std::stringstream stream {std::string(text)};
+ std::vector<std::string> modes;
+ std::string mode;
+ while (std::getline(stream, mode, ',')) {
+ if (!mode.empty()) {
+ modes.emplace_back(std::move(mode));
+ }
+ }
+ return modes;
+}
+
+/// Return whether a benchmark group was requested explicitly or through "all".
+/// @param modes Parsed benchmark groups.
+/// @param target Group to test.
+bool mode_enabled(const std::vector<std::string>& modes, std::string_view
target) {
+ return std::find(modes.begin(), modes.end(), "all") != modes.end() ||
+ std::find(modes.begin(), modes.end(), target) != modes.end();
+}
+
+/// Validate sizes and concurrency before allocating cache capacity or
starting threads.
+Status validate_flags(const std::vector<std::string>& modes,
+ const std::vector<size_t>& worker_counts) {
+ if (modes.empty()) {
+ return Status::InvalidArgument("benchmark_mode cannot be empty");
+ }
+ for (const auto& mode : modes) {
+ if (mode != "all" && mode != "reader" && mode != "service" && mode !=
"index") {
+ return Status::InvalidArgument("unsupported benchmark mode: {}",
mode);
+ }
+ }
+ DORIS_CHECK(!worker_counts.empty());
+ if (FLAGS_producer_threads <= 0 || FLAGS_reader_workers <= 0) {
+ return Status::InvalidArgument("producer_threads and reader_workers
must be positive");
+ }
+ if (FLAGS_block_size == 0 || FLAGS_request_size == 0 || FLAGS_request_size
> FLAGS_block_size) {
+ return Status::InvalidArgument("sizes must satisfy 0 < request_size <=
block_size");
+ }
+ if (FLAGS_reader_operations <
static_cast<uint64_t>(FLAGS_producer_threads) ||
+ FLAGS_service_operations <
static_cast<uint64_t>(FLAGS_producer_threads)) {
+ return Status::InvalidArgument(
+ "reader_operations and service_operations must be at least
producer_threads");
+ }
+ if (FLAGS_service_task_size != FLAGS_block_size || FLAGS_service_key_count
== 0 ||
+ FLAGS_index_operations_per_thread == 0 || FLAGS_index_key_count == 0 ||
+ FLAGS_backpressure_pending_bytes == 0 ||
FLAGS_queue_sample_interval_us == 0 ||
+ FLAGS_timeout_seconds == 0 || FLAGS_repetitions == 0) {
+ return Status::InvalidArgument(
+ "operation counts, timeouts, and service_task_size ==
block_size are required");
+ }
+ return Status::OK();
+}
+
+/// Produce deterministic data without adding network or object-store latency
to reader results.
+class SyntheticRemoteFileReader final : public FileReader {
+public:
+ /// @param path Stable logical path used to derive the file-cache hash.
+ /// @param file_size Virtual file length; no payload is allocated for it.
+ /// @param block_size Pattern granularity used to validate copied bytes.
+ SyntheticRemoteFileReader(Path path, size_t file_size, size_t block_size)
+ : _path(std::move(path)), _file_size(file_size),
_block_size(block_size) {}
+
+ Status close() override {
+ _closed.store(true, std::memory_order_release);
+ return Status::OK();
+ }
+
+ const Path& path() const override { return _path; }
+ size_t size() const override { return _file_size; }
+ bool closed() const override { return
_closed.load(std::memory_order_acquire); }
+ int64_t mtime() const override { return 0; }
+
+ /// Return the byte value expected at an aligned benchmark block.
+ /// @param offset File offset inside the virtual source.
+ char expected_byte(size_t offset) const {
+ return static_cast<char>((offset / _block_size) % 251);
+ }
+
+protected:
+ /// Fill the requested span from a deterministic virtual file.
+ Status read_at_impl(size_t offset, Slice result, size_t* bytes_read,
+ const IOContext* io_ctx) override {
+ DORIS_CHECK(bytes_read != nullptr);
+ if (offset > _file_size || result.size > _file_size - offset) {
+ return Status::InvalidArgument("synthetic read [{}, {}) exceeds
file size {}", offset,
+ offset + result.size, _file_size);
+ }
+ size_t copied = 0;
+ while (copied < result.size) {
+ const size_t current_offset = offset + copied;
+ const size_t block_end =
+ std::min(_file_size, (current_offset / _block_size + 1) *
_block_size);
+ const size_t bytes = std::min(result.size - copied, block_end -
current_offset);
+ std::memset(result.data + copied, expected_byte(current_offset),
bytes);
+ copied += bytes;
+ }
+ *bytes_read = result.size;
+ return Status::OK();
+ }
+
+private:
+ Path _path;
+ size_t _file_size;
+ size_t _block_size;
+ std::atomic<bool> _closed {false};
+};
+
+/// Record the first worker-thread error without obscuring the performance hot
path.
+class ConcurrentError {
+public:
+ /// Store the first non-OK status observed by any producer.
+ void set(Status status) {
+ if (status.ok()) {
+ return;
+ }
+ bool expected = false;
+ if (_failed.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
+ std::lock_guard lock(_mutex);
+ _status = std::move(status);
+ }
+ }
+
+ bool failed() const { return _failed.load(std::memory_order_acquire); }
+
+ /// Return the stored error after producer threads have joined.
+ Status status() const {
+ std::lock_guard lock(_mutex);
+ return _status;
+ }
+
+private:
+ std::atomic<bool> _failed {false};
+ mutable std::mutex _mutex;
+ Status _status;
+};
+
+/// Sample public queue gauges so short benchmark cases still report a
meaningful high-water mark.
+class QueuePeakSampler {
+public:
+ /// @param service Service whose pending and queued gauges are sampled.
+ /// @param index Inflight index paired with the service.
+ QueuePeakSampler(AsyncCacheWriteService* service,
InflightWriteBufferIndex* index)
+ : _service(service), _index(index) {
+ DORIS_CHECK(_service != nullptr);
+ DORIS_CHECK(_index != nullptr);
+ }
+
+ /// Start the sampling thread. The sampler does not mutate benchmark state.
+ void start() {
+ _running.store(true, std::memory_order_release);
+ _thread = std::thread([this]() {
+ while (_running.load(std::memory_order_acquire)) {
+ sample();
+ std::this_thread::sleep_for(
+
std::chrono::microseconds(FLAGS_queue_sample_interval_us));
+ }
+ sample();
+ });
+ }
+
+ /// Stop sampling and join before reading peak values.
+ void stop() {
+ _running.store(false, std::memory_order_release);
+ if (_thread.joinable()) {
+ _thread.join();
+ }
+ }
+
+ ~QueuePeakSampler() { stop(); }
+
+ size_t peak_pending() const { return
_peak_pending.load(std::memory_order_relaxed); }
+ size_t peak_queued() const { return
_peak_queued.load(std::memory_order_relaxed); }
+ size_t peak_inflight() const { return
_peak_inflight.load(std::memory_order_relaxed); }
+ size_t peak_buffer_bytes() const { return
_peak_buffer_bytes.load(std::memory_order_relaxed); }
+
+private:
+ /// Capture one internally consistent set of independently sampled public
gauges.
+ void sample() {
+ update_max(&_peak_pending, _service->pending_count());
+ update_max(&_peak_queued, _service->queued_count());
+ update_max(&_peak_inflight, _index->count());
+ const int64_t buffer_memory_bytes = _service->buffer_memory_bytes();
+ DORIS_CHECK(buffer_memory_bytes >= 0);
+ update_max(&_peak_buffer_bytes,
static_cast<size_t>(buffer_memory_bytes));
+ }
+
+ /// Atomically preserve the largest sampled gauge value.
+ static void update_max(std::atomic<size_t>* maximum, size_t value) {
+ size_t current = maximum->load(std::memory_order_relaxed);
+ while (current < value &&
+ !maximum->compare_exchange_weak(current, value,
std::memory_order_relaxed)) {
+ }
+ }
+
+ AsyncCacheWriteService* _service;
+ InflightWriteBufferIndex* _index;
+ std::atomic<bool> _running {false};
+ std::thread _thread;
+ std::atomic<size_t> _peak_pending {0};
+ std::atomic<size_t> _peak_queued {0};
+ std::atomic<size_t> _peak_inflight {0};
+ std::atomic<size_t> _peak_buffer_bytes {0};
+};
+
+/// Poll one asynchronous completion predicate with a bounded failure mode.
+/// @param predicate Returns true after the expected state is reached.
+/// @param description Included in timeout diagnostics.
+template <typename Predicate>
+Status wait_until(Predicate&& predicate, std::string_view description) {
+ const auto deadline = Clock::now() +
std::chrono::seconds(FLAGS_timeout_seconds);
+ while (!predicate()) {
+ if (Clock::now() >= deadline) {
+ return Status::TimedOut("timed out waiting for {}", description);
+ }
+ std::this_thread::sleep_for(std::chrono::microseconds(100));
+ }
+ return Status::OK();
+}
+
+/// Own one real filesystem cache and install its factory into the otherwise
minimal ExecEnv.
+class BenchmarkEnvironment {
+public:
+ BenchmarkEnvironment() = default;
+
+ ~BenchmarkEnvironment() {
+ if (_factory) {
+ ExecEnv::GetInstance()->set_file_cache_factory(nullptr);
+ _factory.reset();
+ }
+ if (_owns_cache_path && !FLAGS_keep_cache) {
+ std::error_code error;
+ std::filesystem::remove_all(FLAGS_cache_path, error);
+ }
+ }
+
+ /// Configure globals, create the cache, and wait for its asynchronous
metadata open.
+ /// @param cache_capacity Bytes reserved for the benchmark's normal queue.
+ Status initialize(size_t cache_capacity) {
+ std::error_code error;
+ const auto cache_path =
+ std::filesystem::absolute(FLAGS_cache_path,
error).lexically_normal();
+ if (error) {
+ return Status::IOError("failed to resolve cache path {}: {}",
FLAGS_cache_path,
+ error.message());
+ }
+ const auto current_path = std::filesystem::current_path(error);
+ if (error) {
+ return Status::IOError("failed to resolve current directory: {}",
error.message());
+ }
+ if (cache_path == cache_path.root_path() || cache_path ==
current_path) {
+ return Status::InvalidArgument("cache path must be a dedicated
subdirectory: {}",
+ cache_path.string());
+ }
+ const bool cache_path_exists = std::filesystem::exists(cache_path,
error);
+ if (error) {
+ return Status::IOError("failed to inspect cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ if (cache_path_exists) {
+ const bool cache_path_is_directory =
std::filesystem::is_directory(cache_path, error);
+ if (error) {
+ return Status::IOError("failed to inspect cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ if (!cache_path_is_directory) {
+ return Status::InvalidArgument("cache path is not a directory:
{}",
+ cache_path.string());
+ }
+ const bool cache_path_is_empty =
std::filesystem::is_empty(cache_path, error);
+ if (error) {
+ return Status::IOError("failed to inspect cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ if (!cache_path_is_empty) {
+ return Status::InvalidArgument(
+ "cache path must not exist or must be an empty
directory: {}",
+ cache_path.string());
+ }
+ } else {
+ std::filesystem::create_directories(cache_path, error);
+ if (error) {
+ return Status::IOError("failed to create cache path {}: {}",
cache_path.string(),
+ error.message());
+ }
+ }
+ _owns_cache_path = true;
+
+ config::enable_async_file_cache_write = true;
+ config::enable_async_file_cache_write_inflight_write_buffer_index =
true;
+ config::enable_read_cache_file_directly = false;
+ config::enable_cache_read_from_peer = false;
+ config::clear_file_cache = true;
+ config::enable_evict_file_cache_in_advance = false;
+ config::file_cache_enter_disk_resource_limit_mode_percent = 99;
+ config::file_cache_each_block_size =
static_cast<int64_t>(FLAGS_block_size);
+ // Benchmark reads are external-table style with tablet_id=0, so they
do not register
+ // tablet-scoped TTL work. Short intervals only bound cache teardown
latency; production
+ // defaults let the otherwise idle TTL threads sleep for three minutes
before join().
+ config::file_cache_background_ttl_gc_interval_ms = 100;
+ config::file_cache_background_ttl_info_update_interval_ms = 100;
+ config::file_cache_background_tablet_id_flush_interval_ms = 100;
+ config::async_file_cache_write_workers_per_disk = FLAGS_reader_workers;
+ config::async_file_cache_write_max_pending_bytes_per_disk =
static_cast<int64_t>(
+ std::max(FLAGS_reader_operations, FLAGS_service_operations) *
FLAGS_block_size);
+
+ DORIS_CHECK(ExecEnv::GetInstance()->file_cache_factory() == nullptr);
+
ExecEnv::GetInstance()->set_file_cache_open_fd_cache(std::make_unique<FDCache>());
+ _factory = std::make_unique<FileCacheFactory>();
+ ExecEnv::GetInstance()->set_file_cache_factory(_factory.get());
+
+ FileCacheSettings settings;
+ settings.capacity = cache_capacity;
+ settings.max_file_block_size = FLAGS_block_size;
+ settings.max_query_cache_size = 0;
+ const size_t auxiliary_queue_size = FLAGS_block_size;
+ settings.disposable_queue_size = auxiliary_queue_size;
+ settings.disposable_queue_elements = 128;
+ settings.index_queue_size = auxiliary_queue_size;
+ settings.index_queue_elements = 128;
+ settings.ttl_queue_size = auxiliary_queue_size;
+ settings.ttl_queue_elements = 128;
+ settings.query_queue_size = cache_capacity - 3 * auxiliary_queue_size;
+ settings.query_queue_elements = std::max<size_t>(
+ 1024, 2 * std::max(FLAGS_reader_operations,
FLAGS_service_operations));
+
+ RETURN_IF_ERROR(_factory->create_file_cache(FLAGS_cache_path,
settings));
+ _cache = _factory->get_by_path(FLAGS_cache_path);
+ DORIS_CHECK(_cache != nullptr);
+ RETURN_IF_ERROR(wait_until([&]() { return
_cache->get_async_open_success(); },
+ "file cache initialization"));
+ return Status::OK();
+ }
+
+ /// Drain accepted writes, then synchronously invalidate and clear all
cached blocks.
+ Status clear_cache() {
+ RETURN_IF_ERROR(wait_for_idle());
+ std::string clear_result;
+ RETURN_IF_ERROR(_factory->clear_file_caches(true, &clear_result));
+ return wait_for_idle();
+ }
+
+ /// Apply one explicit worker/queue snapshot to the production service.
+ /// @param workers Active background writer count.
+ /// @param max_pending_bytes Maximum accepted buffer-capacity bytes,
including active workers.
+ Status configure_service(size_t workers, size_t max_pending_bytes) {
+ auto options = service()->options();
+ options.worker_count = workers;
+ options.max_pending_bytes = max_pending_bytes;
+ return service()->update_options(options);
+ }
+
+ /// Wait until both queue ownership and reader-visible inflight payloads
are gone.
+ Status wait_for_idle() {
+ return wait_until(
+ [&]() {
+ return service()->pending_count() == 0 &&
service()->queued_count() == 0 &&
+ index()->count() == 0;
+ },
+ "async write queue and inflight index to drain");
+ }
+
+ BlockFileCache* cache() const { return _cache; }
+ AsyncCacheWriteService* service() const { return
_cache->async_write_service(); }
+ InflightWriteBufferIndex* index() const { return
_cache->inflight_write_buffer_index(); }
+
+private:
+ std::unique_ptr<FileCacheFactory> _factory;
+ BlockFileCache* _cache {nullptr};
+ bool _owns_cache_path {false};
+};
+
+/// Verify that a complete range can be resolved from the final cache state.
+/// @param cache Target cache.
+/// @param hash Logical file hash.
+/// @param offset Range offset to verify.
+/// @param size Expected persisted bytes.
+Status verify_cached_range(BlockFileCache* cache, const UInt128Wrapper& hash,
size_t offset,
+ size_t size) {
+ DORIS_CHECK(cache != nullptr);
+ ReadStatistics stats;
+ CacheContext context;
+ context.stats = &stats;
+ FileBlocks blocks;
+ bool fully_covered = false;
+ RETURN_IF_ERROR(cache->get_downloaded_blocks_if_fully_covered(hash,
offset, size, context,
+ &blocks,
&fully_covered));
+ if (!fully_covered) {
+ return Status::InternalError<false>("cache range [{}, {}) was not
persisted", offset,
+ offset + size);
+ }
+ return Status::OK();
+}
+
+/// Shared result fields printed for reader and direct-service cases.
+struct AsyncWriteResult {
+ std::string benchmark;
+ std::string variant;
+ size_t producers {0};
+ size_t workers {0};
+ size_t operations {0};
+ size_t accepted {0};
+ size_t rejected {0};
+ size_t evicted {0};
+ size_t persisted {0};
+ size_t bytes_per_operation {0};
+ double foreground_seconds {0};
+ double drain_seconds {0};
+ double total_seconds {0};
+ size_t peak_pending {0};
+ size_t peak_queued {0};
+ size_t peak_inflight {0};
+ size_t peak_buffer_bytes {0};
+ int64_t queue_lock_wait_p99_us {0};
+ int64_t queue_lock_hold_p99_us {0};
+ LatencySummary latency;
+};
+
+/// Print one machine-readable line without hiding the foreground/drain
distinction.
+/// @param result Completed benchmark result.
+/// @param repetition One-based repetition index.
+void print_async_write_result(const AsyncWriteResult& result, size_t
repetition) {
+ const double foreground_ops_per_sec =
+ static_cast<double>(result.operations) / result.foreground_seconds;
+ const double persisted_mib_per_sec =
+ result.total_seconds > 0
+ ? static_cast<double>(result.persisted *
result.bytes_per_operation) /
+ static_cast<double>(kMiB) / result.total_seconds
+ : 0;
+ std::cout << std::fixed << std::setprecision(3) << "RESULT"
+ << " benchmark=" << result.benchmark << " variant=" <<
result.variant
+ << " repetition=" << repetition << " producers=" <<
result.producers
+ << " workers=" << result.workers << " operations=" <<
result.operations
+ << " accepted=" << result.accepted << " rejected=" <<
result.rejected
+ << " evicted=" << result.evicted << " persisted=" <<
result.persisted
+ << " bytes_per_operation=" << result.bytes_per_operation
+ << " foreground_seconds=" << result.foreground_seconds
+ << " drain_seconds=" << result.drain_seconds
+ << " total_seconds=" << result.total_seconds
+ << " foreground_ops_per_sec=" << foreground_ops_per_sec
+ << " persisted_mib_per_sec=" << persisted_mib_per_sec
+ << " avg_us=" << result.latency.average_us << " p50_us=" <<
result.latency.p50_us
+ << " p95_us=" << result.latency.p95_us << " p99_us=" <<
result.latency.p99_us
+ << " max_us=" << result.latency.maximum_us << " peak_pending="
<< result.peak_pending
+ << " peak_queued=" << result.peak_queued << " peak_inflight=" <<
result.peak_inflight
+ << " peak_buffer_bytes=" << result.peak_buffer_bytes
+ << " queue_lock_wait_p99_us=" << result.queue_lock_wait_p99_us
+ << " queue_lock_hold_p99_us=" << result.queue_lock_hold_p99_us
<< '\n';
+}
+
+/// Compare cold-miss caller latency with synchronous and asynchronous cache
persistence.
+/// @param environment Shared real cache, cleared before the case.
+/// @param mode Explicit write policy applied to every CachedRemoteFileReader.
+/// @param variant Stable output label.
+/// @param repetition One-based repetition index included in output.
+Status run_reader_case(BenchmarkEnvironment* environment, CacheWriteMode mode,
std::string variant,
+ size_t repetition) {
+ DORIS_CHECK(environment != nullptr);
+ RETURN_IF_ERROR(environment->clear_cache());
+ RETURN_IF_ERROR(environment->configure_service(
+ static_cast<size_t>(FLAGS_reader_workers),
+ static_cast<size_t>(FLAGS_reader_operations +
FLAGS_reader_workers) *
+ FLAGS_block_size));
+
+ const size_t producer_count = static_cast<size_t>(FLAGS_producer_threads);
+ const size_t operation_count =
static_cast<size_t>(FLAGS_reader_operations);
+ const size_t file_size = operation_count * FLAGS_block_size;
+ const std::string file_name = "async_write_reader_" + variant + ".bin";
+ const Path path("/synthetic/" + file_name);
+
+ std::vector<std::thread> producers;
+ producers.reserve(producer_count);
+ std::vector<std::vector<int64_t>> latencies(producer_count);
+ std::vector<FileCacheStatistics> statistics(producer_count);
+ std::barrier start_barrier(static_cast<std::ptrdiff_t>(producer_count +
1));
+ ConcurrentError error;
+ QueuePeakSampler sampler(environment->service(), environment->index());
+ sampler.start();
+
+ for (size_t producer = 0; producer < producer_count; ++producer) {
+ producers.emplace_back([&, producer]() {
+ SCOPED_INIT_THREAD_CONTEXT();
+ auto remote =
+ std::make_shared<SyntheticRemoteFileReader>(path,
file_size, FLAGS_block_size);
+ FileReaderOptions options;
+ options.cache_type = FileCachePolicy::FILE_BLOCK_CACHE;
+ options.cache_write_mode = mode;
+ options.cache_base_path = FLAGS_cache_path;
+ options.tablet_id = 0;
+ auto reader = std::make_shared<CachedRemoteFileReader>(remote,
options);
+ IOContext io_context;
+ io_context.file_cache_stats = &statistics[producer];
+ io_context.bypass_peer_read = true;
+ std::string buffer(FLAGS_request_size, '\0');
+ latencies[producer].reserve((operation_count + producer_count - 1)
/ producer_count);
+
+ start_barrier.arrive_and_wait();
+ for (size_t operation = producer; operation < operation_count;
+ operation += producer_count) {
+ if (error.failed()) {
+ break;
+ }
+ const size_t offset = operation * FLAGS_block_size;
+ size_t bytes_read = 0;
+ const auto start = Clock::now();
+ Status status = reader->read_at(offset, Slice(buffer.data(),
buffer.size()),
+ &bytes_read, &io_context);
+ const auto elapsed =
std::chrono::duration_cast<Nanoseconds>(Clock::now() - start);
+ latencies[producer].push_back(elapsed.count());
+ if (!status.ok()) {
+ error.set(std::move(status));
+ break;
+ }
+ if (bytes_read != buffer.size() ||
+ buffer.front() != remote->expected_byte(offset) ||
+ buffer.back() != remote->expected_byte(offset +
buffer.size() - 1)) {
+ error.set(Status::InternalError(
+ "reader result mismatch at operation {},
bytes_read={}", operation,
+ bytes_read));
+ break;
+ }
+ }
+ });
+ }
+
+ const auto foreground_start = Clock::now();
+ start_barrier.arrive_and_wait();
+ for (auto& producer : producers) {
+ producer.join();
+ }
+ const auto foreground_end = Clock::now();
+ if (error.failed()) {
+ sampler.stop();
+ return error.status();
+ }
+
+ RETURN_IF_ERROR(environment->wait_for_idle());
+ const auto drain_end = Clock::now();
+ sampler.stop();
+
+ FileCacheStatistics total_stats;
+ for (const auto& local_stats : statistics) {
+ total_stats.merge_from(local_stats);
+ }
+ const auto hash = BlockFileCache::hash(path.native() + ":0");
+ RETURN_IF_ERROR(verify_cached_range(environment->cache(), hash, 0,
file_size));
+
+ const double foreground_seconds =
+ std::chrono::duration<double>(foreground_end -
foreground_start).count();
+ const double drain_seconds = std::chrono::duration<double>(drain_end -
foreground_end).count();
+ AsyncWriteResult result {
+ .benchmark = "reader",
+ .variant = std::move(variant),
+ .producers = producer_count,
+ .workers = mode == CacheWriteMode::ASYNC_WRITE
+ ? static_cast<size_t>(FLAGS_reader_workers)
+ : 0,
+ .operations = operation_count,
+ .accepted = mode == CacheWriteMode::ASYNC_WRITE
+ ?
static_cast<size_t>(total_stats.async_cache_write_submitted)
+ : 0,
+ .rejected =
static_cast<size_t>(total_stats.async_cache_write_rejected),
+ .persisted = operation_count,
+ .bytes_per_operation = FLAGS_block_size,
+ .foreground_seconds = foreground_seconds,
+ .drain_seconds = drain_seconds,
+ .total_seconds = foreground_seconds + drain_seconds,
+ .peak_pending = sampler.peak_pending(),
+ .peak_queued = sampler.peak_queued(),
+ .peak_inflight = sampler.peak_inflight(),
+ .peak_buffer_bytes = sampler.peak_buffer_bytes(),
+ .latency = summarize_latencies(latencies),
+ };
+ print_async_write_result(result, repetition);
+ return Status::OK();
+}
+
+/// One accepted direct-service task retained for post-timing persistence
verification.
+struct ServiceTaskRecord {
+ UInt128Wrapper hash;
+ size_t offset {0};
+ size_t size {0};
+};
+
+/// Exercise producer admission, inflight publication, locked FIFO
consumption, and persistence.
+/// @param environment Shared real cache, cleared before the case.
+/// @param variant Stable output label.
+/// @param workers Active service consumers.
+/// @param max_pending_bytes Bounded pending-buffer byte limit.
+/// @param repetition One-based repetition index included in output.
+Status run_service_case(BenchmarkEnvironment* environment, std::string
variant, size_t workers,
+ size_t max_pending_bytes, size_t repetition) {
+ DORIS_CHECK(environment != nullptr);
+ RETURN_IF_ERROR(environment->clear_cache());
+ RETURN_IF_ERROR(environment->configure_service(workers,
max_pending_bytes));
+
+ const size_t producer_count = static_cast<size_t>(FLAGS_producer_threads);
+ const size_t operation_count =
static_cast<size_t>(FLAGS_service_operations);
+ std::vector<std::thread> producers;
+ producers.reserve(producer_count);
+ std::vector<std::vector<int64_t>> latencies(producer_count);
+ std::vector<std::vector<ServiceTaskRecord>>
accepted_records(producer_count);
+ std::barrier start_barrier(static_cast<std::ptrdiff_t>(producer_count +
1));
+ std::atomic<size_t> accepted {0};
+ std::atomic<size_t> rejected {0};
+ std::atomic<size_t> completed {0};
+ ConcurrentError error;
+ QueuePeakSampler sampler(environment->service(), environment->index());
+ const uint64_t baseline_evicted =
environment->service()->evicted_oldest_count();
+ sampler.start();
+
+ for (size_t producer = 0; producer < producer_count; ++producer) {
+ producers.emplace_back([&, producer]() {
+ SCOPED_INIT_THREAD_CONTEXT();
+ latencies[producer].reserve((operation_count + producer_count - 1)
/ producer_count);
+ accepted_records[producer].reserve((operation_count +
producer_count - 1) /
+ producer_count);
+ start_barrier.arrive_and_wait();
+
+ for (size_t operation = producer; operation < operation_count;
+ operation += producer_count) {
+ if (error.failed()) {
+ break;
+ }
+ const size_t key_index = operation % FLAGS_service_key_count;
+ const size_t block_index = operation / FLAGS_service_key_count;
+ const auto hash = BlockFileCache::hash("async_write_service_"
+ variant + "_" +
+
std::to_string(key_index));
+ const size_t offset = block_index * FLAGS_block_size;
+ const auto start = Clock::now();
+
+ AsyncCacheWriteBufferPtr buffer;
+ Status status =
environment->service()->allocate_tracked_buffer(
+ FLAGS_service_task_size, &buffer);
+ if (!status.ok()) {
+ error.set(std::move(status));
+ break;
+ }
+ std::memset(buffer->data(), static_cast<int>(operation % 251),
buffer->size());
+ const uint64_t epoch =
environment->service()->current_write_epoch();
+ auto entry = std::make_shared<InflightWriteBufferEntry>(
+ buffer, offset, buffer->size(), MonotonicMicros(),
epoch);
+ auto existing = environment->index()->insert_if_absent(hash,
offset, entry);
+ if (existing != nullptr) {
+ error.set(Status::InternalError(
+ "unexpected duplicate inflight owner at operation
{}", operation));
+ break;
+ }
+
+ AsyncCacheWriteTask task {
+ .cache_hash = hash,
+ .file_offset = offset,
+ .write_size = buffer->size(),
+ .buffer = buffer,
+ .admission_ctx = {},
+ .submit_ts_us = MonotonicMicros(),
+ .write_epoch = epoch,
+ .on_finalized =
+ [index = environment->index(), hash, offset,
entry,
+ &completed](const AsyncCacheWriteTask&) {
+ index->remove_if(hash, offset, entry);
+ completed.fetch_add(1,
std::memory_order_release);
+ },
+ };
+ const bool submitted =
environment->service()->try_submit(std::move(task));
+ const auto elapsed =
std::chrono::duration_cast<Nanoseconds>(Clock::now() - start);
+ latencies[producer].push_back(elapsed.count());
+ if (submitted) {
+ accepted.fetch_add(1, std::memory_order_relaxed);
+ accepted_records[producer].push_back(ServiceTaskRecord {
+ .hash = hash, .offset = offset, .size =
FLAGS_service_task_size});
+ } else {
+ environment->index()->remove_if(hash, offset, entry);
+ environment->index()->record_backpressure_rollback();
+ rejected.fetch_add(1, std::memory_order_relaxed);
+ }
+ }
+ });
+ }
+
+ const auto foreground_start = Clock::now();
+ start_barrier.arrive_and_wait();
+ for (auto& producer : producers) {
+ producer.join();
+ }
+ const auto foreground_end = Clock::now();
+ if (error.failed()) {
+ sampler.stop();
+ return error.status();
+ }
+
+ RETURN_IF_ERROR(wait_until(
+ [&]() {
+ return environment->service()->pending_count() == 0 &&
+ completed.load(std::memory_order_acquire) ==
+ accepted.load(std::memory_order_acquire) &&
+ environment->index()->count() == 0;
+ },
+ "direct service tasks to complete"));
+ const auto drain_end = Clock::now();
+ sampler.stop();
+
+ size_t persisted = 0;
+ for (const auto& records : accepted_records) {
+ for (const auto& record : records) {
+ Status status = verify_cached_range(environment->cache(),
record.hash, record.offset,
+ record.size);
+ if (status.ok()) {
+ ++persisted;
+ }
+ }
+ }
+ const size_t accepted_count = accepted.load(std::memory_order_relaxed);
+ DORIS_CHECK(persisted <= accepted_count);
+ const size_t evicted =
+ static_cast<size_t>(environment->service()->evicted_oldest_count()
- baseline_evicted);
+ if (accepted_count - persisted != evicted) {
+ return Status::InternalError("{} accepted tasks produced {} persisted
and {} evicted",
+ accepted_count, persisted, evicted);
+ }
+
+ const double foreground_seconds =
+ std::chrono::duration<double>(foreground_end -
foreground_start).count();
+ const double drain_seconds = std::chrono::duration<double>(drain_end -
foreground_end).count();
+ AsyncWriteResult result {
+ .benchmark = "service",
+ .variant = std::move(variant),
+ .producers = producer_count,
+ .workers = workers,
+ .operations = operation_count,
+ .accepted = accepted_count,
+ .rejected = rejected.load(std::memory_order_relaxed),
+ .evicted = evicted,
+ .persisted = persisted,
+ .bytes_per_operation = FLAGS_service_task_size,
+ .foreground_seconds = foreground_seconds,
+ .drain_seconds = drain_seconds,
+ .total_seconds = foreground_seconds + drain_seconds,
+ .peak_pending = sampler.peak_pending(),
+ .peak_queued = sampler.peak_queued(),
+ .peak_inflight = sampler.peak_inflight(),
+ .peak_buffer_bytes = sampler.peak_buffer_bytes(),
+ .queue_lock_wait_p99_us =
environment->service()->queue_lock_wait_p99_us(),
Review Comment:
[P2] Report a case-local lock percentile here. `run_benchmarks()` reuses one
service across the reader workload, every worker-count/saturation variant, and
all repetitions, while these `bvar::LatencyRecorder`s are never reset or
baselined. Each RESULT therefore labels a rolling mixture of earlier cases as
the current variant's P99 (and reader cases print zero), so the advertised
worker-count/contention comparison is not interpretable. Recreate/reset the
recorder at an idle case boundary or collect case-owned lock samples.
--
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]