throughput: paced downstream rate, with the qualifier that makes it honest
A throughput number reports the smallest limit on the path, and the sender's own ceiling is one of the candidates. If the server was asked for 50 Mbps and 50 Mbps arrived, the network was never the constraint and "50 Mbps" says nothing about it. So the result always carries limited_by and measures_network, and a finding is raised only when the path is actually implicated. Loss is computed against the *sender's* count, not the requested rate: the server reports what it put on the wire, and the gap is the loss. A receiver alone cannot tell "the network dropped it" from "the sender never sent it", and guessing turns a healthy server-side limit into a phantom network fault. The count is stored per action, not per packet — half a million packets of structs would turn a measurement into memory exhaustion. Sending is paced rather than flat out. An unpaced burst measures the server's NIC and the first queue it meets, then collapses into loss that reads as a network fault. The schedule is absolute rather than sleep-per-packet, which would accumulate scheduler error and drift the rate down over a ten-second run. Throughput gets its own grant budget sized from the request, so every other action stays bounded at 8 MiB. When the byte cap binds before the clock does, the *duration* is shortened and reported, rather than the run being truncated halfway: promising thirty seconds and delivering twenty-one is the same information with a surprise attached, and it keeps "the clock ended the run" as the normal case — the only case where the rate is a clean property of the path. That last behaviour came out of a test that failed honestly: 30 s at 100 Mbps needs 375 MB against a 256 MB cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
35744c609e
commit
3333788d9e
@@ -0,0 +1,99 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package dataplane
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The grant has to be big enough that the *clock* ends a throughput run, not the byte budget. Get
|
||||
// this wrong and the test still "works": it stops early, reports a rate computed over a truncated
|
||||
// window, and nothing anywhere says the number is meaningless. So the sizing is pinned.
|
||||
func TestThroughputBudgetOutlastsTheRequestedRun(t *testing.T) {
|
||||
cases := []struct{ durationMs, kbps int }{
|
||||
{1_000, 1_000},
|
||||
{10_000, 50_000},
|
||||
{10_000, 200_000},
|
||||
{30_000, 100_000},
|
||||
}
|
||||
for _, c := range cases {
|
||||
// Against the *planned* duration, which is what will actually be run: a request the
|
||||
// server shortens is answered with the shorter number, not truncated halfway.
|
||||
planMs, planKbps := ThroughputPlan(c.durationMs, c.kbps)
|
||||
lim := ThroughputLimits(c.durationMs, c.kbps)
|
||||
needed := int64(planKbps) * 1000 / 8 * int64(planMs) / 1000
|
||||
if lim.MaxBytes < needed {
|
||||
t.Errorf("%d ms at %d kbps (planned %d ms) needs %d bytes, budget is %d - the run "+
|
||||
"would stop early and report a rate over a truncated window",
|
||||
c.durationMs, c.kbps, planMs, needed, lim.MaxBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The pacer should control the rate and the grant should be the safety net. If the grant's
|
||||
// ceiling equals the pacing target, ordinary scheduling jitter trips it and cuts the run short
|
||||
// for no real reason.
|
||||
func TestGrantRateCeilingSitsAboveThePacingTarget(t *testing.T) {
|
||||
lim := ThroughputLimits(10_000, 50_000)
|
||||
if lim.MaxKbps <= 50_000 {
|
||||
t.Fatalf("grant ceiling %d kbps is not above the 50000 kbps pacing target", lim.MaxKbps)
|
||||
}
|
||||
}
|
||||
|
||||
// A client asking for more than the server will do must get the server's number, not its own.
|
||||
func TestThroughputRequestsAreClamped(t *testing.T) {
|
||||
lim := ThroughputLimits(10*60*1000, 10_000_000) // ten minutes at 10 Gbps
|
||||
if lim.MaxBytes > maxThroughputBytes {
|
||||
t.Errorf("byte budget %d exceeds the hard cap %d", lim.MaxBytes, maxThroughputBytes)
|
||||
}
|
||||
if lim.MaxKbps > maxThroughputKbps*12/10 {
|
||||
t.Errorf("rate ceiling %d exceeds the hard cap", lim.MaxKbps)
|
||||
}
|
||||
// The hold has to outlast the planned run, or the grant expires mid-send and the run is
|
||||
// reported as rate-limited when it was really time-limited.
|
||||
planMs, _ := ThroughputPlan(10*60*1000, 10_000_000)
|
||||
if lim.MaxHold < time.Duration(planMs)*time.Millisecond {
|
||||
t.Errorf("hold %v is shorter than the planned run of %d ms", lim.MaxHold, planMs)
|
||||
}
|
||||
}
|
||||
|
||||
// When the byte cap binds before the clock does, the server shortens the run and says so, rather
|
||||
// than accepting thirty seconds and delivering twenty-one. Same information, no surprise - and it
|
||||
// keeps "the clock ended the run" as the normal case, which is the only case where the resulting
|
||||
// rate is a clean property of the network.
|
||||
func TestAnOversizedRequestComesBackShorterRatherThanTruncated(t *testing.T) {
|
||||
const kbps = 200_000
|
||||
askedMs := 30_000
|
||||
planMs, planKbps := ThroughputPlan(askedMs, kbps)
|
||||
|
||||
if planKbps != kbps {
|
||||
t.Errorf("rate was reduced to %d; the duration should absorb the cap, not the rate", planKbps)
|
||||
}
|
||||
if planMs >= askedMs {
|
||||
t.Fatalf("plan kept the full %d ms at %d kbps, which exceeds the %d byte cap",
|
||||
askedMs, kbps, maxThroughputBytes)
|
||||
}
|
||||
// And what it does promise must fit.
|
||||
if got := int64(planKbps) * 1000 / 8 * int64(planMs) / 1000; got > maxThroughputBytes {
|
||||
t.Errorf("planned run needs %d bytes, over the %d cap", got, maxThroughputBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// A short, ordinary request must come back untouched - the clamping only exists for the extremes.
|
||||
func TestAnOrdinaryRequestIsNotRewritten(t *testing.T) {
|
||||
planMs, planKbps := ThroughputPlan(10_000, 50_000)
|
||||
if planMs != 10_000 || planKbps != 50_000 {
|
||||
t.Errorf("10 s at 50 Mbps was rewritten to %d ms at %d kbps", planMs, planKbps)
|
||||
}
|
||||
}
|
||||
|
||||
// Every action other than throughput stays on the small default budget. Throughput needs a big
|
||||
// one; raising the global default to suit it would quietly unbound everything else.
|
||||
func TestOnlyThroughputGetsTheLargeBudget(t *testing.T) {
|
||||
big := ThroughputLimits(10_000, 50_000)
|
||||
if big.MaxBytes <= 8<<20 {
|
||||
t.Fatalf("throughput budget %d is no larger than the default action budget", big.MaxBytes)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user