blob: 483672d71f874aa1f635ab4e24fcdc9efe13eb9f [file]
// 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 "net/socket/delayed_stream_socket.h"
#include <algorithm>
#include <limits>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/socket/bandwidth_throttle.h"
#include "net/socket/stream_socket.h"
namespace net {
namespace {
// Size of the scratch buffer for reading from / writing to the inner socket.
// 32KB is large enough for efficiency, small enough to avoid large transient
// allocations per IO.
constexpr int kInnerReadBufferSize = 32 * 1024;
constexpr int kInnerWriteBufferSize = 32 * 1024;
} // namespace
DelayedStreamSocket::DelayedStreamSocket(
std::unique_ptr<StreamSocket> stream_socket,
const DelayedSocketConfig& config,
scoped_refptr<BandwidthThrottle> download_throttle,
scoped_refptr<BandwidthThrottle> upload_throttle)
: wrapped_socket_(std::move(stream_socket)),
config_(config),
download_throttle_(std::move(download_throttle)),
upload_throttle_(std::move(upload_throttle)),
// BottleneckBuffer models propagation delay only; the shared
// BandwidthThrottle(s) above do the rate limiting. Config throughput
// only sizes buffer capacity (absent == unconstrained == 0 to
// BdpCapacity) and is intentionally separate from the throttle rate.
download_buffer_(
config_.rtt / 2,
BottleneckBuffer::BdpCapacity(
config_.download_throughput_bytes_per_sec.value_or(0),
config_.rtt / 2)),
upload_buffer_(config_.rtt / 2,
BottleneckBuffer::BdpCapacity(
config_.upload_throughput_bytes_per_sec.value_or(0),
config_.rtt / 2)),
inner_read_buffer_(
base::MakeRefCounted<IOBufferWithSize>(kInnerReadBufferSize)),
inner_write_buffer_(
base::MakeRefCounted<IOBufferWithSize>(kInnerWriteBufferSize)) {
// Finite throughput requires a shared throttle; a null throttle implies
// unconstrained throughput (std::nullopt).
CHECK(download_throttle_ ||
!config_.download_throughput_bytes_per_sec.has_value());
CHECK(upload_throttle_ ||
!config_.upload_throughput_bytes_per_sec.has_value());
RebindBufferCallbacks();
}
void DelayedStreamSocket::RebindBufferCallbacks() {
download_buffer_.SetCallbacks(
base::BindRepeating(&DelayedStreamSocket::OnDownloadDataReady,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&DelayedStreamSocket::OnDownloadSpaceAvailable,
weak_factory_.GetWeakPtr()));
upload_buffer_.SetCallbacks(
base::BindRepeating(&DelayedStreamSocket::OnUploadDataReady,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&DelayedStreamSocket::OnUploadSpaceAvailable,
weak_factory_.GetWeakPtr()));
}
DelayedStreamSocket::~DelayedStreamSocket() = default;
// --- Connect ---
int DelayedStreamSocket::Connect(CompletionOnceCallback callback) {
// No re-entrant Connect while a prior Connect is still in flight: the
// previous attempt's callback is on the way to the consumer and would
// be lost. In the passthrough branch below (no configured latency)
// the wrapped socket diagnoses its own re-entry.
CHECK(!connect_pending_);
// Reset the per-session usage bit on reconnect so `WasEverUsed()`
// reflects activity on the current session, matching TCPClientSocket.
if (previously_disconnected_) {
was_ever_used_ = false;
previously_disconnected_ = false;
}
// StreamSocket allows reconnecting after Disconnect(); refresh the buffer
// callbacks so they bind to the *current* WeakPtr (a prior Disconnect()
// invalidates all existing weak pointers, including the buffer's).
RebindBufferCallbacks();
// Sync passthrough when no latency is configured. Skip
// `connect_pending_` here: the wrapped socket owns the pending state
// and IsConnected() delegates to it, so a shape-free wrapper matches
// the wrapped socket's own semantics exactly.
if (!config_.rtt.is_positive()) {
return wrapped_socket_->Connect(std::move(callback));
}
// Shaping path: from here until the consumer's callback fires we are
// pending, so IsConnected()/IsConnectedAndIdle() must report false
// even between wrapped-Connect completion and our latency timer
// firing (during that window wrapped_socket_->IsConnected() may
// already be true but the consumer has not been signalled).
connect_pending_ = true;
// Timestamp Connect() entry so ScheduleConnectCompletion can model a full
// RTT measured from here rather than adding a full RTT on top of however
// long the inner connect took.
const base::TimeTicks connect_started = base::TimeTicks::Now();
auto [for_async, for_sync] = base::SplitOnceCallback(std::move(callback));
int rv = wrapped_socket_->Connect(base::BindOnce(
&DelayedStreamSocket::ScheduleConnectCompletion,
weak_factory_.GetWeakPtr(), std::move(for_async), connect_started));
if (rv == ERR_IO_PENDING) {
return ERR_IO_PENDING;
}
ScheduleConnectCompletion(std::move(for_sync), connect_started, rv);
return ERR_IO_PENDING;
}
void DelayedStreamSocket::ScheduleConnectCompletion(
CompletionOnceCallback callback,
base::TimeTicks connect_started,
int result) {
CHECK(config_.rtt.is_positive());
// Model a full-RTT connect (SYN + SYN-ACK) measured from Connect() entry.
// The inner connect already consumed real time; subtract it so the total
// modelled connect latency is `config_.rtt`, not `config_.rtt`
// plus the inner connect duration. Clamp at zero so an inner connect that
// already exceeded the modelled RTT still completes asynchronously, as
// soon as possible.
const base::TimeDelta delay =
std::max(config_.rtt - (base::TimeTicks::Now() - connect_started),
base::TimeDelta());
connect_timer_.Start(
FROM_HERE, delay,
base::BindOnce(&DelayedStreamSocket::OnConnectTimerFired,
weak_factory_.GetWeakPtr(), std::move(callback), result));
}
void DelayedStreamSocket::OnConnectTimerFired(CompletionOnceCallback callback,
int result) {
connect_pending_ = false;
std::move(callback).Run(result);
}
// --- Read ---
int DelayedStreamSocket::Read(IOBuffer* buffer,
int buffer_len,
CompletionOnceCallback callback) {
CHECK(buffer);
CHECK_GT(buffer_len, 0);
CHECK(!pending_read_callback_);
CHECK(!pending_read_if_ready_callback_);
if (IsReadPassthrough()) {
int rv = wrapped_socket_->Read(
buffer, buffer_len,
base::BindOnce(&DelayedStreamSocket::DidCompletePassthroughIO,
weak_factory_.GetWeakPtr(), std::move(callback)));
if (rv > 0) {
was_ever_used_ = true;
}
return rv;
}
// Fast path: bytes are ready and we either have no shared throttle or a
// previous request already granted bytes that we haven't fully consumed.
if (download_buffer_.has_ready_data() &&
(!download_throttle_ || pending_read_throttle_grant_ > 0)) {
int bytes_read = ReadIntoCaller(buffer, buffer_len);
if (bytes_read > 0) {
ConsumeDownloadGrant(bytes_read);
MaybeStartInnerRead();
was_ever_used_ = true;
return bytes_read;
}
}
if (IsInnerReadDone()) {
return inner_read_error_;
}
// Reject Read on a disconnected socket synchronously (matches
// Socket::Read's ERR_SOCKET_NOT_CONNECTED contract) rather than pending
// and then failing async. Preserve leftover reads: the fast path above
// has already drained any ready bytes, and IsInnerReadDone reported
// false so `download_buffer_` may still hold not-yet-ready bytes we
// should still deliver. Only bail when the buffer is fully empty AND
// the inner has never surfaced an error/EOF yet (so we can't already
// return `inner_read_error_`).
if (!wrapped_socket_->IsConnected() && download_buffer_.empty() &&
!inner_read_eof_) {
return ERR_SOCKET_NOT_CONNECTED;
}
pending_read_buffer_ = buffer;
pending_read_buffer_len_ = buffer_len;
pending_read_callback_ = std::move(callback);
// Either bytes are ready and need a shared-throttle grant, or we need to
// pull more from the inner socket. TryFulfillRead handles both.
TryFulfillRead();
MaybeStartInnerRead();
return ERR_IO_PENDING;
}
int DelayedStreamSocket::ReadIfReady(IOBuffer* buffer,
int buffer_len,
CompletionOnceCallback callback) {
CHECK(buffer);
CHECK_GT(buffer_len, 0);
CHECK(!pending_read_callback_);
CHECK(!pending_read_if_ready_callback_);
if (IsReadPassthrough()) {
int rv = wrapped_socket_->ReadIfReady(
buffer, buffer_len,
base::BindOnce(&DelayedStreamSocket::DidCompletePassthroughIO,
weak_factory_.GetWeakPtr(), std::move(callback)));
if (rv > 0) {
was_ever_used_ = true;
}
return rv;
}
// Fast path: data is ready and either no throttle is required or one has
// already granted bandwidth.
if (download_buffer_.has_ready_data() &&
(!download_throttle_ || pending_read_throttle_grant_ > 0)) {
int bytes_read = ReadIntoCaller(buffer, buffer_len);
if (bytes_read > 0) {
ConsumeDownloadGrant(bytes_read);
MaybeStartInnerRead();
was_ever_used_ = true;
return bytes_read;
}
}
if (IsInnerReadDone()) {
return inner_read_error_;
}
// Reject on disconnected inner socket for the same reasons as Read().
if (!wrapped_socket_->IsConnected() && download_buffer_.empty() &&
!inner_read_eof_) {
return ERR_SOCKET_NOT_CONNECTED;
}
// ReadIfReady never holds the caller's IOBuffer; we just need a callback
// to signal readiness with OK once data is available (and bandwidth is
// granted, if a shared throttle is in use).
pending_read_if_ready_callback_ = std::move(callback);
TryFulfillRead();
MaybeStartInnerRead();
return ERR_IO_PENDING;
}
int DelayedStreamSocket::CancelReadIfReady() {
// In passthrough mode the caller's ReadIfReady went straight to the inner
// socket; we must forward the cancellation or the inner read stays
// pending and eventually fires unexpectedly.
if (IsReadPassthrough()) {
return wrapped_socket_->CancelReadIfReady();
}
pending_read_if_ready_callback_.Reset();
// Cancel any already-posted trampoline so the consumer's callback does
// not fire after this returns (Socket::CancelReadIfReady contract).
pending_read_if_ready_dispatch_.Cancel();
// Drop any leftover throttle grant so a subsequent Read can't absorb
// bytes the cancelled caller already paid for (the tokens have already
// been deducted from the shared throttle, but they were earmarked for
// this cancelled request).
pending_read_throttle_grant_ = 0;
// Cancel any in-flight throttle request so it doesn't keep tokens
// charged against the shared link on behalf of a request the caller
// already cancelled, and can't later grant bytes to a no-longer-pending
// read.
download_throttle_cancellation_ = BandwidthThrottle::CancellationHandle();
download_throttle_pending_ = false;
// We deliberately do not cancel inner reads on the pipeline path: data in
// the buffer is kept for future reads.
return OK;
}
bool DelayedStreamSocket::IsReadPassthrough() const {
// `download_throttle_` is null when download bandwidth is unlimited.
return !config_.rtt.is_positive() && !download_throttle_;
}
bool DelayedStreamSocket::IsInnerReadDone() const {
return download_buffer_.empty() && inner_read_eof_;
}
void DelayedStreamSocket::MaybeStartInnerRead() {
// `!download_throttle_pending_` mirrors the sync loop guard in
// StartInnerRead: while the consumer is waiting for a shared
// download-throttle grant, don't issue another inner Read behind its
// back or read-ahead would proceed uncapped and defeat backpressure.
// Reads resume when OnDownloadThrottleReady clears the flag and calls
// back into MaybeStartInnerRead.
if (!processing_inner_read_ && !inner_read_pending_ && !inner_read_eof_ &&
!download_throttle_pending_ && !download_buffer_.full()) {
StartInnerRead();
}
}
// Copies as many ready bytes as possible from the download buffer into the
// caller's IOBuffer, returning the byte count (the value Read() would
// report). Returns 0 if the front chunk isn't ready yet.
//
// `BottleneckBuffer::Pull` in kStream mode concatenates ready chunks across
// boundaries, so if a shared throttle is in use we MUST cap the destination
// span to `pending_read_throttle_grant_` or we would charge the throttle
// for fewer bytes than we hand to the caller and bypass the link's rate
// limit.
int DelayedStreamSocket::ReadIntoCaller(IOBuffer* buffer, int buffer_len) {
int max_to_read = buffer_len;
if (download_throttle_) {
max_to_read = std::min(max_to_read, pending_read_throttle_grant_);
}
auto dest = buffer->span().first(static_cast<size_t>(max_to_read));
return download_buffer_.Pull(dest);
}
void DelayedStreamSocket::CompleteRead(int result) {
// Move state out cleanly so the callback can never observe stale member
// state. Route the actual invocation through DispatchPendingCompletion
// so:
// * the consumer's callback never fires on a stack that is still
// inside Read() / StartInnerRead() / OnInnerReadComplete(); the
// consumer is allowed to destroy the socket inside the callback;
// * Disconnect()/dtor cancels the pending completion per the Socket
// contract (the trampoline is WeakPtr-bound);
// * the caller's IOBuffer is held alive across the post hop (carried
// as the `keep_alive` parameter).
scoped_refptr<IOBuffer> held_buffer = std::move(pending_read_buffer_);
pending_read_buffer_len_ = 0;
if (result > 0) {
ConsumeDownloadGrant(result);
} else {
pending_read_throttle_grant_ = 0;
}
CompletionOnceCallback callback = std::move(pending_read_callback_);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&DelayedStreamSocket::DispatchPendingCompletion,
weak_factory_.GetWeakPtr(), std::move(held_buffer),
std::move(callback), result));
}
void DelayedStreamSocket::ConsumeDownloadGrant(int bytes_consumed) {
pending_read_throttle_grant_ =
std::max(0, pending_read_throttle_grant_ - bytes_consumed);
}
void DelayedStreamSocket::CompleteReadIfReady(int result) {
// See CompleteRead for the rationale. ReadIfReady never holds the
// caller's IOBuffer, so the trampoline carries a null buffer ref.
//
// The trampoline post is wrapped in a CancelableOnceClosure so that
// CancelReadIfReady() can cancel it even after the callback has been
// moved out of `pending_read_if_ready_callback_`. Without this the
// consumer's callback could still run after CancelReadIfReady() returns.
CompletionOnceCallback callback = std::move(pending_read_if_ready_callback_);
pending_read_if_ready_dispatch_.Reset(base::BindOnce(
&DelayedStreamSocket::DispatchPendingCompletion,
weak_factory_.GetWeakPtr(),
/*keep_alive=*/scoped_refptr<IOBuffer>(), std::move(callback), result));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, pending_read_if_ready_dispatch_.callback());
}
void DelayedStreamSocket::DispatchPendingCompletion(
scoped_refptr<IOBuffer> /*keep_alive*/,
CompletionOnceCallback callback,
int result) {
// `keep_alive` is intentionally unused; the parameter exists solely to
// hold a reference to the consumer's IOBuffer across the posted-task hop
// (per the Socket contract).
//
// Chokepoint for all async Read/Write completions: set the
// WasEverUsed() bit only on a positive byte count, matching
// TCPClientSocket. Read errors, EOF (0), and ReadIfReady's readiness
// signal (OK == 0) do NOT flip the bit.
if (result > 0) {
was_ever_used_ = true;
}
std::move(callback).Run(result);
}
void DelayedStreamSocket::DidCompletePassthroughIO(
CompletionOnceCallback callback,
int result) {
if (result > 0) {
was_ever_used_ = true;
}
std::move(callback).Run(result);
}
void DelayedStreamSocket::StartInnerRead() {
CHECK(!inner_read_pending_);
CHECK(!inner_read_eof_);
base::AutoReset<bool> auto_reset(&processing_inner_read_, true);
while (true) {
// free_space() returns size_t; Socket::Read takes int.
int read_size = base::checked_cast<int>(
std::min<size_t>(kInnerReadBufferSize, download_buffer_.free_space()));
if (read_size == 0) {
return;
}
inner_read_pending_ = true;
int rv = wrapped_socket_->Read(
inner_read_buffer_.get(), read_size,
base::BindOnce(&DelayedStreamSocket::OnInnerReadComplete,
weak_factory_.GetWeakPtr()));
if (rv == ERR_IO_PENDING) {
return; // Will continue in OnInnerReadComplete.
}
ProcessInnerReadResult(rv);
// If the consumer is waiting for the shared throttle, keep buffered
// bytes in place to preserve backpressure. Pending reads that are only
// waiting for latency do not block read-ahead.
if (inner_read_eof_ || download_throttle_pending_) {
return;
}
}
}
void DelayedStreamSocket::OnInnerReadComplete(int result) {
ProcessInnerReadResult(result);
// ProcessInnerReadResult may have synchronously fulfilled a pending
// read and re-entered MaybeStartInnerRead(). Go through
// MaybeStartInnerRead here too so we don't try to overlap two inner
// reads on the same socket.
MaybeStartInnerRead();
}
// Common handling for sync and async inner-read completions. Updates EOF /
// error state, pushes bytes into the buffer, and tries to wake any pending
// reader.
void DelayedStreamSocket::ProcessInnerReadResult(int result) {
inner_read_pending_ = false;
if (result <= 0) {
inner_read_eof_ = true;
inner_read_error_ = result;
if (download_buffer_.empty()) {
TryFulfillRead();
}
return;
}
int accepted = download_buffer_.Push(
inner_read_buffer_->span().first(static_cast<size_t>(result)));
CHECK_EQ(accepted, result);
TryFulfillRead();
}
void DelayedStreamSocket::OnDownloadDataReady() {
TryFulfillRead();
}
void DelayedStreamSocket::OnDownloadSpaceAvailable() {
// Buffer freed space; resume reading from OS socket.
if (!inner_read_pending_ && !inner_read_eof_) {
StartInnerRead();
}
}
// Dispatches to whichever read (if any) is pending. Either:
// - a ReadIfReady waiting only for a readiness signal, or
// - a Read waiting to actually copy bytes into the caller's buffer.
void DelayedStreamSocket::TryFulfillRead() {
if (pending_read_if_ready_callback_) {
TryFulfillReadIfReady();
} else if (pending_read_callback_) {
TryFulfillPendingRead();
}
}
void DelayedStreamSocket::TryFulfillReadIfReady() {
if (download_buffer_.has_ready_data()) {
if (!EnsureDownloadGrant(std::numeric_limits<int>::max())) {
return; // Waiting for shared throttle.
}
// ReadIfReady contract: completion only signals readiness; the caller
// re-enters ReadIfReady to copy bytes from the buffer.
CompleteReadIfReady(OK);
return;
}
if (IsInnerReadDone()) {
CompleteReadIfReady(inner_read_error_);
}
}
void DelayedStreamSocket::TryFulfillPendingRead() {
if (download_buffer_.has_ready_data()) {
if (!EnsureDownloadGrant(pending_read_buffer_len_)) {
return; // Waiting for shared throttle.
}
int bytes_read =
ReadIntoCaller(pending_read_buffer_.get(), pending_read_buffer_len_);
if (bytes_read > 0) {
// CompleteRead posts the consumer's callback, so any subsequent
// member access here is safe even if the callback would have
// destroyed `this` had it run synchronously.
CompleteRead(bytes_read);
MaybeStartInnerRead();
return;
}
}
if (IsInnerReadDone()) {
CompleteRead(inner_read_error_);
}
}
// Returns true if we may consume from the download buffer right now: either
// there is no shared throttle, or a previous request already granted bytes.
// Otherwise issues a throttle request (if not already pending) and returns
// false.
bool DelayedStreamSocket::EnsureDownloadGrant(int max_bytes) {
if (!download_throttle_ || pending_read_throttle_grant_ > 0) {
return true;
}
RequestDownloadThrottle(max_bytes);
return false;
}
void DelayedStreamSocket::RequestDownloadThrottle(int max_bytes) {
if (download_throttle_pending_) {
return;
}
// GetReadyBytesAtFront returns size_t; RequestBytes takes int.
int ready =
base::checked_cast<int>(download_buffer_.GetReadyBytesAtFront(max_bytes));
if (ready == 0) {
return;
}
download_throttle_pending_ = true;
download_throttle_cancellation_ = download_throttle_->RequestBytes(
ready, base::BindOnce(&DelayedStreamSocket::OnDownloadThrottleReady,
weak_factory_.GetWeakPtr(), ready));
}
void DelayedStreamSocket::OnDownloadThrottleReady(int granted_bytes) {
download_throttle_pending_ = false;
pending_read_throttle_grant_ = granted_bytes;
TryFulfillRead();
// Resume read-ahead if it was suppressed by `download_throttle_pending_`
// in MaybeStartInnerRead(). TryFulfillRead()'s success paths call
// MaybeStartInnerRead() for the pending-Read case, but the
// pending-ReadIfReady and no-pending-read cases do not, so a bare
// call here covers the gap without needing to reason about which
// caller branch we came from.
MaybeStartInnerRead();
}
// --- Write ---
int DelayedStreamSocket::Write(
IOBuffer* buffer,
int buffer_len,
CompletionOnceCallback callback,
const NetworkTrafficAnnotationTag& traffic_annotation) {
CHECK(buffer);
CHECK_GT(buffer_len, 0);
CHECK(!pending_write_callback_);
if (!config_.rtt.is_positive() && !upload_throttle_) {
int rv = wrapped_socket_->Write(
buffer, buffer_len,
base::BindOnce(&DelayedStreamSocket::DidCompletePassthroughIO,
weak_factory_.GetWeakPtr(), std::move(callback)),
traffic_annotation);
if (rv > 0) {
was_ever_used_ = true;
}
return rv;
}
// Reject Write on a disconnected inner socket synchronously (matches
// Socket::Write's ERR_SOCKET_NOT_CONNECTED contract). The wrapper has
// no read-buffer analog to preserve, so this is unconditional in the
// shaping path.
if (!wrapped_socket_->IsConnected()) {
return ERR_SOCKET_NOT_CONNECTED;
}
auto data = buffer->span().first(static_cast<size_t>(buffer_len));
int accepted = upload_buffer_.Push(data);
// Queue this Write's annotation alongside the bytes it added to the
// upload buffer. The drain path consumes the queue front-first and
// never crosses an annotation boundary in a single inner Write, so a
// partial-accept-then-second-Write sequence still drains each segment
// under its originating caller's annotation.
if (accepted > 0) {
upload_annotations_.push_back(
{static_cast<size_t>(accepted),
MutableNetworkTrafficAnnotationTag(traffic_annotation)});
}
if (accepted == 0) {
// Buffer full. Stash the IOBuffer and retry when space frees up. Keep
// the annotation so OnUploadSpaceAvailable applies the originating
// Write's annotation when it re-Push()es the stashed bytes (rather
// than whichever annotation happened to be set last).
pending_write_callback_ = std::move(callback);
pending_write_buffer_ = buffer;
pending_write_buffer_len_ = buffer_len;
stashed_write_annotation_ =
MutableNetworkTrafficAnnotationTag(traffic_annotation);
return ERR_IO_PENDING;
}
if (accepted < buffer_len) {
// Partial accept: report what we took. The caller will retry the rest.
MaybeDrainUploadBuffer();
was_ever_used_ = true;
return accepted;
}
// All data accepted. Caller's Write completes only after the buffer drains
// to the underlying socket. Store the pending state before starting the
// drain so HandleInnerWriteResult can see the pending callback (and the
// total accepted byte count) when the chunk finishes draining.
//
// Note: we deliberately do NOT retain `buffer` here even though
// Socket::Write documents that the socket "acquires a reference to the
// provided buffer" while the operation is pending. BottleneckBuffer::Push
// has already copied the bytes into its own storage, so this wrapper
// never re-reads from the caller's IOBuffer after Push returns. Callers
// are still expected to hold their own ref per the contract, but nothing
// in this implementation depends on it. The buffer-full path above DOES
// retain the IOBuffer because OnUploadSpaceAvailable re-Push()es from it.
//
// Leaving `pending_write_buffer_` null here is load-bearing:
// has_stashed_write() (used by HandleInnerWriteResult) keys off it to
// decide whether all supplied bytes are already buffered. Do NOT set
// `pending_write_buffer_ = buffer` here or that signal breaks.
pending_write_callback_ = std::move(callback);
pending_write_buffer_ = nullptr;
pending_write_buffer_len_ = accepted;
MaybeDrainUploadBuffer();
return ERR_IO_PENDING;
}
void DelayedStreamSocket::MaybeDrainUploadBuffer() {
if (inner_write_pending_ || !upload_buffer_.has_ready_data()) {
return;
}
if (upload_throttle_) {
RequestUploadThrottle();
return;
}
// Unlimited upload throughput: pull and write as much as fits. Cap the
// pull at the front annotation entry's bytes_remaining so the inner
// Write never aggregates bytes from two different Write() callers'
// annotations into one inner Write.
CHECK(!upload_annotations_.empty());
size_t max_pull = std::min(static_cast<size_t>(kInnerWriteBufferSize),
upload_annotations_.front().bytes_remaining);
auto dest = inner_write_buffer_->span().first(max_pull);
int pulled = upload_buffer_.Pull(dest);
if (pulled > 0) {
MutableNetworkTrafficAnnotationTag annotation =
ConsumeUploadAnnotationFront(pulled);
IssueInnerWrite(pulled, std::move(annotation));
}
}
void DelayedStreamSocket::RequestUploadThrottle() {
if (upload_throttle_pending_) {
return;
}
// Cap the request at the front annotation entry's bytes_remaining so the
// throttle grant we eventually receive corresponds to bytes covered by
// a single Write() caller's annotation; see MaybeDrainUploadBuffer().
CHECK(!upload_annotations_.empty());
size_t max_ready = std::min(static_cast<size_t>(kInnerWriteBufferSize),
upload_annotations_.front().bytes_remaining);
// GetReadyBytesAtFront returns size_t; RequestBytes takes int.
int ready =
base::checked_cast<int>(upload_buffer_.GetReadyBytesAtFront(max_ready));
if (ready == 0) {
return;
}
upload_throttle_pending_ = true;
upload_throttle_cancellation_ = upload_throttle_->RequestBytes(
ready, base::BindOnce(&DelayedStreamSocket::OnUploadThrottleReady,
weak_factory_.GetWeakPtr(), ready));
}
void DelayedStreamSocket::OnUploadThrottleReady(int granted_bytes) {
upload_throttle_pending_ = false;
auto dest =
inner_write_buffer_->span().first(static_cast<size_t>(granted_bytes));
int pulled = upload_buffer_.Pull(dest);
if (pulled > 0) {
MutableNetworkTrafficAnnotationTag annotation =
ConsumeUploadAnnotationFront(pulled);
IssueInnerWrite(pulled, std::move(annotation));
}
}
MutableNetworkTrafficAnnotationTag
DelayedStreamSocket::ConsumeUploadAnnotationFront(int bytes) {
CHECK_GT(bytes, 0);
CHECK(!upload_annotations_.empty());
PendingUploadAnnotation& front = upload_annotations_.front();
CHECK_GE(front.bytes_remaining, static_cast<size_t>(bytes));
MutableNetworkTrafficAnnotationTag annotation = front.annotation;
front.bytes_remaining -= static_cast<size_t>(bytes);
if (front.bytes_remaining == 0) {
upload_annotations_.pop_front();
}
return annotation;
}
// Issues an inner write of `bytes` bytes from `inner_write_buffer_`. Handles
// partial writes by advancing `inner_write_offset_` and re-issuing until the
// chunk completes (or goes async). The scratch buffer is wrapped in a
// DrainableIOBuffer so partial writes do not copy. `annotation` is the
// traffic annotation of the originating Write() call whose bytes are in
// this chunk; it is retained for partial-write retries.
void DelayedStreamSocket::IssueInnerWrite(
int bytes,
MutableNetworkTrafficAnnotationTag annotation) {
CHECK_GT(bytes, 0);
inner_write_pulled_ = bytes;
inner_write_offset_ = 0;
current_inner_write_annotation_ = std::move(annotation);
int rv = SubmitCurrentInnerWriteChunk();
if (rv != ERR_IO_PENDING) {
HandleInnerWriteResult(rv);
}
}
int DelayedStreamSocket::SubmitCurrentInnerWriteChunk() {
int remaining = inner_write_pulled_ - inner_write_offset_;
CHECK_GT(remaining, 0);
inner_write_pending_ = true;
// DrainableIOBuffer wraps the scratch buffer with the current offset, so
// we can re-issue the remainder without copying.
auto chunk = base::MakeRefCounted<DrainableIOBuffer>(
inner_write_buffer_, static_cast<size_t>(inner_write_pulled_));
if (inner_write_offset_ > 0) {
chunk->DidConsume(inner_write_offset_);
}
return wrapped_socket_->Write(
chunk.get(), remaining,
base::BindOnce(&DelayedStreamSocket::OnInnerWriteComplete,
weak_factory_.GetWeakPtr()),
NetworkTrafficAnnotationTag(current_inner_write_annotation_));
}
void DelayedStreamSocket::OnInnerWriteComplete(int result) {
HandleInnerWriteResult(result);
}
// Drives the upload pipeline forward after each inner-write completion.
// Loops to handle partial writes and sync drain continuation without
// recursion.
void DelayedStreamSocket::HandleInnerWriteResult(int result) {
while (true) {
inner_write_pending_ = false;
int remaining = inner_write_pulled_ - inner_write_offset_;
// An inner-socket Write that returns 0 for a positive-length chunk means
// the socket is closed; without this guard the loop would re-issue the
// same chunk forever. Map it to ERR_CONNECTION_CLOSED so the error path
// below tears the pipeline down.
// Zero bytes written for a non-zero buffer indicates connection closure.
if (result == 0 && remaining > 0) {
result = ERR_CONNECTION_CLOSED;
}
// Inner write failed. Surface the error to the caller (if any) and drop
// any buffered upload; the socket is no longer writable.
if (result < 0) {
upload_buffer_.Reset();
upload_annotations_.clear();
inner_write_pulled_ = 0;
inner_write_offset_ = 0;
if (pending_write_callback_) {
pending_write_buffer_ = nullptr;
pending_write_buffer_len_ = 0;
// Route through the WeakPtr-bound trampoline so Disconnect()/dtor
// cancels this pending completion per the Socket contract
// (consistent with the read paths).
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&DelayedStreamSocket::DispatchPendingCompletion,
weak_factory_.GetWeakPtr(),
/*keep_alive=*/scoped_refptr<IOBuffer>(),
std::move(pending_write_callback_), result));
}
return;
}
if (result < remaining) {
// Partial write: advance the offset and re-issue from there.
inner_write_offset_ += result;
int rv = SubmitCurrentInnerWriteChunk();
if (rv == ERR_IO_PENDING) {
return;
}
result = rv;
continue;
}
// Full chunk written. Reset offset state for the next chunk.
inner_write_pulled_ = 0;
inner_write_offset_ = 0;
// If the buffer is drained and a Write was waiting for completion, fire
// its callback now. Post the invocation so this completion never lands
// on the consumer's stack while we are still inside Write() or any
// other method on `this`.
if (upload_buffer_.empty() && pending_write_callback_ &&
!has_stashed_write()) {
int write_result = pending_write_buffer_len_;
pending_write_buffer_len_ = 0;
// Route through the WeakPtr-bound trampoline so Disconnect()/dtor
// cancels this pending completion per the Socket contract.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&DelayedStreamSocket::DispatchPendingCompletion,
weak_factory_.GetWeakPtr(),
/*keep_alive=*/scoped_refptr<IOBuffer>(),
std::move(pending_write_callback_), write_result));
return;
}
if (!upload_buffer_.has_ready_data()) {
return;
}
// More data ready. Throttle path is async; let MaybeDrainUploadBuffer
// handle it. Unlimited-throughput path can pull and write inline.
if (upload_throttle_) {
MaybeDrainUploadBuffer();
return;
}
// Cap the next pull at the front annotation's bytes_remaining (same
// reason as MaybeDrainUploadBuffer): never aggregate two callers'
// annotations into a single inner Write.
CHECK(!upload_annotations_.empty());
size_t max_pull = std::min(static_cast<size_t>(kInnerWriteBufferSize),
upload_annotations_.front().bytes_remaining);
auto dest = inner_write_buffer_->span().first(max_pull);
int pulled = upload_buffer_.Pull(dest);
if (pulled == 0) {
return;
}
current_inner_write_annotation_ = ConsumeUploadAnnotationFront(pulled);
inner_write_pulled_ = pulled;
inner_write_offset_ = 0;
int rv = SubmitCurrentInnerWriteChunk();
if (rv == ERR_IO_PENDING) {
return;
}
result = rv;
}
}
void DelayedStreamSocket::OnUploadDataReady() {
if (!inner_write_pending_) {
MaybeDrainUploadBuffer();
}
}
void DelayedStreamSocket::OnUploadSpaceAvailable() {
// Buffer freed space; retry the stashed write.
if (!pending_write_callback_ || !has_stashed_write()) {
return;
}
auto data = pending_write_buffer_->span().first(
static_cast<size_t>(pending_write_buffer_len_));
int accepted = upload_buffer_.Push(data);
if (accepted == 0) {
return; // Still full; wait for more draining.
}
// Queue the stashed write's annotation alongside the just-Push()ed bytes.
upload_annotations_.push_back(
{static_cast<size_t>(accepted), stashed_write_annotation_});
// Drain synchronously while keeping `pending_write_buffer_` non-null.
// HandleInnerWriteResult's success branch is gated on
// `!pending_write_buffer_`, so it will NOT consume our callback and
// mis-report a full-accept completion. Its error branch, however, does
// not check `pending_write_buffer_`, so a synchronous inner-write error
// during this drain still routes through `pending_write_callback_` and
// reports the failure to the original Write() caller.
MaybeDrainUploadBuffer();
if (!pending_write_callback_) {
// Sync drain hit an error and HandleInnerWriteResult already fired
// our callback with the error code; nothing more to do.
return;
}
// Drain completed (or went async) without error. Move state out and
// post the partial-accept completion. The freshly-pushed bytes will be
// throttled (and timed) during their actual drain to the OS socket;
// don't double-charge the throttle by re-timing the caller's completion
// here. Route through the WeakPtr-bound trampoline so Disconnect()/dtor
// cancels this pending completion per the Socket contract.
CompletionOnceCallback callback = std::move(pending_write_callback_);
pending_write_buffer_ = nullptr;
pending_write_buffer_len_ = 0;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&DelayedStreamSocket::DispatchPendingCompletion,
weak_factory_.GetWeakPtr(),
/*keep_alive=*/scoped_refptr<IOBuffer>(),
std::move(callback), accepted));
}
// --- Passthrough methods ---
int DelayedStreamSocket::SetReceiveBufferSize(int32_t size) {
return wrapped_socket_->SetReceiveBufferSize(size);
}
int DelayedStreamSocket::SetSendBufferSize(int32_t size) {
return wrapped_socket_->SetSendBufferSize(size);
}
void DelayedStreamSocket::SetDnsAliases(std::set<std::string> aliases) {
wrapped_socket_->SetDnsAliases(std::move(aliases));
}
const std::set<std::string>& DelayedStreamSocket::GetDnsAliases() const {
return wrapped_socket_->GetDnsAliases();
}
void DelayedStreamSocket::Disconnect() {
connect_timer_.Stop();
connect_pending_ = false;
download_buffer_.Reset();
upload_buffer_.Reset();
inner_read_pending_ = false;
processing_inner_read_ = false;
inner_read_eof_ = false;
inner_read_error_ = 0;
pending_read_callback_.Reset();
pending_read_buffer_ = nullptr;
pending_read_buffer_len_ = 0;
pending_read_if_ready_callback_.Reset();
// Cancel() is needed *in addition to* InvalidateWeakPtrs() below: the
// posted trampoline binds both a WeakPtr and the consumer's callback.
// InvalidateWeakPtrs() stops the trampoline from running, but the bound
// CompletionOnceCallback would still hold a (cancelled) reference until
// the closure's natural lifetime expires. Cancel() releases it now.
pending_read_if_ready_dispatch_.Cancel();
pending_read_throttle_grant_ = 0;
download_throttle_pending_ = false;
// Cancel any in-flight throttle requests so a Disconnect-then-
// Connect cycle doesn't leave tokens charged against the shared
// throttle on behalf of a request whose completion we'll never honor.
download_throttle_cancellation_ = BandwidthThrottle::CancellationHandle();
inner_write_pending_ = false;
inner_write_pulled_ = 0;
inner_write_offset_ = 0;
// Drop the queued/stashed annotations so a subsequent reconnect's first
// inner write does not inherit the previous session's tag.
current_inner_write_annotation_ = MutableNetworkTrafficAnnotationTag();
stashed_write_annotation_ = MutableNetworkTrafficAnnotationTag();
upload_annotations_.clear();
pending_write_callback_.Reset();
pending_write_buffer_ = nullptr;
pending_write_buffer_len_ = 0;
upload_throttle_pending_ = false;
upload_throttle_cancellation_ = BandwidthThrottle::CancellationHandle();
// Consumed by the next `Connect()` to reset `was_ever_used_`, matching
// TCPClientSocket's `previously_disconnected_` pattern.
previously_disconnected_ = true;
weak_factory_.InvalidateWeakPtrs();
wrapped_socket_->Disconnect();
}
bool DelayedStreamSocket::IsConnected() const {
// While our own Connect is in flight, the wrapped socket may already
// have completed its Connect but our caller hasn't been signalled OK
// yet. Report not-yet-connected so callers respect the wrapper's
// state machine. `connect_pending_` covers the window from Connect()
// entry until our latency callback fires, which is strictly wider
// than `connect_timer_.IsRunning()` (the timer is not yet armed
// between Connect() and the wrapped socket's async completion).
if (connect_pending_) {
return false;
}
// Per StreamSocket::IsConnected: "True is returned if the connection
// was terminated, but there is unread data in the incoming buffer."
// The read-ahead sitting in `download_buffer_` counts as unread data,
// so keep reporting connected until it drains, even after the wrapped
// socket has closed.
return wrapped_socket_->IsConnected() || !download_buffer_.empty();
}
bool DelayedStreamSocket::IsConnectedAndIdle() const {
// See IsConnected(): don't lie about idle-ness while our own Connect
// delay hasn't fired the consumer's callback yet.
if (connect_pending_) {
return false;
}
// Buffered read-ahead bytes that the consumer hasn't pulled yet count as
// pending data on this StreamSocket; if we reported "idle" with those
// bytes still sitting in our buffer a socket-pool reuse would deliver
// stale data to the next transaction.
return wrapped_socket_->IsConnectedAndIdle() && download_buffer_.empty();
}
int DelayedStreamSocket::GetPeerAddress(IPEndPoint* address) const {
return wrapped_socket_->GetPeerAddress(address);
}
int DelayedStreamSocket::GetLocalAddress(IPEndPoint* address) const {
return wrapped_socket_->GetLocalAddress(address);
}
const NetLogWithSource& DelayedStreamSocket::NetLog() const {
return wrapped_socket_->NetLog();
}
bool DelayedStreamSocket::WasEverUsed() const {
// Per `StreamSocket::WasEverUsed`, layered sockets must report whether
// their *own* Read()/Write() methods were called, not the inner
// transport's. The wrapper's Connect already exchanges latency-modelled
// bytes with the inner socket (e.g. read-ahead), so delegating would
// report "used" before the consumer had a chance to call us.
return was_ever_used_;
}
NextProto DelayedStreamSocket::GetNegotiatedProtocol() const {
return wrapped_socket_->GetNegotiatedProtocol();
}
bool DelayedStreamSocket::GetSSLInfo(SSLInfo* ssl_info) {
return wrapped_socket_->GetSSLInfo(ssl_info);
}
int64_t DelayedStreamSocket::GetTotalReceivedBytes() const {
return wrapped_socket_->GetTotalReceivedBytes();
}
void DelayedStreamSocket::ApplySocketTag(const SocketTag& tag) {
wrapped_socket_->ApplySocketTag(tag);
}
void DelayedStreamSocket::SetBeforeConnectCallback(
const BeforeConnectCallback& before_connect_callback) {
wrapped_socket_->SetBeforeConnectCallback(before_connect_callback);
}
int DelayedStreamSocket::ConfirmHandshake(CompletionOnceCallback callback) {
return wrapped_socket_->ConfirmHandshake(std::move(callback));
}
std::optional<std::string_view>
DelayedStreamSocket::GetPeerApplicationSettings() const {
return wrapped_socket_->GetPeerApplicationSettings();
}
void DelayedStreamSocket::GetSSLCertRequestInfo(
SSLCertRequestInfo* cert_request_info) const {
wrapped_socket_->GetSSLCertRequestInfo(cert_request_info);
}
} // namespace net