Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ options:
type: int
description: |
[EXPERIMENTAL] Force set max_connections.
wal_compression:
description: |
Enables compression of full-page writes written to WAL.
Compression can reduce the volume of WAL written and improve I/O performance.
Allowed values are: "on" or "off".
type: string
default: "on"
instance_default_text_search_config:
description: |
Selects the text search configuration that is used by those variants of the text
Expand Down Expand Up @@ -148,6 +155,49 @@ options:
Allowed values are: from 64 to 2147483647.
type: int
default: 4096
max_worker_processes:
description: |
Sets the maximum number of background processes that the system can support.
Allowed values are: "auto" or a number.
When set to "auto", defaults to minimum(8, 2*vCores).
If a number is provided, it will be capped to 10 * vCores with a warning logged if exceeded.
type: string
default: "auto"
max_parallel_workers:
description: |
Sets the maximum number of workers that can be started for parallel operations.
Allowed values are: "auto" or a number.
When set to "auto", defaults to max_worker_processes.
type: string
default: "auto"
max_parallel_maintenance_workers:
description: |
Sets the maximum number of parallel workers for maintenance operations.
Allowed values are: "auto" or a number.
When set to "auto", defaults to max_worker_processes.
type: string
default: "auto"
max_logical_replication_workers:
description: |
Sets the maximum number of logical replication workers.
Allowed values are: "auto" or a number.
When set to "auto", defaults to max_worker_processes.
type: string
default: "auto"
max_sync_workers_per_subscription:
description: |
Sets the maximum number of synchronization workers per subscription.
Allowed values are: "auto" or a number.
When set to "auto", defaults to max_worker_processes.
type: string
default: "auto"
max_parallel_apply_workers_per_subscription:
description: |
Sets the maximum number of parallel apply workers per subscription.
Allowed values are: "auto" or a number.
When set to "auto", defaults to max_worker_processes.
type: string
default: "auto"
optimizer_constraint_exclusion:
description: |
Enables the planner to use constraints to optimize queries.
Expand Down
79 changes: 78 additions & 1 deletion lib/charms/postgresql_k8s/v0/postgresql.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,14 +897,18 @@ def build_postgresql_group_map(group_map: Optional[str]) -> List[Tuple]:

@staticmethod
def build_postgresql_parameters(
config_options: dict, available_memory: int, limit_memory: Optional[int] = None
config_options: dict,
available_memory: int,
limit_memory: Optional[int] = None,
available_cores: Optional[int] = None,
) -> Optional[dict]:
"""Builds the PostgreSQL parameters.

Args:
config_options: charm config options containing profile and PostgreSQL parameters.
available_memory: available memory to use in calculation in bytes.
limit_memory: (optional) limit memory to use in calculation in bytes.
available_cores: (optional) number of available CPU cores for worker process calculations.

Returns:
Dictionary with the PostgreSQL parameters.
Expand All @@ -915,6 +919,19 @@ def build_postgresql_parameters(
logger.debug(f"Building PostgreSQL parameters for {profile=} and {available_memory=}")
parameters = {}
for config, value in config_options.items():
# Handle direct PostgreSQL parameter names (no prefix)
if config in [
"max_worker_processes",
"max_parallel_workers",
"max_parallel_maintenance_workers",
"max_logical_replication_workers",
"max_sync_workers_per_subscription",
"max_parallel_apply_workers_per_subscription",
"wal_compression",
]:
parameters[config] = value
continue

# Filter config option not related to PostgreSQL parameters.
if not config.startswith((
"connection",
Expand All @@ -935,6 +952,66 @@ def build_postgresql_parameters(
if parameter in ["date_style", "time_zone"]:
parameter = "".join(x.capitalize() for x in parameter.split("_"))
parameters[parameter] = value

# Handle worker process parameters with "auto" support
if available_cores is not None:
# Calculate max_worker_processes first (needed as base for other workers)
max_worker_processes_value = None
if "max_worker_processes" in parameters:
if parameters["max_worker_processes"] == "auto":
# auto = minimum(8, 2*vCores)
max_worker_processes_value = min(8, 2 * available_cores)
parameters["max_worker_processes"] = max_worker_processes_value
else:
# It's a number - validate minimum and maximum
max_worker_processes_value = int(parameters["max_worker_processes"])
if max_worker_processes_value < 0:
raise ValueError(
f"max_worker_processes value {max_worker_processes_value} is below "
f"minimum allowed value of 0."
)
max_allowed = 10 * available_cores
if max_worker_processes_value > max_allowed:
raise ValueError(
f"max_worker_processes value {max_worker_processes_value} exceeds "
f"maximum allowed limit of {max_allowed} (10 * vCores)."
)
parameters["max_worker_processes"] = max_worker_processes_value

# Handle other worker parameters that default to max_worker_processes
worker_params = [
"max_parallel_workers",
"max_parallel_maintenance_workers",
"max_logical_replication_workers",
"max_sync_workers_per_subscription",
"max_parallel_apply_workers_per_subscription",
]

for worker_param in worker_params:
if worker_param in parameters:
if parameters[worker_param] == "auto":
# auto = max_worker_processes
if max_worker_processes_value is not None:
parameters[worker_param] = max_worker_processes_value
else:
# Fallback if max_worker_processes not configured
parameters[worker_param] = min(8, 2 * available_cores)
else:
# It's a number - validate minimum and maximum
worker_value = int(parameters[worker_param])
if worker_value < 0:
raise ValueError(
f"{worker_param} value {worker_value} is below "
f"minimum allowed value of 0."
)
max_allowed = 10 * available_cores
if worker_value > max_allowed:
raise ValueError(
f"{worker_param} value {worker_value} exceeds "
f"maximum allowed limit of {max_allowed} (10 * vCores)."
)
parameters[worker_param] = worker_value

shared_buffers_max_value_in_mb = int(available_memory * 0.4 / 10**6)
shared_buffers_max_value = int(shared_buffers_max_value_in_mb * 10**3 / 8)
if parameters.get("shared_buffers", 0) > shared_buffers_max_value:
Expand Down
18 changes: 17 additions & 1 deletion src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2000,7 +2000,10 @@ def update_config(self, is_creating_backup: bool = False, no_peers: bool = False

# Build PostgreSQL parameters.
pg_parameters = self.postgresql.build_postgresql_parameters(
self.model.config, self.get_available_memory(), limit_memory
self.model.config,
self.get_available_memory(),
limit_memory,
self.get_available_cores(),
)

# Update and reload configuration based on TLS files availability.
Expand Down Expand Up @@ -2152,6 +2155,19 @@ def get_available_memory(self) -> int:

return 0

def get_available_cores(self) -> int:
"""Returns the number of available CPU cores.

