You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by zi...@apache.org on 2024/09/11 03:16:54 UTC

(pulsar-client-go) branch master updated: Refactor connection concurrency model (#1275)

This is an automated email from the ASF dual-hosted git repository.

zixuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-client-go.git


The following commit(s) were added to refs/heads/master by this push:
     new 7ae0059f Refactor connection concurrency model (#1275)
7ae0059f is described below

commit 7ae0059fe7464431cf11f916e44216bdf536068f
Author: Gaylor Bosson <gb...@taurusgroup.ch>
AuthorDate: Wed Sep 11 05:16:49 2024 +0200

    Refactor connection concurrency model (#1275)
---
 pulsar/internal/connection.go | 221 ++++++++++++++++++------------------------
 1 file changed, 93 insertions(+), 128 deletions(-)

diff --git a/pulsar/internal/connection.go b/pulsar/internal/connection.go
index c1f38f89..462cfe88 100644
--- a/pulsar/internal/connection.go
+++ b/pulsar/internal/connection.go
@@ -35,8 +35,6 @@ import (
 
 	pb "github.com/apache/pulsar-client-go/pulsar/internal/pulsar_proto"
 	"github.com/apache/pulsar-client-go/pulsar/log"
-
-	ua "go.uber.org/atomic"
 )
 
 const (
@@ -87,7 +85,7 @@ type Connection interface {
 	ID() string
 	GetMaxMessageSize() int32
 	Close()
-	WaitForClose() <-chan interface{}
+	WaitForClose() <-chan struct{}
 	IsProxied() bool
 }
 
@@ -108,7 +106,6 @@ type connectionState int32
 const (
 	connectionInit = iota
 	connectionReady
-	connectionClosing
 	connectionClosed
 )
 
@@ -118,8 +115,6 @@ func (s connectionState) String() string {
 		return "Initializing"
 	case connectionReady:
 		return "Ready"
-	case connectionClosing:
-		return "Closing"
 	case connectionClosed:
 		return "Closed"
 	default:
@@ -139,16 +134,19 @@ type incomingCmd struct {
 }
 
 type connection struct {
-	sync.Mutex
-	cond              *sync.Cond
 	started           int32
-	state             ua.Int32
 	connectionTimeout time.Duration
 	closeOnce         sync.Once
 
+	// mu protects the fields below against concurrency accesses.
+	mu               sync.RWMutex
+	state            atomic.Int32
+	cnx              net.Conn
+	listeners        map[uint64]ConnectionListener
+	consumerHandlers map[uint64]ConsumerHandler
+
 	logicalAddr  *url.URL
 	physicalAddr *url.URL
-	cnx          net.Conn
 
 	writeBufferLock sync.Mutex
 	writeBuffer     Buffer
@@ -162,18 +160,13 @@ type connection struct {
 	incomingRequestsWG sync.WaitGroup
 	incomingRequestsCh chan *request
 	incomingCmdCh      chan *incomingCmd
-	closeCh            chan interface{}
+	closeCh            chan struct{}
+	readyCh            chan struct{}
 	writeRequestsCh    chan Buffer
 
 	pendingLock sync.Mutex
 	pendingReqs map[uint64]*request
 
-	listenersLock sync.RWMutex
-	listeners     map[uint64]ConnectionListener
-
-	consumerHandlersLock sync.RWMutex
-	consumerHandlers     map[uint64]ConsumerHandler
-
 	tlsOptions *TLSOptions
 	auth       auth.Provider
 
@@ -210,7 +203,8 @@ func newConnection(opts connectionOptions) *connection {
 		tlsOptions:           opts.tls,
 		auth:                 opts.auth,
 
-		closeCh:            make(chan interface{}),
+		closeCh:            make(chan struct{}),
+		readyCh:            make(chan struct{}),
 		incomingRequestsCh: make(chan *request, 10),
 		incomingCmdCh:      make(chan *incomingCmd, 10),
 
@@ -224,9 +218,8 @@ func newConnection(opts connectionOptions) *connection {
 		consumerHandlers: make(map[uint64]ConsumerHandler),
 		metrics:          opts.metrics,
 	}
-	cnx.setState(connectionInit)
+	cnx.state.Store(int32(connectionInit))
 	cnx.reader = newConnectionReader(cnx)
-	cnx.cond = sync.NewCond(cnx)
 	return cnx
 }
 
@@ -289,11 +282,11 @@ func (c *connection) connect() bool {
 		return false
 	}
 
-	c.Lock()
+	c.mu.Lock()
 	c.cnx = cnx
 	c.log = c.log.SubLogger(log.Fields{"local_addr": c.cnx.LocalAddr()})
 	c.log.Info("TCP connection established")
-	c.Unlock()
+	c.mu.Unlock()
 
 	return true
 }
@@ -347,7 +340,8 @@ func (c *connection) doHandshake() bool {
 	}
 	c.log.Info("Connection is ready")
 	c.setLastDataReceived(time.Now())
-	c.changeState(connectionReady)
+	c.setStateReady()
+	close(c.readyCh) // broadcast the readiness of the connection.
 	return true
 }
 
@@ -356,22 +350,13 @@ func (c *connection) IsProxied() bool {
 }
 
 func (c *connection) waitUntilReady() error {
-	// If we are going to call cond.Wait() at all, then we must call it _before_ we call cond.Broadcast().
-	// The lock is held here to prevent changeState() from calling cond.Broadcast() in the time between
-	// the state check and call to cond.Wait().
-	c.Lock()
-	defer c.Unlock()
-
-	for c.getState() != connectionReady {
-		c.log.Debugf("Wait until connection is ready state=%s", c.getState().String())
-		if c.getState() == connectionClosed {
-			return errors.New("connection error")
-		}
-		// wait for a new connection state change
-		c.cond.Wait()
+	select {
+	case <-c.readyCh:
+		return nil
+	case <-c.closeCh:
+		// Connection has been closed while waiting for the readiness.
+		return errors.New("connection error")
 	}
-
-	return nil
 }
 
 func (c *connection) failLeftRequestsWhenClose() {
@@ -492,8 +477,8 @@ func (c *connection) WriteData(data Buffer) {
 			// 1. blocked, in which case we need to wait until we have space
 			// 2. the connection is already closed, then we need to bail out
 			c.log.Debug("Couldn't write on connection channel immediately")
-			state := c.getState()
-			if state != connectionReady {
+
+			if c.getState() != connectionReady {
 				c.log.Debug("Connection was already closed")
 				return
 			}
@@ -648,8 +633,7 @@ func (c *connection) SendRequest(requestID uint64, req *pb.BaseCommand,
 	c.incomingRequestsWG.Add(1)
 	defer c.incomingRequestsWG.Done()
 
-	state := c.getState()
-	if state == connectionClosed || state == connectionClosing {
+	if c.getState() == connectionClosed {
 		callback(req, ErrConnectionClosed)
 
 	} else {
@@ -670,8 +654,7 @@ func (c *connection) SendRequestNoWait(req *pb.BaseCommand) error {
 	c.incomingRequestsWG.Add(1)
 	defer c.incomingRequestsWG.Done()
 
-	state := c.getState()
-	if state == connectionClosed || state == connectionClosing {
+	if c.getState() == connectionClosed {
 		return ErrConnectionClosed
 	}
 
@@ -751,9 +734,9 @@ func (c *connection) handleAckResponse(ackResponse *pb.CommandAckResponse) {
 func (c *connection) handleSendReceipt(response *pb.CommandSendReceipt) {
 	producerID := response.GetProducerId()
 
-	c.listenersLock.RLock()
+	c.mu.RLock()
 	producer, ok := c.listeners[producerID]
-	c.listenersLock.RUnlock()
+	c.mu.RUnlock()
 
 	if ok {
 		producer.ReceivedSendReceipt(response)
@@ -893,12 +876,13 @@ func (c *connection) handleSendError(sendError *pb.CommandSendError) {
 }
 
 func (c *connection) deletePendingProducers(producerID uint64) (ConnectionListener, bool) {
-	c.listenersLock.Lock()
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
 	producer, ok := c.listeners[producerID]
 	if ok {
 		delete(c.listeners, producerID)
 	}
-	c.listenersLock.Unlock()
 
 	return producer, ok
 }
@@ -960,9 +944,9 @@ func (c *connection) handleTopicMigrated(commandTopicMigrated *pb.CommandTopicMi
 		return
 	}
 	if commandTopicMigrated.GetResourceType() == pb.CommandTopicMigrated_Producer {
-		c.listenersLock.RLock()
+		c.mu.RLock()
 		producer, ok := c.listeners[resourceID]
-		c.listenersLock.RUnlock()
+		c.mu.RUnlock()
 		if ok {
 			producer.SetRedirectedClusterURI(migratedBrokerServiceURL)
 			c.log.Infof("producerID:{%d} migrated to RedirectedClusterURI:{%s}",
@@ -984,73 +968,55 @@ func (c *connection) handleTopicMigrated(commandTopicMigrated *pb.CommandTopicMi
 }
 
 func (c *connection) RegisterListener(id uint64, listener ConnectionListener) error {
-	// do not add if connection is closed
-	if c.closed() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.getState() == connectionClosed {
 		c.log.Warnf("Connection closed unable register listener id=%+v", id)
 		return errUnableRegisterListener
 	}
 
-	c.listenersLock.Lock()
-	defer c.listenersLock.Unlock()
-
 	c.listeners[id] = listener
 	return nil
 }
 
 func (c *connection) UnregisterListener(id uint64) {
-	c.listenersLock.Lock()
-	defer c.listenersLock.Unlock()
+	c.mu.Lock()
+	defer c.mu.Unlock()
 
 	delete(c.listeners, id)
 }
 
 func (c *connection) ResetLastActive() {
-	c.Lock()
-	defer c.Unlock()
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
 	c.lastActive = time.Now()
 }
 
 func (c *connection) isIdle() bool {
-	{
-		c.pendingLock.Lock()
-		defer c.pendingLock.Unlock()
-		if len(c.pendingReqs) != 0 {
-			return false
-		}
-	}
-
-	{
-		c.listenersLock.RLock()
-		defer c.listenersLock.RUnlock()
-		if len(c.listeners) != 0 {
-			return false
-		}
-	}
-
-	{
-		c.consumerHandlersLock.Lock()
-		defer c.consumerHandlersLock.Unlock()
-		if len(c.consumerHandlers) != 0 {
-			return false
-		}
-	}
-
-	if len(c.incomingRequestsCh) != 0 || len(c.writeRequestsCh) != 0 {
-		return false
-	}
-	return true
+	return len(c.listeners) == 0 &&
+		len(c.consumerHandlers) == 0 &&
+		len(c.incomingRequestsCh) == 0 &&
+		len(c.writeRequestsCh) == 0
 }
 
 func (c *connection) CheckIdle(maxIdleTime time.Duration) bool {
-	// We don't need to lock here because this method should only be
-	// called in a single goroutine of the connectionPool
-	if !c.isIdle() {
+	c.pendingLock.Lock()
+	sizePendingReqs := len(c.pendingReqs)
+	c.pendingLock.Unlock()
+
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if sizePendingReqs != 0 || !c.isIdle() {
 		c.lastActive = time.Now()
 	}
+
 	return time.Since(c.lastActive) > maxIdleTime
 }
 
-func (c *connection) WaitForClose() <-chan interface{} {
+func (c *connection) WaitForClose() <-chan struct{} {
 	return c.closeCh
 }
 
@@ -1059,10 +1025,7 @@ func (c *connection) WaitForClose() <-chan interface{} {
 // This also triggers callbacks to the ConnectionClosed listeners.
 func (c *connection) Close() {
 	c.closeOnce.Do(func() {
-		c.Lock()
-		cnx := c.cnx
-		c.Unlock()
-		c.changeState(connectionClosed)
+		listeners, consumerHandlers, cnx := c.closeAndEmptyObservers()
 
 		if cnx != nil {
 			_ = cnx.Close()
@@ -1070,22 +1033,6 @@ func (c *connection) Close() {
 
 		close(c.closeCh)
 
-		listeners := make(map[uint64]ConnectionListener)
-		c.listenersLock.Lock()
-		for id, listener := range c.listeners {
-			listeners[id] = listener
-			delete(c.listeners, id)
-		}
-		c.listenersLock.Unlock()
-
-		consumerHandlers := make(map[uint64]ConsumerHandler)
-		c.consumerHandlersLock.Lock()
-		for id, handler := range c.consumerHandlers {
-			consumerHandlers[id] = handler
-			delete(c.consumerHandlers, id)
-		}
-		c.consumerHandlersLock.Unlock()
-
 		// notify producers connection closed
 		for _, listener := range listeners {
 			listener.ConnectionClosed(nil)
@@ -1100,22 +1047,35 @@ func (c *connection) Close() {
 	})
 }
 
-func (c *connection) changeState(state connectionState) {
-	// The lock is held here because we need setState() and cond.Broadcast() to be
-	// an atomic operation from the point of view of waitUntilReady().
-	c.Lock()
-	defer c.Unlock()
+func (c *connection) closeAndEmptyObservers() ([]ConnectionListener, []ConsumerHandler, net.Conn) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.setStateClosed()
+
+	listeners := make([]ConnectionListener, 0, len(c.listeners))
+	for _, listener := range c.listeners {
+		listeners = append(listeners, listener)
+	}
 
-	c.setState(state)
-	c.cond.Broadcast()
+	handlers := make([]ConsumerHandler, 0, len(c.consumerHandlers))
+	for _, handler := range c.consumerHandlers {
+		handlers = append(handlers, handler)
+	}
+
+	return listeners, handlers, c.cnx
 }
 
 func (c *connection) getState() connectionState {
 	return connectionState(c.state.Load())
 }
 
-func (c *connection) setState(state connectionState) {
-	c.state.Store(int32(state))
+func (c *connection) setStateReady() {
+	c.state.CompareAndSwap(int32(connectionInit), int32(connectionReady))
+}
+
+func (c *connection) setStateClosed() {
+	c.state.Store(int32(connectionClosed))
 }
 
 func (c *connection) closed() bool {
@@ -1173,32 +1133,37 @@ func (c *connection) getTLSConfig() (*tls.Config, error) {
 }
 
 func (c *connection) AddConsumeHandler(id uint64, handler ConsumerHandler) error {
-	// do not add if connection is closed
-	if c.closed() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.getState() == connectionClosed {
 		c.log.Warnf("Closed connection unable add consumer with id=%+v", id)
 		return errUnableAddConsumeHandler
 	}
 
-	c.consumerHandlersLock.Lock()
-	defer c.consumerHandlersLock.Unlock()
 	c.consumerHandlers[id] = handler
 	return nil
 }
 
 func (c *connection) DeleteConsumeHandler(id uint64) {
-	c.consumerHandlersLock.Lock()
-	defer c.consumerHandlersLock.Unlock()
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
 	delete(c.consumerHandlers, id)
 }
 
 func (c *connection) consumerHandler(id uint64) (ConsumerHandler, bool) {
-	c.consumerHandlersLock.RLock()
-	defer c.consumerHandlersLock.RUnlock()
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
 	h, ok := c.consumerHandlers[id]
 	return h, ok
 }
 
 func (c *connection) ID() string {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
 	return fmt.Sprintf("%s -> %s", c.cnx.LocalAddr(), c.cnx.RemoteAddr())
 }