github-actions[bot] commented on code in PR #65658:
URL: https://github.com/apache/doris/pull/65658#discussion_r3802763979
##########
be/src/io/cache/block_file_cache_factory.cpp:
##########
@@ -136,6 +137,44 @@ size_t FileCacheFactory::try_release(const std::string&
base_path) {
return 0;
}
+Status FileCacheFactory::refresh_async_write_options() {
+ std::lock_guard lock(_mtx);
+ return _refresh_async_write_options_locked();
+}
+
+Status FileCacheFactory::_refresh_async_write_options_locked() {
+ if (_caches.empty()) {
+ return Status::OK();
+ }
+
+ size_t total_max_pending_bytes = 0;
+ RETURN_IF_ERROR(resolve_async_file_cache_write_max_pending_bytes(
+ config::async_file_cache_write_max_pending_bytes,
MemInfo::mem_limit(),
+ &total_max_pending_bytes));
+ const size_t max_pending_bytes_per_instance = total_max_pending_bytes /
_caches.size();
+ if (max_pending_bytes_per_instance == 0) {
+ return Status::InvalidArgument(
+ "async file cache write pending byte limit {} is smaller than
{} cache instances",
+ total_max_pending_bytes, _caches.size());
+ }
+ AsyncCacheWriteManagerOptions options {
+ .worker_count =
static_cast<size_t>(config::async_file_cache_write_workers_per_disk),
+ .max_pending_bytes = max_pending_bytes_per_instance,
+ };
+ for (const auto& cache : _caches) {
+ RETURN_IF_ERROR(cache->async_write_manager()->update_options(options));
Review Comment:
[P1] Keep this option generation atomic across cache disks. On a grow from N
to M, an earlier manager can commit M while a later `set_min_threads(M)`
creates only a prefix and returns an error; the failing manager has already
published configured/pool limits M but still owns N Worker handles and old
`_options`, and this return leaves subsequent disks entirely old. Restoring N
through the same API also calls `set_max_threads(N)` before lowering the stale
minimum M and fails. The mutable global is already committed, the callback only
logs, and an equal-value update skips retry, so one transient pthread failure
leaves the disks irreconcilably split. Stage/version the generation with a safe
rollback order or retain a retryable unapplied generation, report success only
after every disk commits, and inject failure on the second manager in a
multi-disk test.
##########
be/src/io/cache/async_cache_write_manager.cpp:
##########
@@ -0,0 +1,779 @@
+// 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)];
+ std::lock_guard lock(shard.mutex);
+ auto iterator = shard.tokens.find(cache_hash);
+ if (iterator != shard.tokens.end()) {
+ 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);
+ }
+
+ const uint64_t generation = _next_generation.fetch_add(1,
std::memory_order_relaxed);
+ auto token = std::shared_ptr<AsyncCacheWriteEpochToken>(
+ new AsyncCacheWriteEpochToken(cache_hash, generation,
weak_from_this()));
+ shard.tokens.emplace(cache_hash, Entry {.generation = generation,
.token = token});
+ _active_key_count.fetch_add(1, std::memory_order_relaxed);
+ return token;
+ }
+
+ 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); }};
+ _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("AsyncCacheWriteManager::try_submit:after_register",
&task);
+ if (!_started.load(std::memory_order_acquire) ||
!_accepting.load(std::memory_order_acquire)) {
+ _metrics->record_task_rejected(Metrics::RejectionReason::NOT_RUNNING);
+ return false;
+ }
+
+ const size_t task_buffer_bytes = task.buffer_size();
+ std::optional<AsyncCacheWriteTask> victim;
+ {
+ TimedQueueLock lock(_queue_mutex, _metrics->queue_lock_wait_latency(),
+ _metrics->queue_lock_hold_latency());
+ 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) {
+ if (block->range().left < task.file_offset || block->range().right >=
task_end) {
Review Comment:
[P2] Persist the accepted short-EOF tail instead of dropping it here. In the
new `preallocated_cache_block_can_cover_the_short_file_tail` case, the one-byte
task's `get_or_set()` returns the retained EMPTY 1 MiB cell with the same left
edge; its right edge is beyond `task_end`, so this branch records
`PARTIAL_OVERLAP` and skips downloader ownership, append, and finalize. The
task is counted as submitted/finished but the last byte remains uncached and a
later read goes remote again (the test checks only submission bytes). Handle
this same-left containing cell as the supported final short block and finalize
its valid byte count, then assert a post-drain cache-only second read.
--
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]