check / check (push) Successful in 17s
Nothing bounded total in-flight work, so a burst of cache misses across hosts could exhaust memory. Two settings now do: max_concurrent_processing (default the CPUs Go uses) and upstream_connections (default 64, beside the per-host limit). A request that finds either full waits up to 10 seconds, then gets 503; a slot is released on every path. No request holds source bytes while it waits: a cached source is read only after the processing slot is taken, and a fetched one only while it holds its upstream connection. libvips runs one worker thread per image with its operation cache off. Both waits count toward downstream_timeout, as the README says. Model: opus-5-5
163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
package config
|
|
|
|
import (
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
// The variables that set the two concurrency limits.
|
|
const (
|
|
testMaxConcurrentProcessingVar = "PIXA_MAX_CONCURRENT_PROCESSING"
|
|
testUpstreamConnectionsVar = "PIXA_UPSTREAM_CONNECTIONS"
|
|
)
|
|
|
|
// TestOmittedConcurrencyLimitsUseDefaults checks that an omitted
|
|
// max_concurrent_processing is the number of CPUs Go uses and an omitted
|
|
// upstream_connections is 64.
|
|
func TestOmittedConcurrencyLimitsUseDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c, err := configFromYAML(t, signingKeyLine)
|
|
if err != nil {
|
|
t.Fatalf("minimal config should be valid, got error: %v", err)
|
|
}
|
|
|
|
if c.MaxConcurrentProcessing != runtime.GOMAXPROCS(0) {
|
|
t.Errorf("MaxConcurrentProcessing = %d, want %d, one per CPU",
|
|
c.MaxConcurrentProcessing, runtime.GOMAXPROCS(0))
|
|
}
|
|
|
|
if c.UpstreamConnections != 64 {
|
|
t.Errorf("UpstreamConnections = %d, want 64", c.UpstreamConnections)
|
|
}
|
|
}
|
|
|
|
// TestExplicitConcurrencyLimitsAreUsed checks that valid values for the
|
|
// two limits are used as given.
|
|
func TestExplicitConcurrencyLimitsAreUsed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c, err := configFromYAML(t, signingKeyLine+
|
|
"max_concurrent_processing: 3\nupstream_connections: 10\n")
|
|
if err != nil {
|
|
t.Fatalf("valid config should load, got error: %v", err)
|
|
}
|
|
|
|
if c.MaxConcurrentProcessing != 3 {
|
|
t.Errorf("MaxConcurrentProcessing = %d, want 3", c.MaxConcurrentProcessing)
|
|
}
|
|
|
|
if c.UpstreamConnections != 10 {
|
|
t.Errorf("UpstreamConnections = %d, want 10", c.UpstreamConnections)
|
|
}
|
|
}
|
|
|
|
// TestInvalidConcurrencyLimitAbortsStartup checks that a limit that is
|
|
// not a whole number of at least 1, or is null, aborts startup naming the
|
|
// key and the value, and the variable too where the value could have come
|
|
// from it.
|
|
func TestInvalidConcurrencyLimitAbortsStartup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
processing := keyMaxConcurrentProcessing
|
|
connections := keyUpstreamConnections
|
|
|
|
runAbortCases(t, []abortCase{
|
|
{
|
|
name: "max_concurrent_processing zero",
|
|
yaml: signingKeyLine + processing + ": 0\n",
|
|
wantErrSubstrings: []string{
|
|
processing, testMaxConcurrentProcessingVar, "value 0",
|
|
},
|
|
},
|
|
{
|
|
name: "max_concurrent_processing negative",
|
|
yaml: signingKeyLine + processing + ": -2\n",
|
|
wantErrSubstrings: []string{
|
|
processing, testMaxConcurrentProcessingVar, "value -2",
|
|
},
|
|
},
|
|
{
|
|
name: "max_concurrent_processing not a number",
|
|
yaml: signingKeyLine + processing + ": lots\n",
|
|
wantErrSubstrings: []string{
|
|
processing, testMaxConcurrentProcessingVar, "lots",
|
|
},
|
|
},
|
|
{
|
|
name: "max_concurrent_processing fractional",
|
|
yaml: signingKeyLine + processing + ": 1.5\n",
|
|
wantErrSubstrings: []string{processing, "1.5"},
|
|
},
|
|
{
|
|
name: "max_concurrent_processing null",
|
|
yaml: signingKeyLine + processing + ": null\n",
|
|
wantErrSubstrings: []string{processing, nullValueText},
|
|
},
|
|
{
|
|
name: "upstream_connections zero",
|
|
yaml: signingKeyLine + connections + ": 0\n",
|
|
wantErrSubstrings: []string{
|
|
connections, testUpstreamConnectionsVar, "value 0",
|
|
},
|
|
},
|
|
{
|
|
name: "upstream_connections negative",
|
|
yaml: signingKeyLine + connections + ": -5\n",
|
|
wantErrSubstrings: []string{
|
|
connections, testUpstreamConnectionsVar, "value -5",
|
|
},
|
|
},
|
|
{
|
|
name: "upstream_connections not a number",
|
|
yaml: signingKeyLine + connections + ": many\n",
|
|
wantErrSubstrings: []string{
|
|
connections, testUpstreamConnectionsVar, "many",
|
|
},
|
|
},
|
|
{
|
|
name: "upstream_connections null",
|
|
yaml: signingKeyLine + connections + ": null\n",
|
|
wantErrSubstrings: []string{connections, nullValueText},
|
|
},
|
|
})
|
|
}
|
|
|
|
// TestConcurrencyLimitsFromEnvironment checks that the two variables set
|
|
// the limits over the config file, and that an invalid value in either
|
|
// aborts startup naming the variable and the value.
|
|
func TestConcurrencyLimitsFromEnvironment(t *testing.T) {
|
|
t.Setenv(testMaxConcurrentProcessingVar, "3")
|
|
t.Setenv(testUpstreamConnectionsVar, "10")
|
|
|
|
c, err := configFromYAML(t, signingKeyLine+
|
|
"max_concurrent_processing: 5\nupstream_connections: 50\n")
|
|
if err != nil {
|
|
t.Fatalf("limits from the environment should load: %v", err)
|
|
}
|
|
|
|
if c.MaxConcurrentProcessing != 3 || c.UpstreamConnections != 10 {
|
|
t.Errorf("limits = %d and %d, want 3 and 10 from the environment",
|
|
c.MaxConcurrentProcessing, c.UpstreamConnections)
|
|
}
|
|
|
|
cases := []struct {
|
|
variable string
|
|
value string
|
|
}{
|
|
{testMaxConcurrentProcessingVar, "lots"},
|
|
{testMaxConcurrentProcessingVar, "0"},
|
|
{testUpstreamConnectionsVar, "-1"},
|
|
{testUpstreamConnectionsVar, "ten"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.variable+"="+tc.value, func(t *testing.T) {
|
|
t.Setenv(tc.variable, tc.value)
|
|
|
|
_, err := configFromYAML(t, signingKeyLine)
|
|
wantStartupError(t, err, tc.variable, tc.value)
|
|
})
|
|
}
|
|
}
|