@@ -55,7 +55,8 @@ func (r *Reconciler) reconcilePGBouncer(
5555 err = r .reconcilePGBouncerInPostgreSQL (ctx , cluster , instances , secret )
5656 }
5757 if err == nil {
58- // Trigger RECONNECT if primary has changed to force new server connections.
58+ // Send SIGTERM to PgBouncer if primary has changed, triggering graceful
59+ // shutdown and container restart. New process will do fresh DNS lookup.
5960 // This prevents stale connections from routing traffic to a demoted replica.
6061 err = r .reconcilePGBouncerReconnect (ctx , cluster , instances )
6162 }
@@ -116,18 +117,9 @@ func (r *Reconciler) reconcilePGBouncerInPostgreSQL(
116117) error {
117118 log := logging .FromContext (ctx )
118119
119- var pod * corev1.Pod
120-
121120 // Find the PostgreSQL instance that can execute SQL that writes to every
122121 // database. When there is none, return early.
123-
124- for _ , instance := range instances .forCluster {
125- writable , known := instance .IsWritable ()
126- if writable && known && len (instance .Pods ) > 0 {
127- pod = instance .Pods [0 ]
128- break
129- }
130- }
122+ pod , _ := instances .WritablePod (naming .ContainerDatabase )
131123 if pod == nil {
132124 return nil
133125 }
@@ -590,15 +582,32 @@ func (r *Reconciler) reconcilePGBouncerPodDisruptionBudget(
590582 return err
591583}
592584
593- // reconcilePGBouncerReconnect triggers a RECONNECT command on all PgBouncer
594- // pods when the primary has changed. This forces PgBouncer to establish new
585+ // pgbouncerPods returns a list of PgBouncer pods for the given cluster.
586+ func (r * Reconciler ) pgbouncerPods (ctx context.Context , cluster * v1beta1.PostgresCluster ) (* corev1.PodList , error ) {
587+ pgbouncerPods := & corev1.PodList {}
588+ selector , err := naming .AsSelector (naming .ClusterPGBouncerSelector (cluster ))
589+ if err != nil {
590+ return nil , errors .WithStack (err )
591+ }
592+
593+ if err := r .Client .List (ctx , pgbouncerPods ,
594+ client .InNamespace (cluster .Namespace ),
595+ client.MatchingLabelsSelector {Selector : selector }); err != nil {
596+ return nil , errors .WithStack (err )
597+ }
598+ return pgbouncerPods , nil
599+ }
600+
601+ // reconcilePGBouncerReconnect is a sub-reconciler that signals PgBouncer pods
602+ // when the primary has changed. This forces PgBouncer to establish new
595603// server connections to the correct primary, preventing stale connections
596604// from routing traffic to a demoted replica after failover.
597605//
598606// Note: RECONNECT closes server connections when they are "released" according
599607// to the pool mode. In transaction mode, this happens after each transaction.
600608// In session mode, this happens when the client disconnects - so persistent
601609// clients may continue hitting the old primary until they reconnect.
610+ // It returns error for integration with the parent reconciler's error handling chain.
602611func (r * Reconciler ) reconcilePGBouncerReconnect (
603612 ctx context.Context , cluster * v1beta1.PostgresCluster ,
604613 instances * observedInstances ,
@@ -610,79 +619,69 @@ func (r *Reconciler) reconcilePGBouncerReconnect(
610619 return nil
611620 }
612621
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+ primaryPod , _ := instances .WritablePod (naming .ContainerDatabase )
622623 if primaryPod == nil {
623624 // We will retry later.
624- log .V (1 ).Info ("No writable instance found, skipping PgBouncer RECONNECT " )
625+ log .V (1 ).Info ("No writable instance found, skipping PgBouncer failover signal " )
625626 return nil
626627 }
627628
628629 currentPrimaryUID := string (primaryPod .UID )
629- lastReconnectUID := cluster .Status .Proxy .PGBouncer .LastReconnectPrimaryUID
630+ lastFailoverUID := cluster .Status .Proxy .PGBouncer .LastFailoverPrimaryUID
630631
631- if currentPrimaryUID == lastReconnectUID {
632- // Primary hasn't changed, no need to Reconnect .
632+ if currentPrimaryUID == lastFailoverUID {
633+ // Primary hasn't changed, no need to trigger failover .
633634 return nil
634635 }
635636
636- log .Info ("Primary changed, triggering PgBouncer RECONNECT " ,
637- "previousPrimaryUID" , lastReconnectUID ,
637+ log .Info ("Primary changed, triggering PgBouncer failover signal (SIGTERM) " ,
638+ "previousPrimaryUID" , lastFailoverUID ,
638639 "currentPrimaryUID" , currentPrimaryUID ,
639640 "currentPrimaryName" , primaryPod .Name )
640641
641- pgbouncerPods := & corev1.PodList {}
642- selector , err := naming .AsSelector (naming .ClusterPGBouncerSelector (cluster ))
642+ pgbouncerPods , err := r .pgbouncerPods (ctx , cluster )
643643 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 )
644+ return err
651645 }
652646
653- // Send RECONNECT to each running PgBouncer pod
654- var reconnectErr error
647+ // Send SIGTERM to each running PgBouncer pod to trigger graceful shutdown
648+ // and container restart. New PgBouncer process will do fresh DNS lookup.
649+ var failoverErrs []error
655650 successCount := 0
656651
657652 for i := range pgbouncerPods .Items {
658- pod := & pgbouncerPods .Items [i ]
653+ pod := pgbouncerPods .Items [i ] // Copy value to avoid closure reference issues
659654 if pod .Status .Phase != corev1 .PodRunning {
660655 continue
661656 }
662657
663- exec := func (ctx context.Context , stdin io.Reader , stdout , stderr io.Writer , command ... string ) error {
658+ if err := pgbouncer . SignalFailover ( ctx , func (ctx context.Context , stdin io.Reader , stdout , stderr io.Writer , command ... string ) error {
664659 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
660+ }); err != nil {
661+ log .Error (err , "PgBouncer failover signal: failed to send SIGTERM to pod" , "pod" , pod .Name )
662+ failoverErrs = append (failoverErrs , fmt .Errorf ("pod %s: %w" , pod .Name , err ))
670663 } else {
671664 successCount ++
672665 }
673666 }
674667
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
668+ // Update status only if all pods were successfully signaled .
669+ // Partial failures will be retried in the next reconciliation loop.
670+ if len ( failoverErrs ) == 0 {
671+ cluster .Status .Proxy .PGBouncer .LastFailoverPrimaryUID = currentPrimaryUID
679672 }
680673
681- log .Info ("PgBouncer RECONNECT : done" ,
682- "failed" , reconnectErr != nil ,
674+ log .Info ("PgBouncer failover signal : done" ,
675+ "failed" , len ( failoverErrs ) > 0 ,
683676 "successCount" , successCount ,
677+ "failureCount" , len (failoverErrs ),
684678 "totalPods" , len (pgbouncerPods .Items ),
685679 )
686680
687- return reconnectErr
681+ // Return aggregated errors if any pods failed
682+ if len (failoverErrs ) > 0 {
683+ return fmt .Errorf ("failed to signal %d of %d pgbouncer pods: %w" ,
684+ len (failoverErrs ), len (pgbouncerPods .Items ), failoverErrs [0 ])
685+ }
686+ return nil
688687}
0 commit comments