Skip to content

Commit f747ab7

Browse files
[DPE-1781] Configuration support (#239)
* Add missing parameters and add configuration support * Use fixed snap * Fix parameters validation * Add validations * Update library * Revert changes on charm.py * Add configuration logic and tests * PR feedback (including the feedback from canonical/postgresql-k8s-operator#281) * Fix TLS tests * Increase timeout
1 parent 1a1540a commit f747ab7

File tree

13 files changed

+595
-44
lines changed

13 files changed

+595
-44
lines changed

config.yaml

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,103 @@
22
# See LICENSE file for licensing details.
33

44
options:
5+
durability_synchronous_commit:
6+
description: |
7+
Sets the current transactions synchronization level. This charm allows only the
8+
“on”, “remote_apply” and “remote_write” values to avoid data loss if the primary
9+
crashes and there are replicas.
10+
type: string
11+
default: "on"
12+
instance_default_text_search_config:
13+
description: |
14+
Selects the text search configuration that is used by those variants of the text
15+
search functions that do not have an explicit argument specifying it.
16+
Allowed values start with “pg_catalog.” followed by a language name, like
17+
“pg_catalog.english”.
18+
type: string
19+
default: "pg_catalog.simple"
20+
instance_password_encryption:
21+
description: |
22+
Determines the algorithm to use to encrypt the password.
23+
Allowed values are: “md5” and “scram-sha-256”.
24+
type: string
25+
default: "scram-sha-256"
26+
logging_log_connections:
27+
description: |
28+
Logs each successful connection.
29+
type: boolean
30+
default: false
31+
logging_log_disconnections:
32+
description: |
33+
Logs end of a session, including duration.
34+
type: boolean
35+
default: false
36+
logging_log_lock_waits:
37+
description: |
38+
Logs long lock waits.
39+
type: boolean
40+
default: false
41+
logging_log_min_duration_statement:
42+
description: |
43+
Sets the minimum running time (milliseconds) above which statements will be logged.
44+
Allowed values are: from -1 to 2147483647 (-1 disables logging
45+
statement durations).
46+
type: int
47+
default: -1
48+
memory_maintenance_work_mem:
49+
description: |
50+
Sets the maximum memory (KB) to be used for maintenance operations.
51+
Allowed values are: from 1024 to 2147483647.
52+
type: int
53+
default: 65536
54+
memory_max_prepared_transactions:
55+
description: |
56+
Sets the maximum number of simultaneously prepared transactions.
57+
Allowed values are: from 0 to 262143.
58+
type: int
59+
default: 0
60+
memory_shared_buffers:
61+
description: |
62+
Sets the number of shared memory buffers (8 kB) used by the server. This charm allows
63+
to set this value up to 40% of the available memory from the unit, as it is unlikely
64+
that an allocation of more than that will work better than a smaller amount.
65+
Allowed values are: from 16 to 1073741823.
66+
type: int
67+
memory_temp_buffers:
68+
description: |
69+
Sets the maximum number of temporary buffers (8 kB) used by each session.
70+
Allowed values are: from 100 to 1073741823.
71+
type: int
72+
default: 1024
73+
memory_work_mem:
74+
description: |
75+
Sets the maximum memory (KB) to be used for query workspaces.
76+
Allowed values are: from 64 to 2147483647.
77+
type: int
78+
default: 4096
79+
optimizer_constraint_exclusion:
80+
description: |
81+
Enables the planner to use constraints to optimize queries.
82+
Allowed values are: “on”, “off” and “partition”.
83+
type: string
84+
default: "partition"
85+
optimizer_default_statistics_target:
86+
description: |
87+
Sets the default statistics target. Allowed values are: from 1 to 10000.
88+
type: int
89+
default: 100
90+
optimizer_from_collapse_limit:
91+
description: |
92+
Sets the FROM-list size beyond which subqueries are not collapsed.
93+
Allowed values are: from 1 to 2147483647.
94+
type: int
95+
default: 8
96+
optimizer_join_collapse_limit:
97+
description: |
98+
Sets the FROM-list size beyond which JOIN constructs are not flattened.
99+
Allowed values are: from 1 to 2147483647.
100+
type: int
101+
default: 8
5102
plugin_citext_enable:
6103
default: false
7104
type: boolean
@@ -40,3 +137,84 @@ options:
40137
Amount of memory in Megabytes to limit PostgreSQL and associated process to.
41138
If unset, this will be decided according to the default memory limit in the selected profile.
42139
Only comes into effect when the `production` profile is selected.
140+
request_date_style:
141+
description: |
142+
Sets the display format for date and time values. Allowed formats are explained
143+
in https://www.postgresql.org/docs/14/runtime-config-client.html#GUC-DATESTYLE.
144+
type: string
145+
default: "ISO, MDY"
146+
request_standard_conforming_strings:
147+
description: |
148+
Causes ... strings to treat backslashes literally.
149+
type: boolean
150+
default: true
151+
request_time_zone:
152+
description: |
153+
Sets the time zone for displaying and interpreting time stamps.
154+
Allowed values are the ones from IANA time zone data, a time zone abbreviation
155+
like PST and POSIX-style time zone specifications.
156+
type: string
157+
default: "UTC"
158+
response_bytea_output:
159+
description: |
160+
Sets the output format for bytes.
161+
Allowed values are: “escape” and “hex”.
162+
type: string
163+
default: "hex"
164+
response_lc_monetary:
165+
description: |
166+
Sets the locale for formatting monetary amounts.
167+
Allowed values are the locales available in the unit.
168+
type: string
169+
default: "C"
170+
response_lc_numeric:
171+
description: |
172+
Sets the locale for formatting numbers.
173+
Allowed values are the locales available in the unit.
174+
type: string
175+
default: "C"
176+
response_lc_time:
177+
description: |
178+
Sets the locale for formatting date and time values.
179+
Allowed values are the locales available in the unit.
180+
type: string
181+
default: "C"
182+
vacuum_autovacuum_analyze_scale_factor:
183+
description: |
184+
Specifies a fraction of the table size to add to autovacuum_vacuum_threshold when
185+
deciding whether to trigger a VACUUM. The default, 0.1, means 10% of table size.
186+
Allowed values are: from 0 to 100.
187+
type: float
188+
default: 0.1
189+
vacuum_autovacuum_analyze_threshold:
190+
description: |
191+
Sets the minimum number of inserted, updated or deleted tuples needed to trigger
192+
an ANALYZE in any one table. Allowed values are: from 0 to 2147483647.
193+
type: int
194+
default: 50
195+
vacuum_autovacuum_freeze_max_age:
196+
description: |
197+
Maximum age (in transactions) before triggering autovacuum on a table to prevent
198+
transaction ID wraparound. Allowed values are: from 100000 to 2000000000.
199+
type: int
200+
default: 200000000
201+
vacuum_autovacuum_vacuum_cost_delay:
202+
description: |
203+
Sets cost delay value (milliseconds) that will be used in automatic VACUUM operations.
204+
Allowed values are: from -1 to 100 (-1 tells PostgreSQL to use the regular
205+
vacuum_cost_delay value).
206+
type: float
207+
default: 2.0
208+
vacuum_autovacuum_vacuum_scale_factor:
209+
description: |
210+
Specifies a fraction of the table size to add to autovacuum_vacuum_threshold when
211+
deciding whether to trigger a VACUUM. The default, 0.2, means 20% of table size.
212+
Allowed values are: from 0 to 100.
213+
type: float
214+
default: 0.2
215+
vacuum_vacuum_freeze_table_age:
216+
description: |
217+
Age (in transactions) at which VACUUM should scan whole table to freeze tuples.
218+
Allowed values are: from 0 to 2000000000.
219+
type: int
220+
default: 150000000

lib/charms/postgresql_k8s/v0/postgresql.py

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
# Increment this PATCH version before using `charmcraft publish-lib` or reset
3434
# to 0 if you are raising the major API version
35-
LIBPATCH = 17
35+
LIBPATCH = 18
3636

3737
INVALID_EXTRA_USER_ROLE_BLOCKING_MESSAGE = "invalid role(s) for extra user roles"
3838

@@ -310,6 +310,32 @@ def enable_disable_extension(self, extension: str, enable: bool, database: str =
310310
if connection is not None:
311311
connection.close()
312312

313+
def get_postgresql_text_search_configs(self) -> Set[str]:
314+
"""Returns the PostgreSQL available text search configs.
315+
316+
Returns:
317+
Set of PostgreSQL text search configs.
318+
"""
319+
with self._connect_to_database(
320+
connect_to_current_host=True
321+
) as connection, connection.cursor() as cursor:
322+
cursor.execute("SELECT CONCAT('pg_catalog.', cfgname) FROM pg_ts_config;")
323+
text_search_configs = cursor.fetchall()
324+
return {text_search_config[0] for text_search_config in text_search_configs}
325+
326+
def get_postgresql_timezones(self) -> Set[str]:
327+
"""Returns the PostgreSQL available timezones.
328+
329+
Returns:
330+
Set of PostgreSQL timezones.
331+
"""
332+
with self._connect_to_database(
333+
connect_to_current_host=True
334+
) as connection, connection.cursor() as cursor:
335+
cursor.execute("SELECT name FROM pg_timezone_names;")
336+
timezones = cursor.fetchall()
337+
return {timezone[0] for timezone in timezones}
338+
313339
def get_postgresql_version(self) -> str:
314340
"""Returns the PostgreSQL version.
315341
@@ -445,12 +471,12 @@ def is_restart_pending(self) -> bool:
445471

446472
@staticmethod
447473
def build_postgresql_parameters(
448-
profile: str, available_memory: int, limit_memory: Optional[int] = None
449-
) -> Optional[Dict[str, str]]:
474+
config_options: Dict, available_memory: int, limit_memory: Optional[int] = None
475+
) -> Optional[Dict]:
450476
"""Builds the PostgreSQL parameters.
451477
452478
Args:
453-
profile: the profile to use.
479+
config_options: charm config options containing profile and PostgreSQL parameters.
454480
available_memory: available memory to use in calculation in bytes.
455481
limit_memory: (optional) limit memory to use in calculation in bytes.
456482
@@ -459,19 +485,60 @@ def build_postgresql_parameters(
459485
"""
460486
if limit_memory:
461487
available_memory = min(available_memory, limit_memory)
488+
profile = config_options["profile"]
462489
logger.debug(f"Building PostgreSQL parameters for {profile=} and {available_memory=}")
490+
parameters = {}
491+
for config, value in config_options.items():
492+
# Filter config option not related to PostgreSQL parameters.
493+
if not config.startswith(
494+
(
495+
"durability",
496+
"instance",
497+
"logging",
498+
"memory",
499+
"optimizer",
500+
"request",
501+
"response",
502+
"vacuum",
503+
)
504+
):
505+
continue
506+
parameter = "_".join(config.split("_")[1:])
507+
if parameter in ["date_style", "time_zone"]:
508+
parameter = "".join(x.capitalize() for x in parameter.split("_"))
509+
parameters[parameter] = value
510+
shared_buffers_max_value = int(int(available_memory * 0.4) / 10**6)
511+
if parameters.get("shared_buffers", 0) > shared_buffers_max_value:
512+
raise Exception(
513+
f"Shared buffers config option should be at most 40% of the available memory, which is {shared_buffers_max_value}MB"
514+
)
463515
if profile == "production":
464516
# Use 25% of the available memory for shared_buffers.
465517
# and the remaind as cache memory.
466518
shared_buffers = int(available_memory * 0.25)
467519
effective_cache_size = int(available_memory - shared_buffers)
468-
469-
parameters = {
470-
"shared_buffers": f"{int(shared_buffers/10**6)}MB",
471-
"effective_cache_size": f"{int(effective_cache_size/10**6)}MB",
472-
}
473-
474-
return parameters
520+
parameters.setdefault("shared_buffers", f"{int(shared_buffers/10**6)}MB")
521+
parameters.update({"effective_cache_size": f"{int(effective_cache_size/10**6)}MB"})
475522
else:
476523
# Return default
477-
return {"shared_buffers": "128MB"}
524+
parameters.setdefault("shared_buffers", "128MB")
525+
return parameters
526+
527+
def validate_date_style(self, date_style: str) -> bool:
528+
"""Validate a date style against PostgreSQL.
529+
530+
Returns:
531+
Whether the date style is valid.
532+
"""
533+
try:
534+
with self._connect_to_database(
535+
connect_to_current_host=True
536+
) as connection, connection.cursor() as cursor:
537+
cursor.execute(
538+
sql.SQL(
539+
"SET DateStyle to {};",
540+
).format(sql.Identifier(date_style))
541+
)
542+
return True
543+
except psycopg2.Error:
544+
return False

src/charm.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"""Charmed Machine Operator for the PostgreSQL database."""
66
import json
77
import logging
8+
import os
89
import subprocess
10+
import time
911
from typing import Dict, List, Optional, Set
1012

1113
from charms.data_platform_libs.v0.data_models import TypedCharmBase
@@ -1411,14 +1413,14 @@ def _is_workload_running(self) -> bool:
14111413

14121414
def update_config(self, is_creating_backup: bool = False) -> bool:
14131415
"""Updates Patroni config file based on the existence of the TLS files."""
1414-
enable_tls = all(self.tls.get_tls_files())
1416+
enable_tls = self.is_tls_enabled
14151417
limit_memory = None
14161418
if self.config.profile_limit_memory:
14171419
limit_memory = self.config.profile_limit_memory * 10**6
14181420

14191421
# Build PostgreSQL parameters.
14201422
pg_parameters = self.postgresql.build_postgresql_parameters(
1421-
self.config.profile, self.get_available_memory(), limit_memory
1423+
self.model.config, self.get_available_memory(), limit_memory
14221424
)
14231425

14241426
# Update and reload configuration based on TLS files availability.
@@ -1444,10 +1446,21 @@ def update_config(self, is_creating_backup: bool = False) -> bool:
14441446
logger.debug("Early exit update_config: Patroni not started yet")
14451447
return False
14461448

1447-
restart_postgresql = (
1448-
enable_tls != self.postgresql.is_tls_enabled()
1449-
) or self.postgresql.is_restart_pending()
1449+
self._validate_config_options()
1450+
1451+
self._patroni.update_parameter_controller_by_patroni(
1452+
"max_connections", max(4 * os.cpu_count(), 100)
1453+
)
1454+
self._patroni.update_parameter_controller_by_patroni(
1455+
"max_prepared_transactions", self.config.memory_max_prepared_transactions
1456+
)
1457+
1458+
restart_postgresql = self.is_tls_enabled != self.postgresql.is_tls_enabled()
14501459
self._patroni.reload_patroni_configuration()
1460+
# Sleep the same time as Patroni's loop_wait default value, which tells how much time
1461+
# Patroni will wait before checking the configuration file again to reload it.
1462+
time.sleep(10)
1463+
restart_postgresql = restart_postgresql or self.postgresql.is_restart_pending()
14511464
self.unit_peer_data.update({"tls": "enabled" if enable_tls else ""})
14521465

14531466
# Restart PostgreSQL if TLS configuration has changed
@@ -1473,6 +1486,28 @@ def update_config(self, is_creating_backup: bool = False) -> bool:
14731486

14741487
return True
14751488

1489+
def _validate_config_options(self) -> None:
1490+
"""Validates specific config options that need access to the database or to the TLS status."""
1491+
if (
1492+
self.config.instance_default_text_search_config is not None
1493+
and self.config.instance_default_text_search_config
1494+
not in self.postgresql.get_postgresql_text_search_configs()
1495+
):
1496+
raise Exception(
1497+
"instance_default_text_search_config config option has an invalid value"
1498+
)
1499+
1500+
if self.config.request_date_style is not None and not self.postgresql.validate_date_style(
1501+
self.config.request_date_style
1502+
):
1503+
raise Exception("request_date_style config option has an invalid value")
1504+
1505+
if (
1506+
self.config.request_time_zone is not None
1507+
and self.config.request_time_zone not in self.postgresql.get_postgresql_timezones()
1508+
):
1509+
raise Exception("request_time_zone config option has an invalid value")
1510+
14761511
def _update_relation_endpoints(self) -> None:
14771512
"""Updates endpoints and read-only endpoint in all relations."""
14781513
self.postgresql_client_relation.update_endpoints()

0 commit comments

Comments
 (0)