apply nav burst to the first requests only and set it to 10 by default

This commit is contained in:
Pierre Tachoire committed 2026-09-10 08:17:02 +02:00
1 parent ce0474d81f
commit 79b4604f2b
3 files changed
+43 -20

No files matched your search

+22 -1
View File
@@ -659,11 +659,15 @@ pub fn httpNavDelay(self: *const Config) HttpNavDelay {
return if (ms == 0) .off else .{ .fixed = ms };
}
// Navigations an idle host absorbs before the rate limiter spaces them. A
// short job never reaches it, so the limiter stays invisible to it.
pub const DEFAULT_NAV_BURST: u32 = 10;
pub fn httpNavBurst(self: *const Config) u32 {
const burst = switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| opts.http_nav_burst,
else => unreachable,
} orelse 1;
} orelse DEFAULT_NAV_BURST;
return @max(burst, 1);
}
@@ -1262,6 +1266,23 @@ test "Config: httpNavDelay modes" {
}
}
test "Config: httpNavBurst" {
// unset: an idle host absorbs DEFAULT_NAV_BURST navigations at once
{
var config = try Config.init(std.testing.allocator, "test", .{ .serve = .{} });
defer config.deinit(std.testing.allocator);
try std.testing.expectEqual(DEFAULT_NAV_BURST, config.httpNavBurst());
}
// 0 is treated as 1: strict spacing, no burst
{
var config = try Config.init(std.testing.allocator, "test", .{ .serve = .{
.http_nav_burst = 0,
} });
defer config.deinit(std.testing.allocator);
try std.testing.expectEqual(1, config.httpNavBurst());
}
}
test "Config: blockedUrlPatterns splits comma-separated patterns" {
var config = try Config.init(std.testing.allocator, "test", .{ .serve = .{
.block_urls = "*doubleclick*,*://*/*.png",
+4 -3
View File
@@ -421,9 +421,10 @@
\\ Defaults to 1 GiB.
\\ --http-nav-burst <INT>
\\ Number of top-level navigations to an idle host allowed to
\\ start without waiting for --http-nav-delay. After a burst,
\\ navigations are spaced by --http-nav-delay again.
\\ Defaults to 1.
\\ start without waiting. The allowance refills as the host goes
\\ idle again. It buys a head start only: it does not change how
\\ fast the delay grows once the host is busy.
\\ Defaults to 10.
\\ --http-nav-delay <INT>
\\ Fixed time in ms between two top-level navigations to the same
\\ host (see --http-nav-burst). Set to 0 to disable rate limiting.
+17 -16
View File
@@ -35,8 +35,9 @@ pub const Mode = enum { fixed, adaptive };
pub const ADAPTIVE_INTERVAL_MS: u64 = 20;
// In adaptive mode, the interval grows by one base interval every
// `burst * RAMP_BURSTS` navigations: the more we ask a host, the more we
// space our requests.
// `RAMP_BURSTS` navigations: the more we ask a host, the more we space our
// requests. Independent of --http-nav-burst: the burst buys a head start on
// an idle host, it does not change how fast the spacing ramps up.
pub const RAMP_BURSTS: u64 = 10;
// One navigation of pressure is forgiven every `COOLDOWN_INTERVALS` base
@@ -70,11 +71,8 @@ interval_ms: u64,
// base interval: a busy host's burst is spent faster.
tau: u64,
// The four fields below are not constants: they are derived in init from
// the --http-nav-delay and --http-nav-burst flags and the constants above.
// Pressure units per extra base interval (see RAMP_BURSTS).
ramp_step: u64,
// The three fields below are not constants: they are derived in init from
// the --http-nav-delay flag and the constants above.
// Elapsed time forgiving one unit of pressure (see COOLDOWN_INTERVALS).
cooldown_ms: u64,
@@ -126,17 +124,15 @@ pub fn init(allocator: Allocator, interval_ms: u64, burst: u32, mode: Mode) Rate
lp.assert(interval_ms > 0, "RateLimiter.init", .{ .interval_ms = interval_ms, .mode = mode });
const b: u64 = @max(burst, 1);
const ramp_step = b * RAMP_BURSTS;
return .{
.allocator = allocator,
.mode = mode,
.interval_ms = interval_ms,
.tau = interval_ms * (b - 1),
.ramp_step = ramp_step,
.cooldown_ms = interval_ms * COOLDOWN_INTERVALS,
.max_ms = interval_ms * MAX_INTERVALS,
.pressure_max = switch (mode) {
.adaptive => (MAX_INTERVALS - 1) * ramp_step,
.adaptive => (MAX_INTERVALS - 1) * RAMP_BURSTS,
.fixed => 0,
},
};
@@ -253,7 +249,7 @@ pub fn observe(self: *RateLimiter, host: []const u8, feedback: Feedback, now: u6
const h = gop.value_ptr;
self.cooldown(h, now);
h.pressure = @min(h.pressure + intervals * self.ramp_step, self.pressure_max);
h.pressure = @min(h.pressure + intervals * RAMP_BURSTS, self.pressure_max);
if (feedback.retry_after_ms) |ms| {
h.tat = @max(h.tat, now + @min(ms, RETRY_AFTER_MAX_MS));
}
@@ -278,9 +274,9 @@ fn isLoopback(host: []const u8) bool {
}
// Effective interval for a host under the given pressure: one extra base
// interval per full `ramp_step` of navigations, capped at `max_ms`.
// interval per full `RAMP_BURSTS` of navigations, capped at `max_ms`.
fn intervalFor(self: *const RateLimiter, pressure: u64) u64 {
return @min(self.interval_ms * (1 + pressure / self.ramp_step), self.max_ms);
return @min(self.interval_ms * (1 + pressure / RAMP_BURSTS), self.max_ms);
}
// Credit the time elapsed since the host's cooldown clock to its pressure.
@@ -376,7 +372,12 @@ test "RateLimiter: burst" {
var strict = RateLimiter.init(testing.allocator, 100, 0, .adaptive);
defer strict.deinit();
try testing.expectEqual(0, strict.tau);
try testing.expectEqual(10, strict.ramp_step);
// the burst only buys a head start on an idle host. It does not change
// how fast the spacing ramps up, nor how far it can go.
try testing.expectEqual(rl.pressure_max, strict.pressure_max);
try testing.expectEqual(rl.intervalFor(100), strict.intervalFor(100));
try testing.expectEqual(rl.intervalFor(595), strict.intervalFor(595));
}
test "RateLimiter: pressure ramp" {
@@ -385,8 +386,8 @@ test "RateLimiter: pressure ramp" {
defer rl.deinit();
// 10 navigations queued at once: the host is now under 10 units of
// pressure (ramp_step for burst = 1), so the 11th is spaced by 2x the
// base interval, and the 21st by 3x.
// pressure (one RAMP_BURSTS), so the 11th is spaced by 2x the base
// interval, and the 21st by 3x.
var last: u64 = 0;
for (0..10) |_| {
last = try rl.reserve("a.test", 1000);