blob: 2f972421aa5fa0188687615130bfffd5f1fcb022 [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.
#ifndef NET_SOCKET_BANDWIDTH_THROTTLE_H_
#define NET_SOCKET_BANDWIDTH_THROTTLE_H_
#include <stdint.h>
#include "base/containers/queue.h"
#include "base/functional/callback.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker_impl.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "net/base/net_export.h"
namespace base {
template <typename T>
class RefCountedData;
}
namespace net {
// A shared token-bucket bandwidth throttle. Multiple sockets draw from the
// same throttle to simulate a single constrained network link.
//
// Tokens represent bytes. They accumulate at `throughput_bytes_per_sec`
// bytes/sec up to a maximum of `burst_size` bytes. The bucket starts
// full, as this models the link's bandwidth, not congestion control mechanisms.
// Requests are always completed on a posted task — zero delay when tokens are
// immediately available, otherwise after enough tokens have accumulated.
// Posting completion (rather than running it inline) lets callers safely chain
// the throttle into their own Read/Write contracts without reentrancy
// concerns.
//
// Requests are served FIFO. A queued request blocks subsequent requests even
// if a later, smaller request could be served — this prevents starvation and
// models a real link where bytes are delivered in order.
//
// Cancellation: RequestBytes() returns a CancellationHandle. Destroying or
// explicitly cancelling it removes the request from the queue and
// prevents it from charging tokens. Sockets that own a CancellationHandle get
// automatic cancellation on destruction — this is critical because the
// throttle is shared across sockets, and a destroyed socket's pending
// request would otherwise steal tokens from live sockets.
//
// The pending-requests queue is unbounded; the throttle relies on its
// callers for backpressure. The expected pattern is one in-flight
// RequestBytes() per logical flow: hold a single CancellationHandle at a
// time and only enqueue the next byte request after the previous
// callback has fired. Under that discipline the queue size is bounded
// by the number of active flows sharing the throttle (each socket
// using one or two flows).
//
// A misbehaving caller that issues unbounded concurrent
// RequestBytes() calls would grow the queue without bound, consuming
// memory and — because the queue is FIFO — delaying every other
// flow's progress behind their backlog. The throttle does not defend
// against this; bounding is the caller's responsibility, similar to a
// caller-owned write buffer.
//
// Thread-unsafe: all calls must happen on the same sequence.
class NET_EXPORT BandwidthThrottle
: public base::RefCounted<BandwidthThrottle> {
public:
REQUIRE_ADOPTION_FOR_REFCOUNTED_TYPE();
// Callback invoked when a RequestBytes() request has been admitted.
using ThrottleCallback = base::OnceClosure;
// Movable RAII handle to a pending request. Destroying or explicitly
// calling Cancel() removes the request from the throttle's queue and
// prevents it from charging tokens. Cancellation is safe across
// throttle destruction: the cancellation flag is shared between the
// throttle's queue entry and this handle via scoped_refptr.
//
// Default-constructed handles are no-op. CancellationHandles are
// sequence-affine: Cancel() and the destructor must run on the same
// sequence as the throttle they came from.
class NET_EXPORT CancellationHandle {
public:
CancellationHandle();
CancellationHandle(CancellationHandle&&) noexcept;
CancellationHandle& operator=(CancellationHandle&&) noexcept;
CancellationHandle(const CancellationHandle&) = delete;
CancellationHandle& operator=(const CancellationHandle&) = delete;
~CancellationHandle();
// Cancels the associated request. Idempotent. Does nothing if the
// request already completed (the entry is already popped) or this
// handle is default-constructed / moved-from. Triggers the
// throttle to re-evaluate its drain timer so a small successor
// isn't starved by the cancelled head's now-stale wait.
void Cancel();
private:
friend class BandwidthThrottle;
CancellationHandle(scoped_refptr<base::RefCountedData<bool>> canceled_flag,
base::OnceClosure on_cancel);
scoped_refptr<base::RefCountedData<bool>> canceled_flag_;
// Invoked exactly once when this handle is cancelled (whether
// by an explicit Cancel() call or by destruction). Bound to a
// BandwidthThrottle::OnCancellationHandleCancelled method via WeakPtr so
// it's a no-op after the throttle has been destroyed.
base::OnceClosure on_cancel_;
};
// `throughput_bytes_per_sec` is the sustained throughput in bytes/sec.
// Must be strictly positive. Callers that want an unconstrained link
// should not construct a BandwidthThrottle at all: the consumers that
// take a scoped_refptr<BandwidthThrottle> treat a null ref as "no
// throttling", so pass them a null ref instead of a zero-rate throttle.
//
// `burst_duration` controls the maximum burst size:
// burst_size = throughput_bytes_per_sec * burst_duration.
// Must be strictly positive.
explicit BandwidthThrottle(
uint64_t throughput_bytes_per_sec,
base::TimeDelta burst_duration = base::Milliseconds(100));
BandwidthThrottle(const BandwidthThrottle&) = delete;
BandwidthThrottle& operator=(const BandwidthThrottle&) = delete;
// Requests `bytes` from the throttle. `callback` is invoked (with no
// arguments) when the requested bytes have been admitted by the token
// bucket. Completion is always delivered on a separate task, so `callback`
// never fires before RequestBytes returns — callers can safely chain it
// with their own Read/Write contracts.
//
// The returned CancellationHandle auto-cancels the request when destroyed.
// Callers that wrap a socket-bound request must hold the CancellationHandle
// for the duration of the request; otherwise the request will be
// cancelled (and the callback will never fire).
//
// `bytes` must be strictly positive. `callback` must not be null.
[[nodiscard]] CancellationHandle RequestBytes(int bytes,
ThrottleCallback callback);
// Returns the configured throughput in bytes/sec.
uint64_t throughput_bytes_per_sec() const;
private:
friend class base::RefCounted<BandwidthThrottle>;
~BandwidthThrottle();
struct PendingRequest {
PendingRequest(int bytes,
ThrottleCallback callback,
scoped_refptr<base::RefCountedData<bool>> canceled_flag);
PendingRequest(PendingRequest&&);
PendingRequest& operator=(PendingRequest&&);
~PendingRequest();
int bytes;
ThrottleCallback callback;
// `canceled_flag` is shared with the consumer's CancellationHandle. Since
// either one can outlive the other, it's a refptr to RefCountedData. When
// true, the throttle skips this request without charging tokens.
scoped_refptr<base::RefCountedData<bool>> canceled_flag;
};
void CheckValidSequence() const;
// Refills tokens based on elapsed time since last refill.
void RefillTokens();
// Attempts to serve queued requests. Called after token refill or when a
// new request arrives.
void ProcessQueue();
// Pops cancelled entries from the front of the queue. Cancelled entries
// don't consume tokens, so we drop them before computing timing /
// charging tokens for subsequent live requests.
void DropCancelledFrontRequests();
// Invoked by a CancellationHandle's Cancel() / destructor. Re-evaluates the
// drain timer so a small successor isn't delayed by the wait that was
// sized for the now-cancelled head.
void OnCancellationHandleCancelled();
// Schedules the drain timer for the next pending request.
void ScheduleDrainTimer();
// Timer callback for draining the queue.
void OnDrainTimer();
const uint64_t throughput_bytes_per_sec_;
const double burst_size_;
double available_tokens_;
base::TimeTicks last_refill_time_;
base::queue<PendingRequest> pending_requests_;
base::OneShotTimer drain_timer_;
// True while ProcessQueue() is iterating the queue. A re-entrant
// RequestBytes() from inside a callback enqueues normally but skips
// scheduling a fresh drain timer — ProcessQueue() will process the
// newly-enqueued request itself if tokens allow, or schedule a timer
// when it returns. Avoids posting a redundant zero-delay task.
bool processing_queue_ = false;
// Binds on first use and CHECKs that all later calls use the same sequence.
// This protects the mutable token bucket, request queue, and timer state,
// which are intentionally not locked. Used directly rather than via
// SEQUENCE_CHECKER() so the check stays live in release: a cross-sequence
// call here is a memory-safety bug, and the macro compiles to a no-op
// outside DCHECK builds.
mutable base::SequenceCheckerImpl sequence_checker_;
base::WeakPtrFactory<BandwidthThrottle> weak_factory_{this};
};
} // namespace net
#endif // NET_SOCKET_BANDWIDTH_THROTTLE_H_