33import logging
44import time
55
6- from datetime import datetime
6+ from collections .abc import Callable
7+ from datetime import datetime , UTC
78from threading import Event , Thread
8- from typing import List , Optional
9+ from typing import Any , Dict , List , Optional , TYPE_CHECKING
910
1011import six
1112
1516from .dcs .kubernetes import catch_kubernetes_errors
1617from .exceptions import DCSError
1718
19+ if TYPE_CHECKING : # pragma: no cover
20+ from .config import Config
21+ from .dcs import Cluster
22+
1823logger = logging .getLogger (__name__ )
1924
2025
@@ -24,19 +29,20 @@ class AbstractSiteController(object):
2429 is_active = False
2530
2631 dcs : AbstractDCS
32+ _has_leader : bool
2733
2834 def start (self ):
2935 pass
3036
3137 def shutdown (self ):
3238 pass
3339
34- def get_active_standby_config (self ) -> Optional [ dict ]:
40+ def get_active_standby_config (self ) -> Dict [ str , Any ]:
3541 """Returns currently active configuration for standby leader"""
3642 return {}
3743
38- def is_leader_site (self ):
39- return self .get_active_standby_config () is None
44+ def is_leader_site (self ) -> bool :
45+ return self .get_active_standby_config () == {}
4046
4147 def resolve_leader (self ) -> Optional [str ]:
4248 """Try to become leader, update active config correspondingly.
@@ -54,13 +60,13 @@ def heartbeat(self):
5460 def release (self ):
5561 pass
5662
57- def status (self ) -> dict :
63+ def status (self ) -> Dict [ str , Any ] :
5864 return {}
5965
6066 def should_failover (self ) -> bool :
6167 return False
6268
63- def on_shutdown (self , checkpoint_location ):
69+ def on_shutdown (self , checkpoint_location : int , prev_location : int ):
6470 pass
6571
6672 def append_metrics (self , metrics : List [str ], labels : str ) -> None :
@@ -76,7 +82,7 @@ def status(self):
7682class MultisiteController (Thread , AbstractSiteController ):
7783 is_active = True
7884
79- def __init__ (self , config , on_change = None ):
85+ def __init__ (self , config : 'Config' , on_change : Callable [[], None ] ):
8086 super ().__init__ ()
8187 self .stop_requested = False
8288 self .on_change = on_change
@@ -116,21 +122,21 @@ def __init__(self, config, on_change=None):
116122 self .switchover_timeout = msconfig .get ('switchover_timeout' , 300 )
117123
118124 self ._heartbeat = Event ()
119- self ._standby_config = None
125+ self ._standby_config = {}
120126 self ._leader_resolved = Event ()
121127 self ._has_leader = False
122128 self ._release = False
123129 self ._status = None
124130 self ._failover_target = None
125- self ._failover_timeout = None
131+ self ._failover_timeout = 0
126132
127133 self .site_switches = None
128134
129135 self ._dcs_error = None
130136
131137 def status (self ):
132138 return {
133- "status" : "Leader" if self ._has_leader or self ._standby_config is None else "Standby" ,
139+ "status" : "Leader" if self ._has_leader or self ._standby_config == {} else "Standby" ,
134140 "active" : True ,
135141 "name" : self .name ,
136142 "standby_config" : self .get_active_standby_config (),
@@ -167,7 +173,7 @@ def release(self):
167173 def should_failover (self ):
168174 return self ._failover_target is not None and self ._failover_target != self .name
169175
170- def on_shutdown (self , checkpoint_location ):
176+ def on_shutdown (self , checkpoint_location : int , prev_location : int ):
171177 """ Called when shutdown for multisite failover has completed.
172178 """
173179 # TODO: check if we replicated everything to standby site
@@ -193,12 +199,11 @@ def _set_standby_config(self, other: Member):
193199 logger .info (f"Setting standby configuration to: { self ._standby_config } " )
194200 return old_conf != self ._standby_config
195201
196- def _check_transition (self , leader , note = None ):
202+ def _check_transition (self , leader : bool , note : str ):
197203 if self ._has_leader != leader :
198204 logger .info ("Multisite state transition" )
199205 self ._has_leader = leader
200- if self .on_change :
201- self .on_change ()
206+ self .on_change ()
202207 if self ._state_updater and self ._status != leader :
203208 self ._state_updater .state_transition ('Leader' if leader else 'Standby' , note )
204209 self ._status = leader
@@ -225,7 +230,7 @@ def _resolve_multisite_leader(self):
225230 # Became leader of unlocked cluster
226231 if self .dcs .attempt_to_acquire_leader ():
227232 logger .info ("Became multisite leader" )
228- self ._standby_config = None
233+ self ._standby_config = {}
229234 self ._check_transition (leader = True , note = "Acquired multisite leader status" )
230235 if cluster .failover and cluster .failover .target_site and cluster .failover .target_site == self .name :
231236 logger .info ("Cleaning up multisite failover key after acquiring leader status" )
@@ -256,7 +261,7 @@ def _resolve_multisite_leader(self):
256261 if self .dcs .update_leader (cluster , None ):
257262 logger .info ("Updated multisite leader lease" )
258263 # Make sure we are disabled from standby mode
259- self ._standby_config = None
264+ self ._standby_config = {}
260265 self ._check_transition (leader = True , note = "Already have multisite leader status" )
261266 self ._check_for_failover (cluster )
262267 else :
@@ -270,8 +275,8 @@ def _resolve_multisite_leader(self):
270275 # Failover successful or someone else took over
271276 if self ._failover_target is not None :
272277 self ._failover_target = None
273- self ._failover_timeout = None
274- if self ._set_standby_config (cluster .leader .member ):
278+ self ._failover_timeout = 0
279+ if cluster . leader and self ._set_standby_config (cluster .leader .member ):
275280 # Wake up anyway to notice that we need to replicate from new leader. For the other case
276281 # _check_transition() handles the wake.
277282 if not self ._has_leader :
@@ -313,15 +318,15 @@ def _observe_leader(self):
313318 # The leader is us
314319 if lock_owner == self .name :
315320 logger .info ("Multisite leader is us" )
316- self ._standby_config = None
321+ self ._standby_config = {}
317322 else :
318323 logger .info (f"Multisite leader is { lock_owner } " )
319- self ._set_standby_config (cluster .leader .member )
324+ self ._set_standby_config (cluster .leader .member ) # pyright: ignore
320325 except DCSError as e :
321326 # On replicas we need to know the multisite status only for rewinding.
322327 logger .warning (f"Error accessing multisite DCS: { e } " )
323328
324- def _update_history (self , cluster ):
329+ def _update_history (self , cluster : 'Cluster' ):
325330 if cluster .history and cluster .history .lines and isinstance (cluster .history .lines [0 ], dict ):
326331 self .site_switches = cluster .history .lines [0 ].get ('switches' )
327332
@@ -349,7 +354,7 @@ def _check_for_failover(self, cluster: Cluster):
349354 self ._failover_target = cluster .failover .target_site
350355 else :
351356 self ._failover_target = None
352- self ._failover_timeout = None
357+ self ._failover_timeout = 0
353358
354359 def touch_member (self ):
355360 data = {
@@ -379,14 +384,14 @@ def shutdown(self):
379384 self ._heartbeat .set ()
380385 self .join ()
381386
382- def append_metrics (self , metrics , labels ):
387+ def append_metrics (self , metrics : List [ str ] , labels : str ):
383388 metrics .append ("# HELP patroni_multisite_switches Number of times multisite leader has been switched" )
384389 metrics .append ("# TYPE patroni_multisite_switches counter" )
385390 metrics .append ("patroni_multisite_switches{0} {1}" .format (labels , self .site_switches ))
386391
387392
388393class KubernetesStateManagement :
389- def __init__ (self , crd_name , crd_uid , reporter , crd_api ):
394+ def __init__ (self , crd_name : str , crd_uid : str , reporter : str , crd_api : str ):
390395 self .crd_namespace , self .crd_name = (['default' ] + crd_name .rsplit ('.' , 1 ))[- 2 :]
391396 self .crd_uid = crd_uid
392397 self .reporter = reporter
@@ -402,13 +407,15 @@ def __init__(self, crd_name, crd_uid, reporter, crd_api):
402407 self ._status_update = None
403408 self ._event_obj = None
404409
405- def state_transition (self , new_state , note ):
410+ def state_transition (self , new_state : str , note : str ):
406411 self ._status_update = {"status" : {"Multisite" : new_state }}
407412
408- failover_time = datetime .utcnow ( ).strftime ("%Y-%m-%dT%H:%M:%S.%fZ" )
413+ failover_time = datetime .now ( UTC ).strftime ("%Y-%m-%dT%H:%M:%S.%fZ" )
409414 reason = 'Promote' if new_state == 'Leader' else 'Demote'
410- if note is None :
411- note = 'Acquired multisite leader' if new_state == 'Leader' else 'Became a standby cluster'
415+
416+ # TODO: check if this is needed, no current call comes without note (this is already reflected in the signature)
417+ # if note is None:
418+ # note = 'Acquired multisite leader' if new_state == 'Leader' else 'Became a standby cluster'
412419
413420 self ._event_obj = kubernetes .client .EventsV1Event (
414421 action = 'Failover' ,
0 commit comments