From 1cda716da5dace80cac9c8e6043365a717cf5282 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Wed, 23 Oct 2024 12:19:01 -0300 Subject: [PATCH] Add `websocket_idle_timeout_seconds` config for idle WebSocket cleanup (#21493) --- .../cmd/bbb-graphql-middleware/main.go | 3 +++ bbb-graphql-middleware/config/Config.go | 1 + bbb-graphql-middleware/config/config.yml | 1 + .../internal/common/types.go | 3 +++ .../internal/hasura/client.go | 1 + .../internal/websrv/connhandler.go | 25 +++++++++++++++++++ .../internal/websrv/reader/reader.go | 4 +++ 7 files changed, 38 insertions(+) diff --git a/bbb-graphql-middleware/cmd/bbb-graphql-middleware/main.go b/bbb-graphql-middleware/cmd/bbb-graphql-middleware/main.go index bcfc77855d..42029da608 100644 --- a/bbb-graphql-middleware/cmd/bbb-graphql-middleware/main.go +++ b/bbb-graphql-middleware/cmd/bbb-graphql-middleware/main.go @@ -40,6 +40,9 @@ func main() { log.Infof("Json Patch Disabled!") } + // Routine to check for idle connections and close them + go websrv.InvalidateIdleBrowserConnectionsRoutine() + // Websocket listener rateLimiter := common.NewCustomRateLimiter(cfg.Server.MaxConnectionsPerSecond) diff --git a/bbb-graphql-middleware/config/Config.go b/bbb-graphql-middleware/config/Config.go index 8b4e8ac1c9..c4bb4a2ee1 100644 --- a/bbb-graphql-middleware/config/Config.go +++ b/bbb-graphql-middleware/config/Config.go @@ -29,6 +29,7 @@ type Config struct { JsonPatchDisabled bool `yaml:"json_patch_disabled"` SubscriptionAllowedList string `yaml:"subscriptions_allowed_list"` SubscriptionsDeniedList string `yaml:"subscriptions_denied_list"` + WebsocketIdleTimeoutSeconds int `yaml:"websocket_idle_timeout_seconds"` } `yaml:"server"` Redis struct { Host string `yaml:"host"` diff --git a/bbb-graphql-middleware/config/config.yml b/bbb-graphql-middleware/config/config.yml index 0cda076e7c..3f0ecadaae 100644 --- a/bbb-graphql-middleware/config/config.yml +++ b/bbb-graphql-middleware/config/config.yml @@ -12,6 +12,7 @@ server: json_patch_disabled: false subscriptions_allowed_list: subscriptions_denied_list: + websocket_idle_timeout_seconds: 60 redis: host: 127.0.0.1 port: 6379 diff --git a/bbb-graphql-middleware/internal/common/types.go b/bbb-graphql-middleware/internal/common/types.go index 86852ea617..ead7ca1c09 100644 --- a/bbb-graphql-middleware/internal/common/types.go +++ b/bbb-graphql-middleware/internal/common/types.go @@ -6,6 +6,7 @@ import ( "github.com/sirupsen/logrus" "net/http" "sync" + "time" "nhooyr.io/websocket" ) @@ -56,6 +57,8 @@ type BrowserConnection struct { FromBrowserToHasuraChannel *SafeChannelByte // channel to transmit messages from Browser to Hasura FromBrowserToGqlActionsChannel *SafeChannelByte // channel to transmit messages from Browser to Graphq-Actions FromHasuraToBrowserChannel *SafeChannelByte // channel to transmit messages from Hasura/GqlActions to Browser + LastBrowserMessageTime time.Time // stores the time of the last message to control browser idleness + LastBrowserMessageTimeMutex sync.RWMutex // mutex for LastBrowserMessageTime Logger *logrus.Entry // connection logger populated with connection info } diff --git a/bbb-graphql-middleware/internal/hasura/client.go b/bbb-graphql-middleware/internal/hasura/client.go index bdba48c6ae..ae0c8c7c7d 100644 --- a/bbb-graphql-middleware/internal/hasura/client.go +++ b/bbb-graphql-middleware/internal/hasura/client.go @@ -70,6 +70,7 @@ func HasuraClient( defer func() { //When Hasura sends an CloseError, it will forward the error to the browser and close the connection if thisConnection.WebsocketCloseError != nil { + browserConnection.Logger.Infof("Closing browser connection because Hasura connection was closed, reason: %s", thisConnection.WebsocketCloseError.Reason) browserConnection.Websocket.Close(thisConnection.WebsocketCloseError.Code, thisConnection.WebsocketCloseError.Reason) browserConnection.ContextCancelFunc() } diff --git a/bbb-graphql-middleware/internal/websrv/connhandler.go b/bbb-graphql-middleware/internal/websrv/connhandler.go index 125ba6171a..744f9c52f1 100644 --- a/bbb-graphql-middleware/internal/websrv/connhandler.go +++ b/bbb-graphql-middleware/internal/websrv/connhandler.go @@ -98,6 +98,7 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) { FromBrowserToHasuraChannel: common.NewSafeChannelByte(bufferSize), FromBrowserToGqlActionsChannel: common.NewSafeChannelByte(bufferSize), FromHasuraToBrowserChannel: common.NewSafeChannelByte(bufferSize), + LastBrowserMessageTime: time.Now(), Logger: connectionLogger, } @@ -494,3 +495,27 @@ func disconnectWithError( return } + +var websocketIdleTimeoutSeconds = config.GetConfig().Server.WebsocketIdleTimeoutSeconds + +func InvalidateIdleBrowserConnectionsRoutine() { + for { + time.Sleep(15 * time.Second) + + BrowserConnectionsMutex.RLock() + for _, browserConnection := range BrowserConnections { + browserConnection.LastBrowserMessageTimeMutex.RLock() + browserIdleSince := time.Since(browserConnection.LastBrowserMessageTime) + browserConnection.LastBrowserMessageTimeMutex.RUnlock() + + if browserIdleSince > time.Duration(websocketIdleTimeoutSeconds)*time.Second { + browserConnection.Logger.Info("Closing browser connection, reason: idle timeout") + errCloseWs := browserConnection.Websocket.Close(websocket.StatusNormalClosure, "idle timeout") + if errCloseWs != nil { + browserConnection.Logger.Debugf("Error on close websocket: %v", errCloseWs) + } + } + } + BrowserConnectionsMutex.RUnlock() + } +} diff --git a/bbb-graphql-middleware/internal/websrv/reader/reader.go b/bbb-graphql-middleware/internal/websrv/reader/reader.go index a5f2fd9c0c..5d3b66d565 100644 --- a/bbb-graphql-middleware/internal/websrv/reader/reader.go +++ b/bbb-graphql-middleware/internal/websrv/reader/reader.go @@ -35,6 +35,7 @@ func BrowserConnectionReader( for { messageType, message, err := browserConnection.Websocket.Read(browserConnection.Context) + if err != nil { if errors.Is(err, context.Canceled) { browserConnection.Logger.Debugf("Closing Browser ws connection as Context was cancelled!") @@ -45,6 +46,9 @@ func BrowserConnectionReader( } browserConnection.Logger.Tracef("received from browser: %s", string(message)) + browserConnection.LastBrowserMessageTimeMutex.Lock() + browserConnection.LastBrowserMessageTime = time.Now() + browserConnection.LastBrowserMessageTimeMutex.Unlock() if messageType != websocket.MessageText { browserConnection.Logger.Warnf("received non-text message: %v", messageType)