This method uses os.cpu_count() which returns the number of logical CPUs
available to the system. This is appropriate for calculating resource
limits for PostgreSQL worker processes.

Returns:
Number of available CPU cores, or 1 as fallback.
"""
cores = os.cpu_count()
return cores if cores is not None else 1

@property
def client_relations(self) -> list[Relation]:
"""Return the list of established client relations."""
Expand Down
44 changes: 44 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class CharmConfig(BaseConfigModel):
durability_synchronous_commit: str | None
durability_wal_keep_size: int | None
experimental_max_connections: int | None
wal_compression: str | None
instance_default_text_search_config: str | None
instance_max_locks_per_transaction: int | None
instance_password_encryption: str | None
Expand All @@ -42,6 +43,12 @@ class CharmConfig(BaseConfigModel):
memory_shared_buffers: int | None
memory_temp_buffers: int | None
memory_work_mem: int | None
max_worker_processes: str | None
max_parallel_workers: str | None
max_parallel_maintenance_workers: str | None
max_logical_replication_workers: str | None
max_sync_workers_per_subscription: str | None
max_parallel_apply_workers_per_subscription: str | None
optimizer_constraint_exclusion: str | None
optimizer_cpu_index_tuple_cost: float | None
optimizer_cpu_operator_cost: float | None
Expand Down Expand Up @@ -289,6 +296,43 @@ def memory_work_mem_values(cls, value: int) -> int | None:

return value

@validator("wal_compression")
@classmethod
def wal_compression_values(cls, value: str) -> str | None:
"""Check wal_compression config option is one of `on` or `off`."""
if value not in ["on", "off"]:
raise ValueError("Value not one of 'on' or 'off'")

return value

@validator(
"max_worker_processes",
"max_parallel_workers",
"max_parallel_maintenance_workers",
"max_logical_replication_workers",
"max_sync_workers_per_subscription",
"max_parallel_apply_workers_per_subscription",
)
@classmethod
def worker_values(cls, value: str) -> str | None:
"""Check worker config options are 'auto' or a valid number.

Note: The actual cap validation (10 * vCores) is performed at runtime
in the charm layer where CPU information is available.
"""
if value == "auto":
return value

# Validate that if not "auto", it's a valid positive integer
try:
num_value = int(value)
if num_value < 0:
raise ValueError("Value must be 'auto' or a positive number")
except ValueError as e:
raise ValueError("Value must be 'auto' or a valid number") from e

return value

@validator("optimizer_constraint_exclusion")
@classmethod
def optimizer_constraint_exclusion_values(cls, value: str) -> str | None:
Expand Down
27 changes: 27 additions & 0 deletions tests/integration/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ async def test_config_parameters(ops_test: OpsTest, charm) -> None:
"""Build and deploy one unit of PostgreSQL and then test config with wrong parameters."""
# Build and deploy the PostgreSQL charm.
async with ops_test.fast_forward():
# Get the current system architecture for deployment constraints
import subprocess

arch = subprocess.run(
["dpkg", "--print-architecture"], capture_output=True, check=True, encoding="utf-8"
).stdout.strip()

await ops_test.model.deploy(
charm,
num_units=1,
base=CHARM_BASE,
config={"profile": "testing"},
constraints=f"arch={arch}",
)
await ops_test.model.wait_for_idle(apps=[DATABASE_APP_NAME], status="active", timeout=1500)

Expand Down Expand Up @@ -211,6 +219,25 @@ async def test_config_parameters(ops_test: OpsTest, charm) -> None:
{
"vacuum_vacuum_multixact_freeze_table_age": ["-1", "150000000"]
}, # config option is between 0 and 2000000000
{"wal_compression": [test_string, "on"]}, # config option is one of `on` or `off`
{
"max_worker_processes": [test_string, "auto"]
}, # config option is `auto` or a valid number
{
"max_parallel_workers": [test_string, "auto"]
}, # config option is `auto` or a valid number
{
"max_parallel_maintenance_workers": [test_string, "auto"]
}, # config option is `auto` or a valid number
{
"max_logical_replication_workers": [test_string, "auto"]
}, # config option is `auto` or a valid number
{
"max_sync_workers_per_subscription": [test_string, "auto"]
}, # config option is `auto` or a valid number
{
"max_parallel_apply_workers_per_subscription": [test_string, "auto"]
}, # config option is `auto` or a valid number
]

charm_config = {}
Expand Down
Loading