@@ -37,12 +37,18 @@ func drainAndClose(nc *nats.Conn) error {
3737 if nc == nil {
3838 return nil
3939 }
40+ // If connection is already closed, just return
41+ if nc .IsClosed () {
42+ return nil
43+ }
4044 // Drain connection (this will flush any pending messages and prevent new ones)
4145 err := nc .Drain ()
4246 if err != nil {
4347 logger .Log .Warnf ("error draining nats connection: %v" , err )
44- // Even if drain fails, try to close
45- nc .Close ()
48+ // Even if drain fails, try to close (but only if not already closed)
49+ if ! nc .IsClosed () {
50+ nc .Close ()
51+ }
4652 return err
4753 }
4854
@@ -57,7 +63,9 @@ func drainAndClose(nc *nats.Conn) error {
5763 continue
5864 case <- timeout :
5965 logger .Log .Warn ("drain timeout exceeded, forcing close" )
60- nc .Close ()
66+ if ! nc .IsClosed () {
67+ nc .Close ()
68+ }
6169 return fmt .Errorf ("drain timeout exceeded" )
6270 }
6371 }
@@ -66,13 +74,49 @@ func drainAndClose(nc *nats.Conn) error {
6674 return nil
6775}
6876
69- func setupNatsConn (connectString string , appDieChan chan bool , options ... nats.Option ) (* nats.Conn , error ) {
77+ // replaceNatsConnection handles the common logic for replacing NATS connections
78+ // It stores old connection/subscription references, calls initFunc to set up the new connection,
79+ // and then drains the old resources after the new connection is ready.
80+ func replaceNatsConnection (
81+ oldConn * nats.Conn ,
82+ oldSub * nats.Subscription ,
83+ initFunc func () error ,
84+ componentName string ,
85+ ) error {
86+ logger .Log .Infof ("replacing nats %s connection due to lame duck mode" , componentName )
87+
88+ // Re-initialize connection (pass true to indicate this is a replacement)
89+ if err := initFunc (); err != nil {
90+ return err
91+ }
92+
93+ // Drain and close old connection and subscription after new one is set up
94+ if oldSub != nil {
95+ go func () {
96+ if err := oldSub .Drain (); err != nil {
97+ logger .Log .Warnf ("error draining old %s subscription: %v" , componentName , err )
98+ }
99+ }()
100+ }
101+
102+ if oldConn != nil {
103+ go func () {
104+ if err := drainAndClose (oldConn ); err != nil {
105+ logger .Log .Warnf ("error draining old nats %s connection: %v" , componentName , err )
106+ }
107+ }()
108+ }
109+
110+ return nil
111+ }
112+
113+ func setupNatsConn (connectString string , appDieChan chan bool , lameDuckReplacement func () error , options ... nats.Option ) (* nats.Conn , error ) {
70114 connectedCh := make (chan bool )
71115 initialConnectErrorCh := make (chan error )
72116 natsOptions := append (
73117 options ,
74118 nats .DisconnectErrHandler (func (nc * nats.Conn , err error ) {
75- logger .Log .Warnf ("disconnected from nats (%s)! Reason: %q \n " , nc .ConnectedAddr (), err )
119+ logger .Log .Warnf ("disconnected from nats (%s)! Reason: %v " , nc .ConnectedAddr (), err )
76120 }),
77121 nats .ReconnectHandler (func (nc * nats.Conn ) {
78122 logger .Log .Warnf ("reconnected to nats server %s with address %s in cluster %s!" , nc .ConnectedServerName (), nc .ConnectedAddr (), nc .ConnectedClusterName ())
@@ -85,12 +129,28 @@ func setupNatsConn(connectString string, appDieChan chan bool, options ...nats.O
85129 }
86130
87131 logger .Log .Errorf ("nats connection closed. reason: %q" , nc .LastError ())
132+
133+ // If connection was never successfully established, prioritize initialConnectErrorCh
134+ // to allow setupNatsConn to return quickly with an error
135+ wasConnected := nc .ConnectedAddr () != ""
136+
137+ if ! wasConnected {
138+ // During initial connection, send error to initialConnectErrorCh first
139+ select {
140+ case initialConnectErrorCh <- nc .LastError ():
141+ return
142+ default :
143+ // If channel is not ready, fall through to appDieChan handling
144+ }
145+ }
146+
88147 if appDieChan != nil {
89148 select {
90149 case appDieChan <- true :
91150 return
92151 case initialConnectErrorCh <- nc .LastError ():
93152 logger .Log .Warnf ("appDieChan not ready, sending error in initialConnectCh" )
153+ return
94154 default :
95155 logger .Log .Warnf ("no termination channel available, sending termination signal to app" )
96156
@@ -108,6 +168,14 @@ func setupNatsConn(connectString string, appDieChan chan bool, options ...nats.O
108168 os .Exit (1 )
109169 }
110170 }
171+ } else if ! wasConnected {
172+ // If no appDieChan and connection was never established, try initialConnectErrorCh again
173+ select {
174+ case initialConnectErrorCh <- nc .LastError ():
175+ return
176+ default :
177+ // Channel not ready, but we've already logged the error
178+ }
111179 }
112180 }),
113181 nats .ErrorHandler (func (nc * nats.Conn , sub * nats.Subscription , err error ) {
@@ -123,6 +191,18 @@ func setupNatsConn(connectString string, appDieChan chan bool, options ...nats.O
123191 logger .Log .Infof ("connected to nats on %s" , nc .ConnectedAddr ())
124192 connectedCh <- true
125193 }),
194+ nats .LameDuckModeHandler (func (nc * nats.Conn ) {
195+ logger .Log .Warnf ("nats connection entered lame duck mode" )
196+ if lameDuckReplacement != nil {
197+ go func () {
198+ if err := lameDuckReplacement (); err != nil {
199+ logger .Log .Errorf ("failed to replace connection: %v" , err )
200+ // The old connection will eventually close (it's in lame duck mode),
201+ // which will trigger ClosedHandler and appDieChan
202+ }
203+ }()
204+ }
205+ }),
126206 )
127207
128208 nc , err := nats .Connect (connectString , natsOptions ... )
0 commit comments