@@ -45,7 +45,6 @@ type Client struct {
4545 mu sync.Mutex
4646 // this channel is used for correlation-less incoming frames from the server
4747 frameBodyListener chan internal.SyncCommandRead
48- isOpen bool
4948 // this function is used during shutdown to stop the background loop that reads from the network
5049 ioLoopCancelFn context.CancelFunc
5150 connection * internal.Connection
@@ -57,16 +56,23 @@ type Client struct {
5756 publishErrorCh chan * PublishError
5857 chunkCh chan * Chunk
5958 notifyCh chan * CreditError
60- metadataUpdateCh chan * MetadataUpdate
61- consumerUpdateCh chan * ConsumerUpdate
59+ // see constants states of the connection
60+ // we need different states to handle the case where the connection is closed by the server
61+ // Open/ConnectionClosing/Closed
62+ // ConnectionClosing is used to set the connection status no longer open, but still waiting for the server to close the connection
63+ connectionStatus uint8
64+ // socketClosedCh is used to notify the client that the socket has been closed
65+ socketClosedCh chan error
66+ metadataUpdateCh chan * MetadataUpdate
67+ consumerUpdateCh chan * ConsumerUpdate
6268}
6369
6470// IsOpen returns true if the connection is open, false otherwise
6571// IsOpen is thread-safe
6672func (tc * Client ) IsOpen () bool {
6773 tc .mu .Lock ()
6874 defer tc .mu .Unlock ()
69- return tc .isOpen
75+ return tc .connectionStatus == constants . ConnectionOpen
7076}
7177
7278// NewClient returns a common.Clienter implementation to interact with RabbitMQ stream using low level primitives.
@@ -76,7 +82,7 @@ func NewClient(connection net.Conn, configuration *ClientConfiguration) Clienter
7682 rawClient := & Client {
7783 frameBodyListener : make (chan internal.SyncCommandRead ),
7884 connection : internal .NewConnection (connection ),
79- isOpen : false ,
85+ connectionStatus : constants . ConnectionClosed ,
8086 correlationsMap : sync.Map {},
8187 configuration : configuration ,
8288 }
@@ -222,6 +228,24 @@ func (tc *Client) handleIncoming(ctx context.Context) error {
222228 if errors .Is (err , io .ErrClosedPipe ) {
223229 return nil
224230 }
231+
232+ if errors .Is (err , io .EOF ) {
233+ // EOF is returned when the connection is closed
234+ if tc .socketClosedCh != nil {
235+ if tc .IsOpen () {
236+ tc .socketClosedCh <- ErrConnectionClosed
237+ }
238+ // the TCP connection here is closed
239+ // we close the channel since we don't need to send more than one message
240+ close (tc .socketClosedCh )
241+ tc .socketClosedCh = nil
242+ }
243+ // set the shutdown flag to false since we don't want to close the connection
244+ // since it's already closed
245+ _ = tc .shutdown (false )
246+ return nil
247+ }
248+
225249 if err != nil {
226250 // TODO: some errors may be recoverable. We only need to return if reconnection
227251 // is needed
@@ -278,7 +302,7 @@ func (tc *Client) handleIncoming(ctx context.Context) error {
278302 // we do not return here because we must execute the shutdown process and close the socket
279303 }
280304
281- err = tc .shutdown ()
305+ err = tc .shutdown (true )
282306 if err != nil && ! errors .Is (err , io .ErrClosedPipe ) {
283307 return err
284308 }
@@ -594,21 +618,34 @@ func (tc *Client) open(ctx context.Context, brokerIndex int) error {
594618 return streamErrorOrNil (openResp .ResponseCode ())
595619}
596620
597- func (tc * Client ) shutdown () error {
621+ func (tc * Client ) shutdown (closeConnection bool ) error {
598622 tc .mu .Lock ()
599623 defer tc .mu .Unlock ()
600- tc .isOpen = false
624+ // The method can be called by the Close() method or by the
625+ // connection error handler, see: handleIncoming EOF error
626+ // the shutdown method has to be idempotent since it can be called multiple times
627+ // In case of unexpected connection error, the shutdown is called just once
628+ tc .connectionStatus = constants .ConnectionClosed
601629 tc .ioLoopCancelFn ()
602630
603631 if tc .confirmsCh != nil {
604632 close (tc .confirmsCh )
633+ tc .confirmsCh = nil
605634 }
606635
607636 if tc .chunkCh != nil {
608637 close (tc .chunkCh )
638+ tc .chunkCh = nil
609639 }
610640
611- return tc .connection .Close ()
641+ // if the caller is handleIncoming EOF error closeConnection is false,
642+ // The connection is already closed
643+ // So we don't need to close it again
644+
645+ if closeConnection {
646+ return tc .connection .Close ()
647+ }
648+ return nil
612649}
613650
614651func (tc * Client ) handleClose (ctx context.Context , req * internal.CloseRequest ) error {
@@ -863,7 +900,7 @@ func (tc *Client) Connect(ctx context.Context) error {
863900 }
864901
865902 tc .mu .Lock ()
866- tc .isOpen = true
903+ tc .connectionStatus = constants . ConnectionOpen
867904 defer tc .mu .Unlock ()
868905 log .Info ("connection is open" )
869906
@@ -1113,6 +1150,8 @@ func (tc *Client) Close(ctx context.Context) error {
11131150 return errNilContext
11141151 }
11151152
1153+ tc .connectionStatus = constants .ConnectionClosing
1154+
11161155 log := loggerFromCtxOrDiscard (ctx ).WithGroup ("close" )
11171156 log .Debug ("starting connection close" )
11181157
@@ -1127,7 +1166,7 @@ func (tc *Client) Close(ctx context.Context) error {
11271166 log .Error ("close response code is not OK" , "error" , err )
11281167 }
11291168
1130- err = tc .shutdown ()
1169+ err = tc .shutdown (true )
11311170 if err != nil && ! errors .Is (err , io .ErrClosedPipe ) {
11321171 return err
11331172 }
@@ -1238,6 +1277,20 @@ func (tc *Client) NotifyChunk(c chan *Chunk) <-chan *Chunk {
12381277 return c
12391278}
12401279
1280+ // NotifyConnectionClosed receives notifications about connection closed events.
1281+ // It is raised only once when the connection is closed in unexpected way.
1282+ // Connection gracefully closed by the client will not raise this event.
1283+ func (tc * Client ) NotifyConnectionClosed () <- chan error {
1284+ tc .mu .Lock ()
1285+ defer tc .mu .Unlock ()
1286+ // The user can't decide the size of the channel, so we use a buffer of 1.
1287+ // NotifyConnectionClosed is one shot notification, so we don't need a buffer.
1288+ // buffer greater than 1 cloud cause a deadlock since the channel is closed after the first notification.
1289+ c := make (chan error , 1 )
1290+ tc .socketClosedCh = c
1291+ return c
1292+ }
1293+
12411294// NotifyCreditError TODO: go docs
12421295func (tc * Client ) NotifyCreditError (notification chan * CreditError ) <- chan * CreditError {
12431296 tc .mu .Lock ()
0 commit comments