mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user