| // 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 |