feat: make database connection pool size configurable (#21403)

Closes https://github.com/coder/coder/issues/21360

A few considerations/notes:
- I've kept the number of conns to 10 in all other places, except coderd
- which uses the config value
- I opted to also make idle conns configurable; the greater the delta
between max open and max idle, the more connection churn
- Postgres maintains a [_process_ per
connection](https://www.postgresql.org/docs/current/connect-estab.html),
contrary to what the comment said previously
- Operators should be able to tune this, since process churn can
negatively affect OS scheduling
- I've set the value to `"auto"` by default so it's not another knob one
_has to_ twiddle, and sets max idle = max conns / 3

---------

Signed-off-by: Danny Kopping <danny@coder.com>
This commit is contained in:
Danny Kopping
2026-01-13 10:50:57 +02:00
committed by GitHub
parent 61ae5b81ab
commit 49a42eff5c
13 changed files with 334 additions and 17 deletions
+55
View File
@@ -442,6 +442,10 @@ var PostgresAuthDrivers = []string{
string(PostgresAuthAWSIAMRDS),
}
// PostgresConnMaxIdleAuto is the value for auto-computing max idle connections
// based on max open connections.
const PostgresConnMaxIdleAuto = "auto"
// DeploymentValues is the central configuration values the coder server.
type DeploymentValues struct {
Verbose serpent.Bool `json:"verbose,omitempty"`
@@ -462,6 +466,8 @@ type DeploymentValues struct {
EphemeralDeployment serpent.Bool `json:"ephemeral_deployment,omitempty" typescript:",notnull"`
PostgresURL serpent.String `json:"pg_connection_url,omitempty" typescript:",notnull"`
PostgresAuth string `json:"pg_auth,omitempty" typescript:",notnull"`
PostgresConnMaxOpen serpent.Int64 `json:"pg_conn_max_open,omitempty" typescript:",notnull"`
PostgresConnMaxIdle serpent.String `json:"pg_conn_max_idle,omitempty" typescript:",notnull"`
OAuth2 OAuth2Config `json:"oauth2,omitempty" typescript:",notnull"`
OIDC OIDCConfig `json:"oidc,omitempty" typescript:",notnull"`
Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"`
@@ -2623,6 +2629,30 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
Value: serpent.EnumOf(&c.PostgresAuth, PostgresAuthDrivers...),
YAML: "pgAuth",
},
{
Name: "Postgres Connection Max Open",
Description: "Maximum number of open connections to the database. Defaults to 10.",
Flag: "postgres-conn-max-open",
Env: "CODER_PG_CONN_MAX_OPEN",
Default: "10",
Value: serpent.Validate(&c.PostgresConnMaxOpen, func(value *serpent.Int64) error {
if value.Value() <= 0 {
return xerrors.New("must be greater than zero")
}
return nil
}),
YAML: "pgConnMaxOpen",
},
{
Name: "Postgres Connection Max Idle",
Description: "Maximum number of idle connections to the database. Set to \"auto\" (the default) to use max open / 3. " +
"Value must be greater or equal to 0; 0 means explicitly no idle connections.",
Flag: "postgres-conn-max-idle",
Env: "CODER_PG_CONN_MAX_IDLE",
Default: PostgresConnMaxIdleAuto,
Value: &c.PostgresConnMaxIdle,
YAML: "pgConnMaxIdle",
},
{
Name: "Secure Auth Cookie",
Description: "Controls if the 'Secure' property is set on browser session cookies.",
@@ -4128,3 +4158,28 @@ func (c CryptoKey) CanVerify(now time.Time) bool {
beforeDelete := c.DeletesAt.IsZero() || now.Before(c.DeletesAt)
return hasSecret && beforeDelete
}
// ComputeMaxIdleConns calculates the effective maxIdleConns value. If
// configuredIdle is "auto", it returns maxOpen/3 with a minimum of 1. If
// configuredIdle exceeds maxOpen, it returns an error.
func ComputeMaxIdleConns(maxOpen int, configuredIdle string) (int, error) {
configuredIdle = strings.TrimSpace(configuredIdle)
if configuredIdle == PostgresConnMaxIdleAuto {
computed := maxOpen / 3
if computed < 1 {
return 1, nil
}
return computed, nil
}
idle, err := strconv.Atoi(configuredIdle)
if err != nil {
return 0, xerrors.Errorf("invalid max idle connections %q: must be %q or >= 0", configuredIdle, PostgresConnMaxIdleAuto)
}
if idle < 0 {
return 0, xerrors.Errorf("max idle connections must be %q or >= 0", PostgresConnMaxIdleAuto)
}
if idle > maxOpen {
return 0, xerrors.Errorf("max idle connections (%d) cannot exceed max open connections (%d)", idle, maxOpen)
}
return idle, nil
}
+117
View File
@@ -765,3 +765,120 @@ func TestRetentionConfigParsing(t *testing.T) {
})
}
}
func TestComputeMaxIdleConns(t *testing.T) {
t.Parallel()
tests := []struct {
name string
maxOpen int
configuredIdle string
expectedIdle int
expectError bool
errorContains string
}{
{
name: "auto_default_10_open",
maxOpen: 10,
configuredIdle: "auto",
expectedIdle: 3, // 10/3 = 3
},
{
name: "auto_with_whitespace",
maxOpen: 10,
configuredIdle: " auto ",
expectedIdle: 3, // 10/3 = 3
},
{
name: "auto_30_open",
maxOpen: 30,
configuredIdle: "auto",
expectedIdle: 10, // 30/3 = 10
},
{
name: "auto_minimum_1",
maxOpen: 1,
configuredIdle: "auto",
expectedIdle: 1, // 1/3 = 0, but minimum is 1
},
{
name: "auto_minimum_2_open",
maxOpen: 2,
configuredIdle: "auto",
expectedIdle: 1, // 2/3 = 0, but minimum is 1
},
{
name: "auto_3_open",
maxOpen: 3,
configuredIdle: "auto",
expectedIdle: 1, // 3/3 = 1
},
{
name: "explicit_equal_to_max",
maxOpen: 10,
configuredIdle: "10",
expectedIdle: 10,
},
{
name: "explicit_less_than_max",
maxOpen: 10,
configuredIdle: "5",
expectedIdle: 5,
},
{
name: "explicit_with_whitespace",
maxOpen: 10,
configuredIdle: " 5 ",
expectedIdle: 5,
},
{
name: "explicit_0",
maxOpen: 10,
configuredIdle: "0",
expectedIdle: 0,
},
{
name: "error_exceeds_max",
maxOpen: 10,
configuredIdle: "15",
expectError: true,
errorContains: "cannot exceed",
},
{
name: "error_exceeds_max_by_1",
maxOpen: 10,
configuredIdle: "11",
expectError: true,
errorContains: "cannot exceed",
},
{
name: "error_invalid_string",
maxOpen: 10,
configuredIdle: "invalid",
expectError: true,
errorContains: "must be \"auto\" or >= 0",
},
{
name: "error_negative",
maxOpen: 10,
configuredIdle: "-1",
expectError: true,
errorContains: "must be \"auto\" or >= 0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := codersdk.ComputeMaxIdleConns(tt.maxOpen, tt.configuredIdle)
if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorContains)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedIdle, result)
}
})
}
}