49
49
PostgreSQL ,
50
50
PostgreSQLEnableDisableExtensionError ,
51
51
PostgreSQLGetCurrentTimelineError ,
52
+ PostgreSQLListGroupsError ,
52
53
PostgreSQLUpdateUserPasswordError ,
53
54
)
54
55
from charms .postgresql_k8s .v0 .postgresql_tls import PostgreSQLTLS
134
135
REPLICATION_OFFER_RELATION ,
135
136
PostgreSQLAsyncReplication ,
136
137
)
138
+ from relations .logical_replication import (
139
+ LOGICAL_REPLICATION_VALIDATION_ERROR_STATUS ,
140
+ PostgreSQLLogicalReplication ,
141
+ )
137
142
from relations .postgresql_provider import PostgreSQLProvider
138
143
from upgrade import PostgreSQLUpgrade , get_postgresql_k8s_dependencies_model
139
144
from utils import any_cpu_to_cores , any_memory_to_bytes , new_password
@@ -250,6 +255,7 @@ def __init__(self, *args):
250
255
self .ldap = PostgreSQLLDAP (self , "ldap" )
251
256
self .tls = PostgreSQLTLS (self , PEER , [self .primary_endpoint , self .replicas_endpoint ])
252
257
self .async_replication = PostgreSQLAsyncReplication (self )
258
+ self .logical_replication = PostgreSQLLogicalReplication (self )
253
259
self .restart_manager = RollingOpsManager (
254
260
charm = self , relation = "restart" , callback = self ._restart
255
261
)
@@ -709,7 +715,7 @@ def _on_secret_changed(self, event: SecretChangedEvent) -> None:
709
715
except PostgreSQLUpdateUserPasswordError :
710
716
event .defer ()
711
717
712
- def _on_config_changed (self , event ) -> None :
718
+ def _on_config_changed (self , event ) -> None : # noqa: C901
713
719
"""Handle configuration changes, like enabling plugins."""
714
720
if not self .is_cluster_initialised :
715
721
logger .debug ("Defer on_config_changed: cluster not initialised yet" )
@@ -744,6 +750,9 @@ def _on_config_changed(self, event) -> None:
744
750
# Update the sync-standby endpoint in the async replication data.
745
751
self .async_replication .update_async_replication_data ()
746
752
753
+ if not self .logical_replication .apply_changed_config (event ):
754
+ return
755
+
747
756
if not self .unit .is_leader ():
748
757
return
749
758
@@ -1097,6 +1106,12 @@ def _set_active_status(self):
1097
1106
self .app_peer_data ["s3-initialization-block-message" ]
1098
1107
)
1099
1108
return
1109
+ if self .unit .is_leader () and (
1110
+ self .app_peer_data .get ("logical-replication-validation" ) == "error"
1111
+ or self .logical_replication .has_remote_publisher_errors ()
1112
+ ):
1113
+ self .unit .status = BlockedStatus (LOGICAL_REPLICATION_VALIDATION_ERROR_STATUS )
1114
+ return
1100
1115
if (
1101
1116
self ._patroni .get_primary (unit_name_pattern = True ) == self .unit .name
1102
1117
or self .is_standby_leader
@@ -1488,7 +1503,9 @@ def _on_update_status_early_exit_checks(self, container) -> bool:
1488
1503
self ._check_pgdata_storage_size ()
1489
1504
1490
1505
if (
1491
- self ._has_blocked_status and self .unit .status not in S3_BLOCK_MESSAGES
1506
+ self ._has_blocked_status
1507
+ and self .unit .status not in S3_BLOCK_MESSAGES
1508
+ and self .unit .status .message != LOGICAL_REPLICATION_VALIDATION_ERROR_STATUS
1492
1509
) or self ._has_non_restore_waiting_status :
1493
1510
# If charm was failing to disable plugin, try again and continue (user may have removed the objects)
1494
1511
if self .unit .status .message == EXTENSION_OBJECT_MESSAGE :
@@ -1565,6 +1582,8 @@ def _on_update_status(self, _) -> None:
1565
1582
1566
1583
self .backup .coordinate_stanza_fields ()
1567
1584
1585
+ self .logical_replication .retry_validations ()
1586
+
1568
1587
self ._set_active_status ()
1569
1588
1570
1589
def _was_restore_successful (self , container : Container , service : ServiceInfo ) -> bool :
@@ -2078,6 +2097,8 @@ def update_config(self, is_creating_backup: bool = False) -> bool:
2078
2097
self .model .config , available_memory , limit_memory
2079
2098
)
2080
2099
2100
+ replication_slots = self .logical_replication .replication_slots ()
2101
+
2081
2102
logger .info ("Updating Patroni config file" )
2082
2103
# Update and reload configuration based on TLS files availability.
2083
2104
self ._patroni .render_patroni_yml_file (
@@ -2094,6 +2115,7 @@ def update_config(self, is_creating_backup: bool = False) -> bool:
2094
2115
restore_stanza = self .app_peer_data .get ("restore-stanza" ),
2095
2116
parameters = postgresql_parameters ,
2096
2117
user_databases_map = self .relations_user_databases_map ,
2118
+ slots = replication_slots or None ,
2097
2119
)
2098
2120
2099
2121
if not self ._is_workload_running :
@@ -2133,6 +2155,8 @@ def update_config(self, is_creating_backup: bool = False) -> bool:
2133
2155
"wal_keep_size" : self .config .durability_wal_keep_size ,
2134
2156
})
2135
2157
2158
+ self ._patroni .ensure_slots_controller_by_patroni (replication_slots )
2159
+
2136
2160
self ._handle_postgresql_restart_need ()
2137
2161
self ._restart_metrics_service ()
2138
2162
self ._restart_ldap_sync_service ()
@@ -2323,22 +2347,30 @@ def client_relations(self) -> list[Relation]:
2323
2347
@property
2324
2348
def relations_user_databases_map (self ) -> dict :
2325
2349
"""Returns a user->databases map for all relations."""
2326
- if (
2327
- not self .is_cluster_initialised
2328
- or not self ._patroni .member_started
2329
- or self .postgresql .list_access_groups (current_host = self .is_connectivity_enabled )
2330
- != set (ACCESS_GROUPS )
2331
- ):
2350
+ try :
2351
+ if (
2352
+ not self .is_cluster_initialised
2353
+ or not self ._patroni .member_started
2354
+ or self .postgresql .list_access_groups (current_host = self .is_connectivity_enabled )
2355
+ != set (ACCESS_GROUPS )
2356
+ ):
2357
+ return {USER : "all" , REPLICATION_USER : "all" , REWIND_USER : "all" }
2358
+ except PostgreSQLListGroupsError as e :
2359
+ logger .warning (f"Failed to list access groups: { e } " )
2332
2360
return {USER : "all" , REPLICATION_USER : "all" , REWIND_USER : "all" }
2333
2361
user_database_map = {}
2334
2362
for user in self .postgresql .list_users_from_relation (
2335
2363
current_host = self .is_connectivity_enabled
2336
2364
):
2337
- user_database_map [ user ] = "," .join (
2365
+ databases = "," .join (
2338
2366
self .postgresql .list_accessible_databases_for_user (
2339
2367
user , current_host = self .is_connectivity_enabled
2340
2368
)
2341
2369
)
2370
+ if databases :
2371
+ user_database_map [user ] = databases
2372
+ else :
2373
+ logger .debug (f"User { user } has no databases to connect to" )
2342
2374
2343
2375
# Copy relations users directly instead of waiting for them to be created
2344
2376
for relation in self .model .relations [self .postgresql_client_relation .relation_name ]:
0 commit comments