cache: Per-client fair-share bandwidth on table uplink
CI / vulncheck (pull_request) Successful in 20s
CI / check-and-test (pull_request) Failing after 21s

This commit is contained in:
2026-09-09 20:22:23 +00:00
parent 50ca0a071c
commit b7710de0ca
13 changed files with 439 additions and 18 deletions
+13
View File
@@ -19,6 +19,11 @@ type Config struct {
MaxConcurrentRequests int64 `yaml:"max_concurrent_requests" default:"200"`
MaxRequestsPerClient int64 `yaml:"max_requests_per_client" default:"5"`
// Table-tier uplink bandwidth shaping (bytes/sec). Distinct from MaxRequestsPerClient.
// Empty/"0" uplink and 0 max_bytes_per_client_per_sec = disabled (current unlimited behavior).
UplinkBandwidth string `yaml:"uplink_bandwidth"` // e.g. "10MB" via go-units = bytes/sec
MaxBytesPerClientPerSec int64 `yaml:"max_bytes_per_client_per_sec"` // absolute per-client cap; 0 = none
// Hardening limits (security/correctness)
MaxObjectSize string `yaml:"max_object_size" default:"0"` // 0=unlimited; e.g. "256MB" protects against OOM from huge/malicious upstream responses
TrustedProxies []string `yaml:"trusted_proxies"` // CIDR list; empty=never trust X-Forwarded-For (safe default). See README security notes.
@@ -186,6 +191,14 @@ func (c Config) Validate() error {
if c.MaxRequestsPerClient < 0 {
return fmt.Errorf("negative per-client limit not allowed")
}
if c.MaxBytesPerClientPerSec < 0 {
return fmt.Errorf("negative max_bytes_per_client_per_sec not allowed")
}
if c.UplinkBandwidth != "" && c.UplinkBandwidth != "0" {
if _, err := units.FromHumanSize(c.UplinkBandwidth); err != nil {
return fmt.Errorf("invalid uplink_bandwidth: %w", err)
}
}
if c.Cache.Memory.GCAlgorithm != "" {
switch c.Cache.Memory.GCAlgorithm {
+69
View File
@@ -211,3 +211,72 @@ func TestValidate(t *testing.T) {
})
}
}
func TestValidateUplinkBandwidth(t *testing.T) {
cases := []struct {
name string
mutate func(*Config)
wantErr bool
errSub string
}{
{
name: "empty uplink ok",
mutate: func(c *Config) {
c.UplinkBandwidth = ""
c.MaxBytesPerClientPerSec = 0
},
},
{
name: "zero uplink ok",
mutate: func(c *Config) {
c.UplinkBandwidth = "0"
},
},
{
name: "valid human size",
mutate: func(c *Config) {
c.UplinkBandwidth = "10MB"
},
},
{
name: "invalid uplink",
mutate: func(c *Config) {
c.UplinkBandwidth = "not-a-size"
},
wantErr: true,
errSub: "uplink_bandwidth",
},
{
name: "negative max bytes",
mutate: func(c *Config) {
c.MaxBytesPerClientPerSec = -1
},
wantErr: true,
errSub: "max_bytes_per_client_per_sec",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
c := GetDefaultConfig()
tt.mutate(&c)
err := c.Validate()
if tt.wantErr {
if err == nil {
t.Fatalf("Validate() error = nil, wantErr")
}
if tt.errSub != "" && !contains(err.Error(), tt.errSub) {
t.Fatalf("Validate() error %q does not contain %q", err.Error(), tt.errSub)
}
return
}
if err != nil {
t.Fatalf("Validate() unexpected error: %v", err)
}
})
}
}
func contains(s, sub string) bool {
return strings.Contains(s, sub)
}