blob: 5fdabd082118dc2262b2440f5b3a3d99ccfed4ca [file] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "quiche/quic/qbone/bonnet/async_write_packet_exchanger.h"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <queue>
#include <thread> // NOLINT (for open-sourceable thread ID)
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/base/thread_annotations.h"
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "absl/synchronization/notification.h"
#include "absl/types/span.h"
#include "quiche/quic/platform/api/quic_thread.h"
#include "quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h"
#include "quiche/quic/qbone/platform/kernel_interface.h"
#include "quiche/quic/qbone/platform/netlink_interface.h"
#include "quiche/common/platform/api/quiche_bug_tracker.h"
#include "quiche/common/platform/api/quiche_logging.h"
namespace quic {
class AsyncWritePacketExchanger::WriteThread : public QuicThread {
public:
WriteThread(QboneClientPacketExchanger* absl_nonnull thread_local_exchanger,
AsyncWritePacketExchanger* absl_nonnull async_exchanger)
: QuicThread("AsyncWritePacketExchanger::WriteThread"),
thread_local_exchanger_(*thread_local_exchanger),
async_exchanger_(*async_exchanger) {}
void CompleteWritesAndStop() {
QUICHE_DCHECK(async_exchanger_.silo_executor_->IsOnSiloThread());
{
absl::MutexLock lock(async_exchanger_.write_queue_mutex_);
stop_requested_ = true;
}
Join();
}
bool IsRunningOnThread() const {
return thread_started_.HasBeenNotified() &&
std::this_thread::get_id() == thread_id_;
}
void ProcessWriteResults(absl::StatusOr<std::vector<WriteResult>> results) {
QUICHE_DCHECK(IsRunningOnThread());
if (results.ok()) {
for (const WriteResult& result : *results) {
auto packet_node = in_flight_writes_.extract(result.packet.data());
if (packet_node.empty()) {
// Unexpected. Assuming `thread_local_exchanger_` should always pass
// results to the visitor during the WritePacketToNetwork() call and
// that the result packet should point to the original packet buffer
// stored in `in_flight_writes_`.
QUICHE_BUG(qbone_async_write_packet_exchanger_write_result_not_found);
continue;
}
ready_results_.push(
InternalWriteResult{.packet = std::move(packet_node.mapped()),
.latency = result.latency});
}
} else {
in_flight_error_count_++;
ready_results_.push(results.status());
}
}
protected:
void Run() override {
QUICHE_DCHECK_EQ(thread_id_, std::thread::id());
thread_id_ = std::this_thread::get_id();
thread_started_.Notify();
while (true) {
std::queue<std::vector<std::byte>> packets_to_write =
GetPacketsToWriteCriticalSection();
if (packets_to_write.empty()) {
break;
}
while (!packets_to_write.empty()) {
// Store packet buffers in `in_flight_writes_` during the write. Should
// be unnecessary, assuming `thread_local_exchanger_` always completes
// the write and calls the visitor synchronously, but this allows for
// validating that the received write results are for the expected
// packets.
absl::Span<const std::byte> packet_span = packets_to_write.front();
QUICHE_DCHECK(packet_span.data() != nullptr);
in_flight_writes_[packet_span.data()] =
std::move(packets_to_write.front());
QUICHE_DCHECK_EQ(in_flight_writes_[packet_span.data()].data(),
packet_span.data());
thread_local_exchanger_.WritePacketToNetwork(packet_span);
packets_to_write.pop();
}
// Abandon any packets that failed to write.
QUICHE_BUG_IF(qbone_async_write_packet_exchanger_missing_write_results,
in_flight_writes_.size() != in_flight_error_count_);
in_flight_writes_.clear();
in_flight_error_count_ = 0;
if (!ready_results_.empty()) {
PassResultsCriticalSection();
// Trigger the silo thread to process the results and pass out to the
// visitor. This may trigger unnecessary extra calls if this happens
// multiple times before the silo thread executes the task, but it
// should be safe and have negligible impact on performance, so we won't
// worry about tracking when the call is or isn't necessary.
//
// Use a direct reference to the exchanger because `this` may be stopped
// and destroyed before the lambda is executed. But we assume the
// exchanger itself is safe because it owns the executor and stops its
// execution on shutdown.
AsyncWritePacketExchanger* exchanger = &async_exchanger_;
async_exchanger_.silo_executor_->ExecuteInSilo(
[exchanger]() { exchanger->OnWriteResultsReadyForSiloThread(); });
}
}
{
absl::MutexLock lock(async_exchanger_.write_queue_mutex_);
QUICHE_DCHECK(stop_requested_);
}
}
private:
bool IsQueueNonEmptyOrStopRequested() const
ABSL_SHARED_LOCKS_REQUIRED(async_exchanger_.write_queue_mutex_) {
return !async_exchanger_.write_queue_.empty() || stop_requested_;
}
// May return an empty queue if stop is requested. Otherwise, blocks until the
// queue is non-empty.
std::queue<std::vector<std::byte>> GetPacketsToWriteCriticalSection() {
std::queue<std::vector<std::byte>> packets_to_write;
absl::MutexLock lock(
async_exchanger_.write_queue_mutex_,
absl::Condition(this, &WriteThread::IsQueueNonEmptyOrStopRequested));
if (async_exchanger_.write_queue_.empty()) {
return {};
} else {
packets_to_write.swap(async_exchanger_.write_queue_);
async_exchanger_.write_queue_size_ = 0;
return packets_to_write;
}
}
void PassResultsCriticalSection() {
absl::MutexLock lock(async_exchanger_.result_queue_mutex_);
while (!ready_results_.empty()) {
// For simplicity, only track packet size in the result queue, rather than
// complete size of the result struct. Expect that to be the only part
// with significant memory usage.
int64_t packet_size = ready_results_.front().ok()
? ready_results_.front()->packet.size()
: 0;
int64_t new_queue_size =
async_exchanger_.result_queue_size_ + packet_size;
if (new_queue_size <= async_exchanger_.max_result_buffer_size_bytes_) {
async_exchanger_.result_queue_size_ = new_queue_size;
async_exchanger_.result_queue_.push(std::move(ready_results_.front()));
} else {
// Result passing is even more best-effort than packet writing. If the
// results queue is full, drop the result.
QUICHE_LOG_EVERY_N_SEC(WARNING, 5)
<< "Result queue is full, dropping result.";
}
ready_results_.pop();
}
}
QboneClientPacketExchanger& thread_local_exchanger_;
AsyncWritePacketExchanger& async_exchanger_;
absl::flat_hash_map<const std::byte*, std::vector<std::byte>>
in_flight_writes_;
int in_flight_error_count_ = 0;
std::queue<absl::StatusOr<AsyncWritePacketExchanger::InternalWriteResult>>
ready_results_;
absl::Notification thread_started_;
bool stop_requested_ ABSL_GUARDED_BY(async_exchanger_.write_queue_mutex_) =
false;
// Thread-safe to read after notification of `thread_started_`.
std::thread::id thread_id_;
};
class AsyncWritePacketExchanger::IntermediateVisitor : public Visitor {
public:
IntermediateVisitor(AsyncWritePacketExchanger* absl_nonnull async_exchanger)
: async_exchanger_(*async_exchanger) {}
void OnRead(absl::StatusOr<std::vector<ReadResult>> results) override {
// Expect read to occur on the silo thread, so results can be passed through
// directly.
QUICHE_BUG_IF(qbone_async_write_packet_exchanger_read_result_wrong_thread,
!async_exchanger_.silo_executor_->IsOnSiloThread());
async_exchanger_.visitor_.OnRead(std::move(results));
}
void OnWrite(absl::StatusOr<std::vector<WriteResult>> results) override {
// Expect write to occur on the separate write thread.
QUICHE_BUG_IF(qbone_async_write_packet_exchanger_write_result_wrong_thread,
!async_exchanger_.write_thread_ ||
!async_exchanger_.write_thread_->IsRunningOnThread());
async_exchanger_.write_thread_->ProcessWriteResults(std::move(results));
}
private:
AsyncWritePacketExchanger& async_exchanger_;
};
AsyncWritePacketExchanger::AsyncWritePacketExchanger(
size_t mtu, KernelInterface* absl_nonnull kernel,
NetlinkInterface* absl_nonnull netlink, Visitor* absl_nonnull visitor,
bool is_tap, absl::string_view ifname, int64_t max_buffer_size_bytes,
int64_t max_result_buffer_size_bytes,
absl_nonnull std::unique_ptr<SiloExecutor> silo_executor)
: max_buffer_size_bytes_(max_buffer_size_bytes),
max_result_buffer_size_bytes_(max_result_buffer_size_bytes),
silo_executor_(std::move(silo_executor)),
visitor_(*visitor),
intermediate_visitor_(std::make_unique<IntermediateVisitor>(this)),
thread_local_exchanger_(mtu, kernel, netlink, intermediate_visitor_.get(),
is_tap, ifname) {}
AsyncWritePacketExchanger::~AsyncWritePacketExchanger() {
QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
QUICHE_BUG_IF(qbone_async_write_packet_exchanger_not_stopped,
write_thread_ != nullptr);
Stop();
silo_executor_->Stop();
}
void AsyncWritePacketExchanger::Start(
int read_fd, int write_fd,
QboneClientPacketExchanger* absl_nullable exchanger) {
QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
if (exchanger == nullptr) {
exchanger = this;
}
thread_local_exchanger_.Start(read_fd, write_fd, exchanger);
// Allow idempotent Start() calls (assuming `thread_local_exchanger_`
// validates same inputs).
if (!write_thread_) {
write_thread_ =
std::make_unique<WriteThread>(&thread_local_exchanger_, this);
write_thread_->Start();
}
}
void AsyncWritePacketExchanger::Stop() {
QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
if (write_thread_) {
write_thread_->CompleteWritesAndStop();
}
thread_local_exchanger_.Stop();
// Flush any pending write results to visitor. Along with stopping the write
// thread to ensure no more results are added, this will ensure that if
// `silo_executor_` has any pending calls to run
// OnWriteResultsReadyForSiloThread(), it will safely find an empty results
// queue and not call `visitor_` callbacks.
OnWriteResultsReadyForSiloThread();
write_thread_.reset();
}
int AsyncWritePacketExchanger::OnReadFromNetworkReady(int max_packets_to_read) {
QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
return thread_local_exchanger_.OnReadFromNetworkReady(max_packets_to_read);
}
void AsyncWritePacketExchanger::WritePacketToNetwork(
absl::Span<const std::byte> packet) {
QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
if (packet.empty()) {
QUICHE_BUG(qbone_async_write_packet_exchanger_write_empty_packet);
return;
}
if (!write_thread_) {
QUICHE_BUG(qbone_async_write_packet_exchanger_write_not_started);
return;
}
// This will waste time copying if the write queue is full, but that should be
// an exceptional case. Better to not hold the lock while copying.
std::vector<std::byte> packet_copy(packet.begin(), packet.end());
bool buffer_full = false;
{
absl::MutexLock lock(write_queue_mutex_);
if (write_queue_size_ + packet_copy.size() <= max_buffer_size_bytes_) {
write_queue_size_ += packet_copy.size();
write_queue_.push(std::move(packet_copy));
} else {
buffer_full = true;
}
}
// QBONE packet delivery is best-effort, so if the buffer is full, drop the
// packet. No attempt to wait/retry.
if (buffer_full) {
visitor_.OnWrite(absl::ResourceExhaustedError(
"AsyncWritePacketExchanger::WritePacketToNetwork: buffer full"));
}
}
void AsyncWritePacketExchanger::OnWriteResultsReadyForSiloThread() {
QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
std::vector<absl::StatusOr<InternalWriteResult>> results;
{
absl::MutexLock lock(result_queue_mutex_);
results.reserve(result_queue_.size());
while (!result_queue_.empty()) {
results.push_back(std::move(result_queue_.front()));
result_queue_.pop();
}
result_queue_size_ = 0;
}
std::vector<WriteResult> write_results;
std::vector<absl::Status> errors;
for (const auto& result : results) {
if (result.ok()) {
write_results.push_back(
WriteResult{.packet = result->packet, .latency = result->latency});
} else {
errors.push_back(result.status());
}
}
// Report all successful writes in one batch for efficiency. Errors are less
// common, and we don't optimize for them.
if (!write_results.empty()) {
QUICHE_DCHECK(write_thread_); // Expect no results after stop.
visitor_.OnWrite(std::move(write_results));
}
for (const auto& error : errors) {
QUICHE_DCHECK(write_thread_); // Expect no errors after stop.
visitor_.OnWrite(error);
}
}
} // namespace quic