knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/websocket/connection.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package websocket
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/gob"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http/httputil"
    26  	"sync"
    27  	"time"
    28  
    29  	"go.uber.org/zap"
    30  
    31  	"k8s.io/apimachinery/pkg/util/wait"
    32  
    33  	"github.com/gorilla/websocket"
    34  )
    35  
    36  var (
    37  	// ErrConnectionNotEstablished is returned by methods that need a connection
    38  	// but no connection is already created.
    39  	ErrConnectionNotEstablished = errors.New("connection has not yet been established")
    40  
    41  	// errShuttingDown is returned internally once the shutdown signal has been sent.
    42  	errShuttingDown = errors.New("shutdown in progress")
    43  
    44  	// pongTimeout defines the amount of time allowed between two pongs to arrive
    45  	// before the connection is considered broken.
    46  	pongTimeout = 10 * time.Second
    47  )
    48  
    49  // RawConnection is an interface defining the methods needed
    50  // from a websocket connection
    51  type rawConnection interface {
    52  	WriteMessage(messageType int, data []byte) error
    53  	NextReader() (int, io.Reader, error)
    54  	Close() error
    55  
    56  	SetReadDeadline(deadline time.Time) error
    57  	SetPongHandler(func(string) error)
    58  }
    59  
    60  // ManagedConnection represents a websocket connection.
    61  type ManagedConnection struct {
    62  	connection        rawConnection
    63  	connectionFactory func() (rawConnection, error)
    64  
    65  	closeChan chan struct{}
    66  	closeOnce sync.Once
    67  
    68  	establishChan chan struct{}
    69  	establishOnce sync.Once
    70  
    71  	// Used to capture asynchronous processes to be waited
    72  	// on when shutting the connection down.
    73  	processingWg sync.WaitGroup
    74  
    75  	// If set, messages will be forwarded to this channel
    76  	messageChan chan []byte
    77  
    78  	// This mutex controls access to the connection reference
    79  	// itself.
    80  	connectionLock sync.RWMutex
    81  
    82  	// Gorilla's documentation states, that one reader and
    83  	// one writer are allowed concurrently.
    84  	readerLock sync.Mutex
    85  	writerLock sync.Mutex
    86  
    87  	// Used for the exponential backoff when connecting
    88  	connectionBackoff wait.Backoff
    89  
    90  	// OnConnect is called when a connection is successfully established.
    91  	// This callback is invoked each time the connection is established,
    92  	// including reconnections.
    93  	OnConnect func()
    94  
    95  	// OnDisconnect is called when a connection is lost.
    96  	// The error parameter contains the reason for the disconnection.
    97  	OnDisconnect func(error)
    98  }
    99  
   100  // ConnectionOption is a functional option for configuring ManagedConnection.
   101  type ConnectionOption func(*ManagedConnection)
   102  
   103  // WithOnConnect sets a callback that is invoked when a connection is established.
   104  func WithOnConnect(f func()) ConnectionOption {
   105  	return func(c *ManagedConnection) {
   106  		c.OnConnect = f
   107  	}
   108  }
   109  
   110  // WithOnDisconnect sets a callback that is invoked when a connection is lost.
   111  func WithOnDisconnect(f func(error)) ConnectionOption {
   112  	return func(c *ManagedConnection) {
   113  		c.OnDisconnect = f
   114  	}
   115  }
   116  
   117  // NewDurableSendingConnection creates a new websocket connection
   118  // that can only send messages to the endpoint it connects to.
   119  // The connection will continuously be kept alive and reconnected
   120  // in case of a loss of connectivity.
   121  func NewDurableSendingConnection(target string, logger *zap.SugaredLogger, opts ...ConnectionOption) *ManagedConnection {
   122  	return NewDurableConnection(target, nil, logger, opts...)
   123  }
   124  
   125  // NewDurableSendingConnectionGuaranteed creates a new websocket connection
   126  // that can only send messages to the endpoint it connects to. It returns
   127  // the connection if the connection can be established within the given
   128  // `duration`. Otherwise it returns the ErrConnectionNotEstablished error.
   129  //
   130  // The connection will continuously be kept alive and reconnected
   131  // in case of a loss of connectivity.
   132  func NewDurableSendingConnectionGuaranteed(target string, duration time.Duration, logger *zap.SugaredLogger) (*ManagedConnection, error) {
   133  	c := NewDurableConnection(target, nil, logger)
   134  
   135  	select {
   136  	case <-c.establishChan:
   137  		return c, nil
   138  	case <-time.After(duration):
   139  		c.Shutdown()
   140  		return nil, ErrConnectionNotEstablished
   141  	}
   142  }
   143  
   144  // NewDurableConnection creates a new websocket connection, that
   145  // passes incoming messages to the given message channel. It can also
   146  // send messages to the endpoint it connects to.
   147  // The connection will continuously be kept alive and reconnected
   148  // in case of a loss of connectivity.
   149  //
   150  // Note: The given channel needs to be drained after calling `Shutdown`
   151  // to not cause any deadlocks. If the channel's buffer is likely to be
   152  // filled, this needs to happen in separate goroutines, i.e.
   153  //
   154  // go func() {conn.Shutdown(); close(messageChan)}
   155  // go func() {for range messageChan {}}
   156  func NewDurableConnection(target string, messageChan chan []byte, logger *zap.SugaredLogger, opts ...ConnectionOption) *ManagedConnection {
   157  	websocketConnectionFactory := func() (rawConnection, error) {
   158  		dialer := &websocket.Dialer{
   159  			// This needs to be relatively short to avoid the connection getting blackholed for a long time
   160  			// by restarting the serving side of the connection behind a Kubernetes Service.
   161  			HandshakeTimeout: 3 * time.Second,
   162  		}
   163  		conn, resp, err := dialer.Dial(target, nil) //nolint:bodyclose
   164  		if err != nil {
   165  			if resp != nil {
   166  				dresp, _ := httputil.DumpResponse(resp, true /*body*/) // This is for logging so don't care if it fails.
   167  				logger.Errorw("Websocket connection could not be established", zap.Error(err),
   168  					zap.String("request", string(dresp)))
   169  			} else {
   170  				logger.Errorw("Websocket connection could not be established", zap.Error(err))
   171  			}
   172  		}
   173  		return conn, err
   174  	}
   175  
   176  	c := newConnection(websocketConnectionFactory, messageChan)
   177  
   178  	// Apply options before starting the goroutine
   179  	for _, opt := range opts {
   180  		opt(c)
   181  	}
   182  
   183  	// Keep the connection alive asynchronously and reconnect on
   184  	// connection failure.
   185  	c.processingWg.Add(1)
   186  	go func() {
   187  		defer c.processingWg.Done()
   188  
   189  		for {
   190  			select {
   191  			default:
   192  				logger.Info("Connecting to ", target)
   193  				if err := c.connect(); err != nil {
   194  					logger.Errorw("Failed connecting to "+target, zap.Error(err))
   195  					continue
   196  				}
   197  				logger.Debug("Connected to ", target)
   198  				if c.OnConnect != nil {
   199  					c.OnConnect()
   200  				}
   201  				if err := c.keepalive(); err != nil {
   202  					logger.Errorw(fmt.Sprintf("Connection to %s broke down, reconnecting...", target), zap.Error(err))
   203  					if c.OnDisconnect != nil {
   204  						c.OnDisconnect(err)
   205  					}
   206  				}
   207  				if err := c.closeConnection(); err != nil {
   208  					logger.Errorw("Failed to close the connection after crashing", zap.Error(err))
   209  				}
   210  			case <-c.closeChan:
   211  				logger.Infof("Connection to %s is being shutdown", target)
   212  				return
   213  			}
   214  		}
   215  	}()
   216  
   217  	// Keep sending pings 3 times per pongTimeout interval.
   218  	c.processingWg.Add(1)
   219  	go func() {
   220  		defer c.processingWg.Done()
   221  
   222  		ticker := time.NewTicker(pongTimeout / 3)
   223  		defer ticker.Stop()
   224  		for {
   225  			select {
   226  			case <-ticker.C:
   227  				if err := c.write(websocket.PingMessage, []byte{}); err != nil {
   228  					logger.Errorw("Failed to send ping message to "+target, zap.Error(err))
   229  				}
   230  			case <-c.closeChan:
   231  				return
   232  			}
   233  		}
   234  	}()
   235  
   236  	return c
   237  }
   238  
   239  // newConnection creates a new connection primitive.
   240  func newConnection(connFactory func() (rawConnection, error), messageChan chan []byte) *ManagedConnection {
   241  	conn := &ManagedConnection{
   242  		connectionFactory: connFactory,
   243  		closeChan:         make(chan struct{}),
   244  		establishChan:     make(chan struct{}),
   245  		messageChan:       messageChan,
   246  		connectionBackoff: wait.Backoff{
   247  			Duration: 100 * time.Millisecond,
   248  			Factor:   1.3,
   249  			Steps:    20,
   250  			Jitter:   0.5,
   251  		},
   252  	}
   253  
   254  	return conn
   255  }
   256  
   257  // connect tries to establish a websocket connection.
   258  func (c *ManagedConnection) connect() error {
   259  	return wait.ExponentialBackoff(c.connectionBackoff, func() (bool, error) {
   260  		select {
   261  		default:
   262  			conn, err := c.connectionFactory()
   263  			if err != nil {
   264  				return false, nil
   265  			}
   266  
   267  			// Setting the read deadline will cause NextReader in read
   268  			// to fail if it is exceeded. This deadline is reset each
   269  			// time we receive a pong message so we know the connection
   270  			// is still intact.
   271  			conn.SetReadDeadline(time.Now().Add(pongTimeout))
   272  			conn.SetPongHandler(func(string) error {
   273  				conn.SetReadDeadline(time.Now().Add(pongTimeout))
   274  				return nil
   275  			})
   276  
   277  			c.connectionLock.Lock()
   278  			defer c.connectionLock.Unlock()
   279  
   280  			c.connection = conn
   281  			c.establishOnce.Do(func() {
   282  				close(c.establishChan)
   283  			})
   284  			return true, nil
   285  		case <-c.closeChan:
   286  			return false, errShuttingDown
   287  		}
   288  	})
   289  }
   290  
   291  // keepalive keeps the connection open.
   292  func (c *ManagedConnection) keepalive() error {
   293  	for {
   294  		select {
   295  		default:
   296  			if err := c.read(); err != nil {
   297  				return err
   298  			}
   299  		case <-c.closeChan:
   300  			return errShuttingDown
   301  		}
   302  	}
   303  }
   304  
   305  // closeConnection closes the underlying websocket connection.
   306  func (c *ManagedConnection) closeConnection() error {
   307  	c.connectionLock.Lock()
   308  	defer c.connectionLock.Unlock()
   309  
   310  	if c.connection != nil {
   311  		err := c.connection.Close()
   312  		c.connection = nil
   313  		return err
   314  	}
   315  	return nil
   316  }
   317  
   318  // read reads the next message from the connection.
   319  // If a messageChan is supplied and the current message type is not
   320  // a control message, the message is sent to that channel.
   321  func (c *ManagedConnection) read() error {
   322  	c.connectionLock.RLock()
   323  	defer c.connectionLock.RUnlock()
   324  
   325  	if c.connection == nil {
   326  		return ErrConnectionNotEstablished
   327  	}
   328  
   329  	c.readerLock.Lock()
   330  	defer c.readerLock.Unlock()
   331  
   332  	messageType, reader, err := c.connection.NextReader()
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	// Send the message to the channel if its an application level message
   338  	// and if that channel is set.
   339  	// TODO(markusthoemmes): Return the messageType along with the payload.
   340  	if c.messageChan != nil && (messageType == websocket.TextMessage || messageType == websocket.BinaryMessage) {
   341  		if message, _ := io.ReadAll(reader); message != nil {
   342  			c.messageChan <- message
   343  		}
   344  	}
   345  
   346  	return nil
   347  }
   348  
   349  func (c *ManagedConnection) write(messageType int, body []byte) error {
   350  	c.connectionLock.RLock()
   351  	defer c.connectionLock.RUnlock()
   352  
   353  	if c.connection == nil {
   354  		return ErrConnectionNotEstablished
   355  	}
   356  
   357  	c.writerLock.Lock()
   358  	defer c.writerLock.Unlock()
   359  
   360  	return c.connection.WriteMessage(messageType, body)
   361  }
   362  
   363  // Status checks the connection status of the webhook.
   364  func (c *ManagedConnection) Status() error {
   365  	c.connectionLock.RLock()
   366  	defer c.connectionLock.RUnlock()
   367  
   368  	if c.connection == nil {
   369  		return ErrConnectionNotEstablished
   370  	}
   371  	return nil
   372  }
   373  
   374  // Send sends an encodable message over the websocket connection.
   375  func (c *ManagedConnection) Send(msg interface{}) error {
   376  	var b bytes.Buffer
   377  	enc := gob.NewEncoder(&b)
   378  	if err := enc.Encode(msg); err != nil {
   379  		return err
   380  	}
   381  
   382  	return c.write(websocket.BinaryMessage, b.Bytes())
   383  }
   384  
   385  // SendRaw sends a message over the websocket connection without performing any encoding.
   386  func (c *ManagedConnection) SendRaw(messageType int, msg []byte) error {
   387  	return c.write(messageType, msg)
   388  }
   389  
   390  // Shutdown closes the websocket connection.
   391  func (c *ManagedConnection) Shutdown() error {
   392  	c.closeOnce.Do(func() {
   393  		close(c.closeChan)
   394  	})
   395  
   396  	err := c.closeConnection()
   397  	c.processingWg.Wait()
   398  	return err
   399  }