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 }