cache: Per-client fair-share bandwidth on table uplink
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user