| // 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 <optional> | |
| #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/bonnet/tun_device_packet_exchanger.h" | |
| #include "quiche/quic/qbone/platform/kernel_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, | |
| Visitor* absl_nonnull visitor, 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, intermediate_visitor_.get()) {} | |
| 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, | |
| std::optional<ethhdr> eth_hdr) { | |
| QUICHE_DCHECK(silo_executor_->IsOnSiloThread()); | |
| if (exchanger == nullptr) { | |
| exchanger = this; | |
| } | |
| thread_local_exchanger_.Start(read_fd, write_fd, exchanger, eth_hdr); | |
| // 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 |