Skip to content
Open
283 changes: 283 additions & 0 deletions docs/adr/ADR-0024-fetch-requesters-stay-within-the-responder-share.md

Large diffs are not rendered by default.

276 changes: 276 additions & 0 deletions docs/adr/ADR-0025-fetch-busy-reply.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ See [`TOOLING.md`](./TOOLING.md) for `adrs`, `adr-kit`, and AI harness setup.
- [ADR-0020: Kademlia-style chunk GET: return the chunk or closer peers](./ADR-0020-kademlia-chunk-get-or-closer.md)
- [ADR-0022: Remove the LMDB Chunk Store](./ADR-0022-remove-the-lmdb-chunk-store.md)
- [ADR-0023: Spread hot-record reads across the close group, and refuse full browser sessions explicitly](./ADR-0023-spread-hot-reads-and-refuse-full-browser-sessions.md)
- [ADR-0024: Fetch requesters stay within the responder's per-peer share](./ADR-0024-fetch-requesters-stay-within-the-responder-share.md)
- [ADR-0025: Tell a fetch requester "busy" instead of refusing silently](./ADR-0025-fetch-busy-reply.md)
- [ADR-0027: Map the browser listener's port through UPnP](./ADR-0027-map-the-browser-listener-port-through-upnp.md)
- [ADR-0028: Pressure-gated retention of out-of-range records](./ADR-0028-pressure-gated-retention.md)
139 changes: 139 additions & 0 deletions src/replication/audit_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,25 @@ static RESPONDER_ADMISSION_DROPS: [AtomicU64; N_RESPONDER_CLASSES * N_ADMISSION_
static RESPONDER_STALENESS_SHEDS: [AtomicU64; N_RESPONDER_CLASSES] =
[const { AtomicU64::new(0) }; N_RESPONDER_CLASSES];

/// Fetch admission refusals by what became of the busy reply, indexed by
/// [`FetchBusyReplyOutcome::index`].
static FETCH_BUSY_REPLIES: [AtomicU64; FetchBusyReplyOutcome::COUNT] =
[const { AtomicU64::new(0) }; FetchBusyReplyOutcome::COUNT];

/// Busy answers this node received to its own fetches.
static FETCH_BUSY_RECEIVED: AtomicU64 = AtomicU64::new(0);

/// Busy answers this node charged, because their source had used up its
/// neutral allowance without serving it.
static FETCH_BUSY_CHARGED: AtomicU64 = AtomicU64::new(0);

/// Times this node, choosing where to start or retry a fetch, passed over a
/// holder that had recently answered it busy.
static FETCH_BUSY_COOLING_SKIPS: AtomicU64 = AtomicU64::new(0);

/// Holders this node currently remembers as having answered it busy.
static FETCH_BUSY_SOURCES_TRACKED: AtomicU64 = AtomicU64::new(0);

static SERIAL_QUEUE_OVERFLOW_DROPS: [AtomicU64; protocol::N_REPLICATION_VARIANTS] =
[const { AtomicU64::new(0) }; protocol::N_REPLICATION_VARIANTS];

Expand Down Expand Up @@ -532,6 +551,111 @@ pub fn record_responder_staleness_shed(class: ReplicationResponderClass) {
}
}

/// What became of a fetch refused at admission, as far as a busy reply goes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FetchBusyReplyOutcome {
/// The requester did not ask for busy replies (an older node): silence,
/// as before.
NotAsked,
/// Every busy-reply send permit was in use: silence.
SendPoolFull,
/// Over the per-peer or global busy-reply rate: silence.
RateLimited,
/// A busy reply to the same peer was still being sent: silence.
PeerInFlight,
/// The busy reply was handed to the transport.
Sent,
/// The transport refused the busy reply.
SendFailed,
/// The transport did not take the busy reply in time; abandoned.
TimedOut,
/// Shutdown began before the busy reply was sent.
Cancelled,
}

impl FetchBusyReplyOutcome {
const COUNT: usize = 8;

const fn index(self) -> usize {
match self {
Self::NotAsked => 0,
Self::SendPoolFull => 1,
Self::RateLimited => 2,
Self::PeerInFlight => 3,
Self::Sent => 4,
Self::SendFailed => 5,
Self::TimedOut => 6,
Self::Cancelled => 7,
}
}
}

/// Count what became of one fetch refusal's busy reply.
pub fn record_fetch_busy_reply(outcome: FetchBusyReplyOutcome) {
if let Some(counter) = FETCH_BUSY_REPLIES.get(outcome.index()) {
counter.fetch_add(1, Ordering::Relaxed);
}
}

/// Fetch refusals with this busy-reply outcome since process start.
#[must_use]
#[cfg_attr(not(feature = "logging"), allow(dead_code))]
pub fn fetch_busy_replies_total(outcome: FetchBusyReplyOutcome) -> u64 {
FETCH_BUSY_REPLIES
.get(outcome.index())
.map_or(0, |counter| counter.load(Ordering::Relaxed))
}

/// Count a busy answer to one of this node's own fetches.
pub fn record_fetch_busy_received() {
FETCH_BUSY_RECEIVED.fetch_add(1, Ordering::Relaxed);
}

/// Busy answers to this node's own fetches since process start.
#[must_use]
#[cfg_attr(not(feature = "logging"), allow(dead_code))]
pub fn fetch_busy_received_total() -> u64 {
FETCH_BUSY_RECEIVED.load(Ordering::Relaxed)
}

/// Count a busy answer charged because its source was past its neutral
/// allowance.
pub fn record_fetch_busy_charged() {
FETCH_BUSY_CHARGED.fetch_add(1, Ordering::Relaxed);
}

/// Busy answers this node charged since process start.
#[must_use]
#[cfg_attr(not(feature = "logging"), allow(dead_code))]
pub fn fetch_busy_charged_total() -> u64 {
FETCH_BUSY_CHARGED.load(Ordering::Relaxed)
}

/// Count a fetch started or retried on a later holder because an earlier one
/// had recently answered busy.
pub fn record_fetch_busy_cooling_skip() {
FETCH_BUSY_COOLING_SKIPS.fetch_add(1, Ordering::Relaxed);
}

/// Fetches started or retried past a cooling holder since process start.
#[must_use]
#[cfg_attr(not(feature = "logging"), allow(dead_code))]
pub fn fetch_busy_cooling_skips_total() -> u64 {
FETCH_BUSY_COOLING_SKIPS.load(Ordering::Relaxed)
}

/// Set how many holders this node remembers as having answered it busy.
pub fn set_fetch_busy_sources_tracked(count: usize) {
FETCH_BUSY_SOURCES_TRACKED.store(u64::try_from(count).unwrap_or(u64::MAX), Ordering::Relaxed);
}

/// Holders this node remembers as having answered it busy.
#[must_use]
#[cfg_attr(not(feature = "logging"), allow(dead_code))]
pub fn fetch_busy_sources_tracked() -> u64 {
FETCH_BUSY_SOURCES_TRACKED.load(Ordering::Relaxed)
}

/// One class's admission pressure, read together for a summary line.
#[cfg_attr(not(feature = "logging"), allow(dead_code))]
struct ResponderAdmissionSnapshot {
Expand Down Expand Up @@ -640,6 +764,21 @@ pub fn log_responder_admission_summary() {
fetch_admission_dropped_global_pool = fetch.global_pool,
fetch_admission_dropped_per_peer_share = fetch.per_peer_share,
fetch_staleness_shed = fetch.staleness_shed,
fetch_busy_reply_not_asked = fetch_busy_replies_total(FetchBusyReplyOutcome::NotAsked),
fetch_busy_reply_send_pool_full =
fetch_busy_replies_total(FetchBusyReplyOutcome::SendPoolFull),
fetch_busy_reply_rate_limited =
fetch_busy_replies_total(FetchBusyReplyOutcome::RateLimited),
fetch_busy_reply_peer_in_flight =
fetch_busy_replies_total(FetchBusyReplyOutcome::PeerInFlight),
fetch_busy_reply_sent = fetch_busy_replies_total(FetchBusyReplyOutcome::Sent),
fetch_busy_reply_send_failed = fetch_busy_replies_total(FetchBusyReplyOutcome::SendFailed),
fetch_busy_reply_timed_out = fetch_busy_replies_total(FetchBusyReplyOutcome::TimedOut),
fetch_busy_reply_cancelled = fetch_busy_replies_total(FetchBusyReplyOutcome::Cancelled),
fetch_busy_received = fetch_busy_received_total(),
fetch_busy_charged = fetch_busy_charged_total(),
fetch_busy_cooling_skips = fetch_busy_cooling_skips_total(),
fetch_busy_sources_tracked = fetch_busy_sources_tracked(),
verification_admission_dropped_global_pool = verification.global_pool,
verification_admission_dropped_per_peer_share = verification.per_peer_share,
verification_staleness_shed = verification.staleness_shed,
Expand Down
Loading
Loading