@@ -54,6 +54,11 @@ func (r *Reconciler) reconcilePGBouncer(
5454 if err == nil {
5555 err = r .reconcilePGBouncerInPostgreSQL (ctx , cluster , instances , secret )
5656 }
57+ if err == nil {
58+ // Trigger RECONNECT if primary has changed to force new server connections.
59+ // This prevents stale connections from routing traffic to a demoted replica.
60+ err = r .reconcilePGBouncerReconnect (ctx , cluster , instances )
61+ }
5762 return err
5863}
5964
@@ -584,3 +589,100 @@ func (r *Reconciler) reconcilePGBouncerPodDisruptionBudget(
584589 }
585590 return err
586591}
592+
593+ // reconcilePGBouncerReconnect triggers a RECONNECT command on all PgBouncer
594+ // pods when the primary has changed. This forces PgBouncer to establish new
595+ // server connections to the correct primary, preventing stale connections
596+ // from routing traffic to a demoted replica after failover.
597+ //
598+ // Note: RECONNECT closes server connections when they are "released" according
599+ // to the pool mode. In transaction mode, this happens after each transaction.
600+ // In session mode, this happens when the client disconnects - so persistent
601+ // clients may continue hitting the old primary until they reconnect.
602+ func (r * Reconciler ) reconcilePGBouncerReconnect (
603+ ctx context.Context , cluster * v1beta1.PostgresCluster ,
604+ instances * observedInstances ,
605+ ) error {
606+ log := logging .FromContext (ctx )
607+
608+ // Skip if PgBouncer is disabled
609+ if cluster .Spec .Proxy == nil || cluster .Spec .Proxy .PGBouncer == nil {
610+ return nil
611+ }
612+
613+ var primaryPod * corev1.Pod
614+ for _ , instance := range instances .forCluster {
615+ // Same condition as writablePod fn
616+ if writable , known := instance .IsWritable (); writable && known && len (instance .Pods ) > 0 {
617+ primaryPod = instance .Pods [0 ]
618+ break
619+ }
620+ }
621+
622+ if primaryPod == nil {
623+ // We will retry later.
624+ log .V (1 ).Info ("No writable instance found, skipping PgBouncer RECONNECT" )
625+ return nil
626+ }
627+
628+ currentPrimaryUID := string (primaryPod .UID )
629+ lastReconnectUID := cluster .Status .Proxy .PGBouncer .LastReconnectPrimaryUID
630+
631+ if currentPrimaryUID == lastReconnectUID {
632+ // Primary hasn't changed, no need to Reconnect.
633+ return nil
634+ }
635+
636+ log .Info ("Primary changed, triggering PgBouncer RECONNECT" ,
637+ "previousPrimaryUID" , lastReconnectUID ,
638+ "currentPrimaryUID" , currentPrimaryUID ,
639+ "currentPrimaryName" , primaryPod .Name )
640+
641+ pgbouncerPods := & corev1.PodList {}
642+ selector , err := naming .AsSelector (naming .ClusterPGBouncerSelector (cluster ))
643+ if err != nil {
644+ return errors .WithStack (err )
645+ }
646+
647+ if err := r .Client .List (ctx , pgbouncerPods ,
648+ client .InNamespace (cluster .Namespace ),
649+ client.MatchingLabelsSelector {Selector : selector }); err != nil {
650+ return errors .WithStack (err )
651+ }
652+
653+ // Send RECONNECT to each running PgBouncer pod
654+ var reconnectErr error
655+ successCount := 0
656+
657+ for i := range pgbouncerPods .Items {
658+ pod := & pgbouncerPods .Items [i ]
659+ if pod .Status .Phase != corev1 .PodRunning {
660+ continue
661+ }
662+
663+ exec := func (ctx context.Context , stdin io.Reader , stdout , stderr io.Writer , command ... string ) error {
664+ return r .PodExec (ctx , pod .Namespace , pod .Name , naming .ContainerPGBouncer , stdin , stdout , stderr , command ... )
665+ }
666+
667+ if err := pgbouncer .Reconnect (ctx , exec ); err != nil {
668+ log .Error (err , "PgBouncer RECONNECT: failed to issue command to pod." , "pod" , pod .Name )
669+ reconnectErr = err
670+ } else {
671+ successCount ++
672+ }
673+ }
674+
675+ // If we can't send a RECONNECT command to one of the pods, we won't update the LastReconnectPrimaryUID.
676+ // This means this will run again in the next reconciliation loop.
677+ if reconnectErr == nil {
678+ cluster .Status .Proxy .PGBouncer .LastReconnectPrimaryUID = currentPrimaryUID
679+ }
680+
681+ log .Info ("PgBouncer RECONNECT: done" ,
682+ "failed" , reconnectErr != nil ,
683+ "successCount" , successCount ,
684+ "totalPods" , len (pgbouncerPods .Items ),
685+ )
686+
687+ return reconnectErr
688+ }
0 commit comments