blob: 15d35198236478a4f9a3a3fd677a16e715285c0c [file]
// 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_manager.h"
#include <algorithm>
#include <array>
#include <exception>
#include <limits>
#include <optional>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include "common/exception.h"
#include "common/logging.h"
#include "core/allocator.h"
#include "cpp/sync_point.h"
#include "io/cache/async_cache_write_manager_metrics.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
CacheAdmissionContext CacheAdmissionContext::from_cache_context(const CacheContext& context,
int64_t tablet_id) {
return CacheAdmissionContext {
.query_id = context.query_id,
.cache_type = context.cache_type,
.expiration_time = context.expiration_time,
.tablet_id = tablet_id,
.is_warmup = context.is_warmup,
};
}
CacheContext CacheAdmissionContext::to_cache_context(ReadStatistics* stats) const {
DORIS_CHECK(stats != nullptr);
CacheContext context;
context.query_id = query_id;
context.cache_type = cache_type;
context.expiration_time = expiration_time;
context.tablet_id = tablet_id;
context.is_warmup = is_warmup;
context.stats = stats;
return context;
}
void AsyncCacheWriteTask::validate() const {
DORIS_CHECK(buffer != nullptr);
DORIS_CHECK(write_epoch.key_token != nullptr);
DORIS_CHECK(write_size > 0);
DORIS_CHECK(write_size <= buffer_size());
DORIS_CHECK(write_size <= std::numeric_limits<size_t>::max() - file_offset);
}
size_t AsyncCacheWriteTask::buffer_size() const {
return buffer->size();
}
void AsyncCacheWriteTask::finalize() const {
if (on_finalized) {
on_finalized(*this);
}
}
class AsyncCacheWriteEpochRegistry
: public std::enable_shared_from_this<AsyncCacheWriteEpochRegistry> {
public:
std::shared_ptr<AsyncCacheWriteEpochToken> capture(const UInt128Wrapper& cache_hash) {
auto& shard = _shards[_shard_index(cache_hash)];
const auto find_live_token = [&]() -> std::shared_ptr<AsyncCacheWriteEpochToken> {
auto iterator = shard.tokens.find(cache_hash);
if (iterator == shard.tokens.end()) {
return nullptr;
}
auto token = iterator->second.token.lock();
if (token != nullptr) {
return token;
}
shard.tokens.erase(iterator);
_active_key_count.fetch_sub(1, std::memory_order_relaxed);
return nullptr;
};
{
std::lock_guard lock(shard.mutex);
if (auto token = find_live_token(); token != nullptr) {
return token;
}
}
const uint64_t generation = _next_generation.fetch_add(1, std::memory_order_relaxed);
auto candidate_object = std::unique_ptr<AsyncCacheWriteEpochToken>(
new AsyncCacheWriteEpochToken(cache_hash, generation, weak_from_this()));
TEST_SYNC_POINT("AsyncCacheWriteEpochRegistry::capture:after_candidate_object_created");
auto candidate = std::shared_ptr<AsyncCacheWriteEpochToken>(std::move(candidate_object));
{
std::lock_guard lock(shard.mutex);
if (auto token = find_live_token(); token != nullptr) {
return token;
}
TEST_SYNC_POINT("AsyncCacheWriteEpochRegistry::capture:before_candidate_publish");
shard.tokens.emplace(cache_hash, Entry {.generation = generation, .token = candidate});
_active_key_count.fetch_add(1, std::memory_order_relaxed);
}
return candidate;
}
void invalidate(const UInt128Wrapper& cache_hash) {
// The token destructor calls release(), so its last strong reference must outlive the shard
// lock instead of re-entering the same mutex from inside this critical section.
std::shared_ptr<AsyncCacheWriteEpochToken> token;
{
auto& shard = _shards[_shard_index(cache_hash)];
std::lock_guard lock(shard.mutex);
auto iterator = shard.tokens.find(cache_hash);
if (iterator == shard.tokens.end()) {
return;
}
token = iterator->second.token.lock();
if (token != nullptr) {
token->_valid.store(false, std::memory_order_release);
}
shard.tokens.erase(iterator);
_active_key_count.fetch_sub(1, std::memory_order_relaxed);
}
}
void release(const UInt128Wrapper& cache_hash, uint64_t generation) {
auto& shard = _shards[_shard_index(cache_hash)];
std::lock_guard lock(shard.mutex);
auto iterator = shard.tokens.find(cache_hash);
if (iterator == shard.tokens.end() || iterator->second.generation != generation) {
return;
}
shard.tokens.erase(iterator);
_active_key_count.fetch_sub(1, std::memory_order_relaxed);
}
size_t active_key_count() const { return _active_key_count.load(std::memory_order_relaxed); }
private:
struct Entry {
uint64_t generation {0};
std::weak_ptr<AsyncCacheWriteEpochToken> token;
};
struct Shard {
std::mutex mutex;
std::unordered_map<UInt128Wrapper, Entry, KeyHash> tokens;
};
static constexpr size_t kShardCount = 64;
static size_t _shard_index(const UInt128Wrapper& cache_hash) {
return KeyHash()(cache_hash) % kShardCount;
}
std::array<Shard, kShardCount> _shards;
std::atomic<uint64_t> _next_generation {1};
std::atomic<size_t> _active_key_count {0};
};
AsyncCacheWriteEpochToken::AsyncCacheWriteEpochToken(
const UInt128Wrapper& cache_hash, uint64_t generation,
std::weak_ptr<AsyncCacheWriteEpochRegistry> registry)
: _cache_hash(cache_hash), _generation(generation), _registry(std::move(registry)) {}
AsyncCacheWriteEpochToken::~AsyncCacheWriteEpochToken() {
auto registry = _registry.lock();
if (registry != nullptr) {
registry->release(_cache_hash, _generation);
}
}
Status resolve_async_file_cache_write_max_pending_bytes(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 = 1024LL * 1024 * 1024;
*resolved_bytes = static_cast<size_t>(std::max(kMinimumAutoPendingBytes, be_mem_limit / 100));
return Status::OK();
}
class AsyncCacheWriteManager::Worker : public std::enable_shared_from_this<Worker> {
public:
explicit Worker(AsyncCacheWriteManager& manager) : _manager(manager) {}
Status start() {
auto self = shared_from_this();
return _manager._worker_pool->submit_func([self = std::move(self)]() { self->_run(); });
}
// The caller must hold the manager queue mutex so changing the wait predicate cannot race
// with a worker between evaluating it and blocking on the condition variable.
void request_stop() { _stop_requested.store(true, std::memory_order_release); }
void wait_until_stopped() { _stopped.wait(); }
private:
void _run() {
_manager._running_worker_count.fetch_add(1, std::memory_order_relaxed);
Defer mark_finished {[this]() {
const size_t old_running =
_manager._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 (_manager._try_activate_task(&task)) {
_process_task(std::move(task));
continue;
}
std::unique_lock lock(_manager._queue_mutex);
TEST_SYNC_POINT("AsyncCacheWriteManager::Worker::_run:before_wait");
_manager._queue_cv.wait(lock, [this]() {
return !_manager._queue.empty() || _stop_requested.load(std::memory_order_acquire);
});
}
}
// A task is the worker loop's exception boundary. Its manager-side completion guard releases
// active accounting and owner state while this boundary keeps the long-lived worker alive.
void _process_task(AsyncCacheWriteTask task) {
const UInt128Wrapper cache_hash = task.cache_hash;
const size_t file_offset = task.file_offset;
const size_t write_size = task.write_size;
try {
_manager._process_task(std::move(task));
} catch (const Exception& exception) {
_record_task_exception(cache_hash, file_offset, write_size, exception.what());
} catch (const std::exception& exception) {
_record_task_exception(cache_hash, file_offset, write_size, exception.what());
} catch (...) {
_record_task_exception(cache_hash, file_offset, write_size, "unknown exception");
}
}
void _record_task_exception(const UInt128Wrapper& cache_hash, size_t file_offset,
size_t write_size, const char* message) {
LOG(WARNING) << "Async file cache write task threw an exception, cache="
<< _manager._cache->get_base_path() << ", hash=" << cache_hash.to_string()
<< ", offset=" << file_offset << ", size=" << write_size
<< ", exception=" << message;
}
AsyncCacheWriteManager& _manager;
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);
}
AsyncCacheWriteManager::AsyncCacheWriteManager(BlockFileCache* cache,
AsyncCacheWriteManagerOptions options)
: _cache(cache),
_options(std::make_shared<const AsyncCacheWriteManagerOptions>(options)),
_write_epoch_registry(std::make_shared<AsyncCacheWriteEpochRegistry>()),
_configured_worker_count(options.worker_count) {
DORIS_CHECK(_cache != nullptr);
DORIS_CHECK(options.worker_count > 0);
DORIS_CHECK(options.max_pending_bytes > 0);
_mem_tracker = MemTrackerLimiter::create_shared(
MemTrackerLimiter::Type::CACHE,
fmt::format("AsyncFileCacheWrite:{}", _cache->get_base_path()));
_metrics = std::make_unique<Metrics>(*this, _cache->get_base_path().c_str());
}
AsyncCacheWriteManager::~AsyncCacheWriteManager() {
shutdown();
}
AsyncCacheWriteEpoch AsyncCacheWriteManager::current_write_epoch(const UInt128Wrapper& cache_hash) {
return AsyncCacheWriteEpoch {
.cache_epoch = current_cache_epoch(),
.key_token = _write_epoch_registry->capture(cache_hash),
};
}
bool AsyncCacheWriteManager::is_current_write_epoch(const AsyncCacheWriteEpoch& epoch) const {
DORIS_CHECK(epoch.key_token != nullptr);
return epoch.cache_epoch == current_cache_epoch() && epoch.key_token->is_valid();
}
bool AsyncCacheWriteManager::check_write_epoch(const AsyncCacheWriteEpoch& epoch) {
DORIS_CHECK(epoch.key_token != nullptr);
if (epoch.cache_epoch != current_cache_epoch()) {
_metrics->record_stale_epoch(Metrics::StaleEpochReason::CACHE);
return false;
}
if (!epoch.key_token->is_valid()) {
_metrics->record_stale_epoch(Metrics::StaleEpochReason::KEY);
return false;
}
return true;
}
void AsyncCacheWriteManager::invalidate_pending_writes(const UInt128Wrapper& cache_hash) {
_metrics->record_epoch_invalidation(Metrics::EpochInvalidationScope::KEY);
_write_epoch_registry->invalidate(cache_hash);
}
uint64_t AsyncCacheWriteManager::invalidate_all_pending_writes() {
_metrics->record_epoch_invalidation(Metrics::EpochInvalidationScope::CACHE);
return _cache_epoch.fetch_add(1, std::memory_order_acq_rel) + 1;
}
size_t AsyncCacheWriteManager::active_write_epoch_key_count() const {
return _write_epoch_registry->active_key_count();
}
Status AsyncCacheWriteManager::start() {
std::lock_guard lifecycle_lock(_lifecycle_mutex);
if (!_accepting.load(std::memory_order_acquire)) {
return Status::InternalError("async file cache write manager 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 AsyncCacheWriteManager::try_submit(AsyncCacheWriteTask task) {
task.validate();
const int64_t submit_start_us = MonotonicMicros();
Defer record_submit_latency {
[&]() { _metrics->record_submit_latency(MonotonicMicros() - submit_start_us); }};
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteManager::try_submit:before_register", &task);
const size_t task_buffer_bytes = task.buffer_size();
bool registered = false;
Defer submitter_done {[&]() {
if (registered) {
_active_submitters.fetch_sub(1, std::memory_order_acq_rel);
}
}};
std::optional<AsyncCacheWriteTask> victim;
{
TimedQueueLock lock(_queue_mutex, _metrics->queue_lock_wait_latency(),
_metrics->queue_lock_hold_latency());
if (!_started.load(std::memory_order_acquire) ||
!_accepting.load(std::memory_order_acquire)) {
_metrics->record_task_rejected(Metrics::RejectionReason::NOT_RUNNING);
return false;
}
_active_submitters.fetch_add(1, std::memory_order_acq_rel);
registered = true;
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) {
_metrics->record_task_rejected(Metrics::RejectionReason::BACKPRESSURE);
return false;
}
const bool has_capacity = pending_bytes <= max_pending_bytes - task_buffer_bytes;
if (!has_capacity && _queue.empty()) {
_metrics->record_task_rejected(Metrics::RejectionReason::BACKPRESSURE);
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();
}
}
_metrics->record_task_submitted(task_buffer_bytes);
_queue_cv.notify_one();
if (victim) {
_complete_task(std::move(*victim), TaskFinalizationReason::EVICTED_OLDEST);
}
return true;
}
Status AsyncCacheWriteManager::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 {[&]() {
_metrics->record_buffer_allocation_latency(MonotonicMicros() - allocation_start_us);
}};
Status injected_status;
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteManager::allocate_tracked_buffer:inject_failure",
&injected_status);
if (!injected_status.ok()) {
_metrics->record_buffer_allocation_failure();
return injected_status;
}
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_mem_tracker);
Status status = Status::OK();
try {
ASSIGN_STATUS_IF_CATCH_EXCEPTION(*buffer = AsyncCacheWriteBufferPtr(
new AsyncCacheWriteBuffer(size, _mem_tracker)),
status);
} catch (const std::exception& e) {
status = Status::MemoryAllocFailed("allocate async file cache write buffer failed: {}",
e.what());
}
if (!status.ok()) {
_metrics->record_buffer_allocation_failure();
}
return status;
}
void AsyncCacheWriteManager::_process_task(AsyncCacheWriteTask task) {
Defer complete {[&]() { _complete_active_task(std::move(task)); }};
const int64_t age_us = MonotonicMicros() - task.submit_ts_us;
_metrics->record_queue_wait_latency(age_us);
if (!check_write_epoch(task.write_epoch)) {
return;
}
const int64_t start_us = MonotonicMicros();
Status status = _persist_task(task);
_metrics->record_worker_task_latency(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 AsyncCacheWriteManager::_try_activate_task(AsyncCacheWriteTask* task) {
TimedQueueLock lock(_queue_mutex, _metrics->queue_lock_wait_latency(),
_metrics->queue_lock_hold_latency());
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 AsyncCacheWriteManager::_persist_task(const AsyncCacheWriteTask& task) {
if (!check_write_epoch(task.write_epoch)) {
return Status::OK();
}
ReadStatistics dummy_stats;
CacheContext context = task.admission_ctx.to_cache_context(&dummy_stats);
auto holder = [&]() {
ScopedActiveCounter active_get_or_set(_active_get_or_set_count);
const int64_t start_us = MonotonicMicros();
Defer record_latency {
[&]() { _metrics->record_get_or_set_latency(MonotonicMicros() - start_us); }};
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteManager::_persist_task: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("AsyncCacheWriteManager::_persist_task:after_get_or_set", &task);
return result;
}();
if (!check_write_epoch(task.write_epoch)) {
return Status::OK();
}
const size_t task_end = task.file_offset + task.write_size;
for (const auto& block : holder.file_blocks) {
const auto block_range = block->range();
const bool contained_in_task =
block_range.left >= task.file_offset && block_range.right < task_end;
// A short task is the physical EOF block. Its EMPTY cell may have been preallocated with
// full cache-block capacity, so write only the valid prefix and let finalize() shrink it.
const bool is_preallocated_eof_container = task.write_size < task.buffer_size() &&
block_range.left == task.file_offset &&
block_range.right >= task_end;
if (!contained_in_task && !is_preallocated_eof_container) {
_metrics->record_skipped_block(Metrics::SkippedBlockReason::PARTIAL_OVERLAP);
continue;
}
if (!check_write_epoch(task.write_epoch)) {
return Status::OK();
}
if (_cache->is_block_deleting(block)) {
_metrics->record_skipped_block(Metrics::SkippedBlockReason::DELETING);
continue;
}
switch (block->state()) {
case FileBlock::State::DOWNLOADED:
_metrics->record_skipped_block(Metrics::SkippedBlockReason::DOWNLOADED);
continue;
case FileBlock::State::DOWNLOADING:
_metrics->record_skipped_block(Metrics::SkippedBlockReason::DOWNLOADING);
continue;
case FileBlock::State::SKIP_CACHE:
continue;
case FileBlock::State::EMPTY:
break;
}
if (block->get_or_set_downloader() != FileBlock::get_caller_id()) {
_metrics->record_skipped_block(Metrics::SkippedBlockReason::DOWNLOADING);
continue;
}
const size_t buffer_offset = block_range.left - task.file_offset;
const size_t append_size =
is_preallocated_eof_container ? task.write_size : block_range.size();
DORIS_CHECK(buffer_offset <= task.write_size);
DORIS_CHECK(append_size <= task.write_size - buffer_offset);
Status status;
{
ScopedActiveCounter active_append(_active_append_count);
TEST_SYNC_POINT_CALLBACK("AsyncCacheWriteManager::_persist_task:before_append", &task);
const int64_t start_us = MonotonicMicros();
status = block->append(Slice(task.buffer->data() + buffer_offset, append_size));
_metrics->record_block_operation_latency(Metrics::BlockOperation::APPEND,
MonotonicMicros() - start_us);
}
if (!status.ok()) {
_metrics->record_block_operation_failure(Metrics::BlockOperation::APPEND);
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();
_metrics->record_block_operation_latency(Metrics::BlockOperation::FINALIZE,
MonotonicMicros() - start_us);
}
if (!status.ok()) {
_metrics->record_block_operation_failure(Metrics::BlockOperation::FINALIZE);
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;
}
}
return Status::OK();
}
void AsyncCacheWriteManager::_complete_active_task(AsyncCacheWriteTask task) {
const size_t task_buffer_bytes = task.buffer_size();
bool became_empty = false;
{
TimedQueueLock lock(_queue_mutex, _metrics->queue_lock_wait_latency(),
_metrics->queue_lock_hold_latency());
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);
DCHECK_GT(old_pending, 0);
_pending_bytes.fetch_sub(task_buffer_bytes, std::memory_order_relaxed);
became_empty = old_pending == 1;
}
_complete_task(std::move(task), TaskFinalizationReason::WORKER_FINISHED);
if (became_empty) {
_queue_cv.notify_all();
}
}
void AsyncCacheWriteManager::_complete_task(AsyncCacheWriteTask task,
TaskFinalizationReason reason) {
_metrics->record_task_finalized(task, reason);
task.finalize();
}
Status AsyncCacheWriteManager::resize_workers(size_t worker_count) {
if (worker_count == 0) {
return Status::InvalidArgument("async file cache write worker count must be positive");
}
std::lock_guard lifecycle_lock(_lifecycle_mutex);
if (!_accepting.load(std::memory_order_acquire)) {
return Status::InternalError("async file cache write manager is shutting down");
}
if (!_started.load(std::memory_order_acquire)) {
_configured_worker_count.store(worker_count, std::memory_order_release);
return Status::OK();
}
_configured_worker_count.store(worker_count, std::memory_order_release);
return _resize_workers_locked(worker_count);
}
Status AsyncCacheWriteManager::_resize_workers_locked(size_t worker_count) {
DORIS_CHECK(_worker_pool != nullptr);
if (worker_count < _workers.size()) {
_stop_workers_locked(worker_count);
RETURN_IF_ERROR(_worker_pool->set_min_threads(static_cast<int>(worker_count)));
RETURN_IF_ERROR(_worker_pool->set_max_threads(static_cast<int>(worker_count)));
return Status::OK();
}
RETURN_IF_ERROR(_worker_pool->set_max_threads(static_cast<int>(worker_count)));
// Worker tasks live until resize or shutdown, so reserve one actual pool thread for each task
// before submitting any new Worker. Unlike submit_func(), set_min_threads() propagates an OS
// thread creation failure even when another pool thread is already running; no accepted Worker
// task can therefore remain queued forever without a backing thread.
RETURN_IF_ERROR(_worker_pool->set_min_threads(static_cast<int>(worker_count)));
while (_workers.size() < worker_count) {
auto worker = std::make_shared<Worker>(*this);
RETURN_IF_ERROR(worker->start());
_workers.emplace_back(std::move(worker));
}
return Status::OK();
}
void AsyncCacheWriteManager::_stop_workers_locked(size_t keep_worker_count) {
DORIS_CHECK(keep_worker_count <= _workers.size());
if (keep_worker_count == _workers.size()) {
return;
}
TEST_SYNC_POINT("AsyncCacheWriteManager::_stop_workers_locked:before_request_stop");
{
std::lock_guard queue_lock(_queue_mutex);
for (size_t index = keep_worker_count; index < _workers.size(); ++index) {
_workers[index]->request_stop();
}
TEST_SYNC_POINT("AsyncCacheWriteManager::_stop_workers_locked:after_request_stop");
}
_queue_cv.notify_all();
for (size_t index = keep_worker_count; index < _workers.size(); ++index) {
_workers[index]->wait_until_stopped();
}
_workers.resize(keep_worker_count);
}
Status AsyncCacheWriteManager::update_options(const AsyncCacheWriteManagerOptions& options) {
if (options.worker_count == 0) {
return Status::InvalidArgument("async file cache write worker count must be positive");
}
if (options.max_pending_bytes == 0) {
return Status::InvalidArgument(
"async file cache write pending byte limit must be positive");
}
auto next_options = std::make_shared<const AsyncCacheWriteManagerOptions>(options);
RETURN_IF_ERROR(resize_workers(options.worker_count));
{
TimedQueueLock lock(_queue_mutex, _metrics->queue_lock_wait_latency(),
_metrics->queue_lock_hold_latency());
_options.store(std::move(next_options), std::memory_order_release);
}
return Status::OK();
}
AsyncCacheWriteManagerOptions AsyncCacheWriteManager::options() const {
AsyncCacheWriteManagerOptions result = *_options.load(std::memory_order_acquire);
result.worker_count = _configured_worker_count.load(std::memory_order_acquire);
return result;
}
size_t AsyncCacheWriteManager::queued_count() const {
std::lock_guard lock(_queue_mutex);
return _queue.size();
}
int64_t AsyncCacheWriteManager::queue_lock_wait_p99_us() const {
return _metrics->queue_lock_wait_p99_us();
}
int64_t AsyncCacheWriteManager::queue_lock_hold_p99_us() const {
return _metrics->queue_lock_hold_p99_us();
}
uint64_t AsyncCacheWriteManager::evicted_oldest_count() const {
return _metrics->evicted_oldest_count();
}
void AsyncCacheWriteManager::shutdown() {
std::lock_guard lifecycle_lock(_lifecycle_mutex);
{
std::lock_guard queue_lock(_queue_mutex);
if (!_accepting.exchange(false, std::memory_order_acq_rel)) {
return;
}
}
TEST_SYNC_POINT("AsyncCacheWriteManager::shutdown:after_stop_accepting");
while (_active_submitters.load(std::memory_order_acquire) != 0) {
std::this_thread::yield();
}
if (_worker_pool) {
{
std::unique_lock queue_lock(_queue_mutex);
_queue_cv.wait(queue_lock, [this]() {
return _pending_count.load(std::memory_order_relaxed) == 0;
});
}
_stop_workers_locked(0);
_worker_pool->shutdown();
}
}
} // namespace doris::io