server: close subscriptions (with 1012) on metadata change

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9159
GitOrigin-RevId: 5ab25ef57782c126de8852f3546809feb66a9d44
This commit is contained in:
paritosh-08 2023-06-13 14:52:36 +05:30 committed by hasura-bot
parent 4a269b84fc
commit 14b4de37f7
25 changed files with 203 additions and 67 deletions

View File

@ -206,6 +206,20 @@ for JSON encoding-decoding.
| **Default** | `false` | | **Default** | `false` |
| **Supported in** | CE, Enterprise Edition | | **Supported in** | CE, Enterprise Edition |
### Close WebSocket connections on metadata change
When metadata changes, close all WebSocket connections (with error code `1012`). This is useful when you want to ensure
that all clients reconnect to the latest metadata.
| | |
| ------------------- | ----------------------------------------------------- |
| **Flag** | `--disable-close-websockets-on-metadata-change` |
| **Env var** | `HASURA_GRAPHQL_CLOSE_WEBSOCKETS_ON_METADATA_CHANGE` |
| **Accepted values** | Boolean |
| **Options** | `true` or `false` |
| **Default** | `true` |
| **Supported in** | CE, Enterprise Edition, Cloud |
### Connections per Read-Replica ### Connections per Read-Replica
The maximum number of Postgres connections per [read-replica](databases/database-config/read-replicas.mdx) that can be The maximum number of Postgres connections per [read-replica](databases/database-config/read-replicas.mdx) that can be

View File

@ -308,7 +308,8 @@ serveOptions =
soDefaultNamingConvention = Init._default Init.defaultNamingConventionOption, soDefaultNamingConvention = Init._default Init.defaultNamingConventionOption,
soExtensionsSchema = ExtensionsSchema "public", soExtensionsSchema = ExtensionsSchema "public",
soMetadataDefaults = emptyMetadataDefaults, soMetadataDefaults = emptyMetadataDefaults,
soApolloFederationStatus = ApolloFederationDisabled soApolloFederationStatus = ApolloFederationDisabled,
soCloseWebsocketsOnMetadataChangeStatus = Init._default Init.closeWebsocketsOnMetadataChangeOption
} }
-- | What log level should be used by the engine; this is not exported, and -- | What log level should be used by the engine; this is not exported, and

View File

@ -22,7 +22,6 @@ import Harness.Logging.Messages
import Harness.Services.Composed qualified as Services import Harness.Services.Composed qualified as Services
import Harness.Test.BackendType import Harness.Test.BackendType
import Hasura.Prelude import Hasura.Prelude
import Network.WebSockets qualified as WS
-- | static information across an entire test suite run -- | static information across an entire test suite run
data GlobalTestEnvironment = GlobalTestEnvironment data GlobalTestEnvironment = GlobalTestEnvironment
@ -88,7 +87,7 @@ instance Show GlobalTestEnvironment where
-- | How should we make requests to `graphql-engine`? Both WebSocket- and HTTP- -- | How should we make requests to `graphql-engine`? Both WebSocket- and HTTP-
-- based requests are supported. -- based requests are supported.
data Protocol = HTTP | WebSocket WS.Connection data Protocol = HTTP | WebSocket
-- | Credentials for our testing modes. See 'SpecHook.setupTestingMode' for the -- | Credentials for our testing modes. See 'SpecHook.setupTestingMode' for the
-- practical consequences of this type. -- practical consequences of this type.

View File

@ -69,7 +69,7 @@ import Harness.Http qualified as Http
import Harness.Logging import Harness.Logging
import Harness.Quoter.Yaml (fromYaml, yaml) import Harness.Quoter.Yaml (fromYaml, yaml)
import Harness.Services.GraphqlEngine import Harness.Services.GraphqlEngine
import Harness.TestEnvironment (Protocol (..), Server (..), TestEnvironment (..), TestingRole (..), getServer, requestProtocol, serverUrl, traceIf) import Harness.TestEnvironment (Protocol (..), Server (..), TestEnvironment (..), TestingRole (..), getServer, requestProtocol, server, serverUrl, traceIf)
import Harness.WebSockets (responseListener, sendMessages) import Harness.WebSockets (responseListener, sendMessages)
import Hasura.App qualified as App import Hasura.App qualified as App
import Hasura.Logging (Hasura) import Hasura.Logging (Hasura)
@ -150,38 +150,46 @@ postGraphqlViaHttpOrWebSocketWithHeadersStatus ::
(HasCallStack) => Int -> TestEnvironment -> Http.RequestHeaders -> Value -> IO Value (HasCallStack) => Int -> TestEnvironment -> Http.RequestHeaders -> Value -> IO Value
postGraphqlViaHttpOrWebSocketWithHeadersStatus statusCode testEnv headers requestBody = do postGraphqlViaHttpOrWebSocketWithHeadersStatus statusCode testEnv headers requestBody = do
withFrozenCallStack $ case requestProtocol (globalEnvironment testEnv) of withFrozenCallStack $ case requestProtocol (globalEnvironment testEnv) of
WebSocket connection -> postWithHeadersStatusViaWebSocket testEnv connection (addAuthzHeaders testEnv headers) requestBody WebSocket -> postWithHeadersStatusViaWebSocket testEnv (addAuthzHeaders testEnv headers) requestBody
HTTP -> postWithHeadersStatus statusCode testEnv "/v1/graphql" headers requestBody HTTP -> postWithHeadersStatus statusCode testEnv "/v1/graphql" headers requestBody
-- | Post some JSON to graphql-engine, getting back more JSON, via websockets. -- | Post some JSON to graphql-engine, getting back more JSON, via websockets.
-- --
-- This will be used by 'postWithHeadersStatus' if the 'TestEnvironment' sets -- This will be used by 'postWithHeadersStatus' if the 'TestEnvironment' sets
-- the 'requestProtocol' to 'WebSocket'. -- the 'requestProtocol' to 'WebSocket'.
postWithHeadersStatusViaWebSocket :: TestEnvironment -> WS.Connection -> Http.RequestHeaders -> Value -> IO Value postWithHeadersStatusViaWebSocket :: TestEnvironment -> Http.RequestHeaders -> Value -> IO Value
postWithHeadersStatusViaWebSocket testEnv connection headers requestBody = do postWithHeadersStatusViaWebSocket testEnv headers requestBody = do
let preparedHeaders :: HashMap Text ByteString let preparedHeaders :: HashMap Text ByteString
preparedHeaders = preparedHeaders =
HashMap.fromList HashMap.fromList
[ (decodeUtf8 (original key), value) [ (decodeUtf8 (original key), value)
| (key, value) <- headers | (key, value) <- headers
] ]
sendMessages server' = server (globalEnvironment testEnv)
testEnv port' = port server'
connection host = T.unpack (T.drop 7 (T.pack (urlPrefix server')))
[ object path = "/v1/graphql"
[ "type" .= String "connection_init",
"payload" .= object ["headers" .= preparedHeaders]
],
object
[ "id" .= String "some-request-id",
"type" .= String "start",
"payload" .= requestBody
]
]
responseListener testEnv connection \_ type' payload -> do -- paritosh: We are initiating a websocket connection for each request and sending the request instead of initiating a
when (type' `notElem` ["data", "error"]) $ fail ("Websocket message type " <> T.unpack type' <> " received. Payload: " <> Char8.unpack (encode payload)) -- websocket connection for the test and reusing the connection for requests. This is done to avoid managing the
pure payload -- connection (as we do have metadata changes as part of test and HGE closes websockets on metadata changes).
WS.runClient host (fromIntegral port') path \connection -> do
sendMessages
testEnv
connection
[ object
[ "type" .= String "connection_init",
"payload" .= object ["headers" .= preparedHeaders]
],
object
[ "id" .= String "some-request-id",
"type" .= String "start",
"payload" .= requestBody
]
]
responseListener testEnv connection \_ type' payload -> do
when (type' `notElem` ["data", "error"]) $ fail ("Websocket message type " <> T.unpack type' <> " received. Payload: " <> Char8.unpack (encode payload))
pure payload
-- | Post some JSON to graphql-engine, getting back more JSON. -- | Post some JSON to graphql-engine, getting back more JSON.
-- --

View File

@ -3,11 +3,9 @@ module Harness.Test.Protocol
) )
where where
import GHC.Word (Word16)
import Harness.TestEnvironment (GlobalTestEnvironment, Protocol (..)) import Harness.TestEnvironment (GlobalTestEnvironment, Protocol (..))
import Harness.TestEnvironment qualified as TestEnvironment import Harness.TestEnvironment qualified as TestEnvironment
import Hasura.Prelude import Hasura.Prelude
import Network.WebSockets qualified as WS
import Test.Hspec (ActionWith, SpecWith, aroundAllWith, describe) import Test.Hspec (ActionWith, SpecWith, aroundAllWith, describe)
import Test.Hspec.Core.Spec (Item (..), mapSpecItem_) import Test.Hspec.Core.Spec (Item (..), mapSpecItem_)
@ -25,14 +23,10 @@ withEachProtocol spec = do
let connectWS :: ActionWith GlobalTestEnvironment -> ActionWith GlobalTestEnvironment let connectWS :: ActionWith GlobalTestEnvironment -> ActionWith GlobalTestEnvironment
connectWS k globalTestEnvironment = do connectWS k globalTestEnvironment = do
let port' :: Word16 k
port' = TestEnvironment.port (TestEnvironment.server globalTestEnvironment) globalTestEnvironment
{ TestEnvironment.requestProtocol = WebSocket
WS.runClient "127.0.0.1" (fromIntegral port') "/v1/graphql" \connection -> }
k
globalTestEnvironment
{ TestEnvironment.requestProtocol = WebSocket connection
}
aroundAllWith connectWS $ describe "Over WebSockets" $ flip mapSpecItem_ spec \item -> aroundAllWith connectWS $ describe "Over WebSockets" $ flip mapSpecItem_ spec \item ->
item {itemExample = \params -> itemExample item params} item {itemExample = \params -> itemExample item params}

View File

@ -508,6 +508,7 @@ initialiseAppContext env serveOptions@ServeOptions {..} AppInit {..} = do
soDefaultNamingConvention soDefaultNamingConvention
soMetadataDefaults soMetadataDefaults
soApolloFederationStatus soApolloFederationStatus
soCloseWebsocketsOnMetadataChangeStatus
-- Create the schema cache -- Create the schema cache
rebuildableSchemaCache <- rebuildableSchemaCache <-

View File

@ -158,7 +158,8 @@ data AppContext = AppContext
acEnableTelemetry :: TelemetryStatus, acEnableTelemetry :: TelemetryStatus,
acEventEngineCtx :: EventEngineCtx, acEventEngineCtx :: EventEngineCtx,
acAsyncActionsFetchInterval :: OptionalInterval, acAsyncActionsFetchInterval :: OptionalInterval,
acApolloFederationStatus :: ApolloFederationStatus acApolloFederationStatus :: ApolloFederationStatus,
acCloseWebsocketsOnMetadataChangeStatus :: CloseWebsocketsOnMetadataChangeStatus
} }
-- | Collection of the LoggerCtx, the regular Logger and the PGLogger -- | Collection of the LoggerCtx, the regular Logger and the PGLogger
@ -268,7 +269,8 @@ buildAppContextRule = proc (ServeOptions {..}, env, _keys) -> do
acEnableTelemetry = soEnableTelemetry, acEnableTelemetry = soEnableTelemetry,
acEventEngineCtx = eventEngineCtx, acEventEngineCtx = eventEngineCtx,
acAsyncActionsFetchInterval = soAsyncActionsFetchInterval, acAsyncActionsFetchInterval = soAsyncActionsFetchInterval,
acApolloFederationStatus = soApolloFederationStatus acApolloFederationStatus = soApolloFederationStatus,
acCloseWebsocketsOnMetadataChangeStatus = soCloseWebsocketsOnMetadataChangeStatus
} }
where where
buildSqlGenCtx = Inc.cache proc (experimentalFeatures, stringifyNum, dangerousBooleanCollapse) -> do buildSqlGenCtx = Inc.cache proc (experimentalFeatures, stringifyNum, dangerousBooleanCollapse) -> do
@ -340,5 +342,6 @@ buildCacheDynamicConfig AppContext {..} = do
_cdcExperimentalFeatures = acExperimentalFeatures, _cdcExperimentalFeatures = acExperimentalFeatures,
_cdcDefaultNamingConvention = acDefaultNamingConvention, _cdcDefaultNamingConvention = acDefaultNamingConvention,
_cdcMetadataDefaults = acMetadataDefaults, _cdcMetadataDefaults = acMetadataDefaults,
_cdcApolloFederationStatus = acApolloFederationStatus _cdcApolloFederationStatus = acApolloFederationStatus,
_cdcCloseWebsocketsOnMetadataChangeStatus = acCloseWebsocketsOnMetadataChangeStatus
} }

View File

@ -584,7 +584,7 @@ removeStreamingQuery logger serverMetrics prometheusMetrics subscriptionState (S
(GaugeVector.dec numSubscriptionMetric promMetricGranularLabel) (GaugeVector.dec numSubscriptionMetric promMetricGranularLabel)
(GaugeVector.dec numSubscriptionMetric promMetricLabel) (GaugeVector.dec numSubscriptionMetric promMetricLabel)
-- | An async action query whose relationships are refered to table in a source. -- | An async action query whose relationships are referred to table in a source.
-- We need to generate an SQL statement with the action response and execute it -- We need to generate an SQL statement with the action response and execute it
-- in the source database so as to fetch response joined with relationship rows. -- in the source database so as to fetch response joined with relationship rows.
-- For more details see Note [Resolving async action query] -- For more details see Note [Resolving async action query]

View File

@ -17,6 +17,9 @@ module Hasura.GraphQL.Transport.WebSocket
onClose, onClose,
sendMsg, sendMsg,
sendCloseWithMsg, sendCloseWithMsg,
mkCloseWebsocketsOnMetadataChangeAction,
runWebsocketCloseOnMetadataChangeAction,
WebsocketCloseOnMetadataChangeAction,
) )
where where
@ -68,6 +71,7 @@ import Hasura.GraphQL.Transport.Instances ()
import Hasura.GraphQL.Transport.WebSocket.Protocol import Hasura.GraphQL.Transport.WebSocket.Protocol
import Hasura.GraphQL.Transport.WebSocket.Server qualified as WS import Hasura.GraphQL.Transport.WebSocket.Server qualified as WS
import Hasura.GraphQL.Transport.WebSocket.Types import Hasura.GraphQL.Transport.WebSocket.Types
import Hasura.GraphQL.Transport.WebSocket.Types qualified as WS
import Hasura.Logging qualified as L import Hasura.Logging qualified as L
import Hasura.Metadata.Class import Hasura.Metadata.Class
import Hasura.Prelude import Hasura.Prelude
@ -1213,3 +1217,18 @@ onClose logger serverMetrics prometheusMetrics subscriptionsState wsConn granula
StreamingQuerySubscriber streamSubscriberId -> ES.removeStreamingQuery logger serverMetrics prometheusMetrics subscriptionsState streamSubscriberId granularPrometheusMetricsState operationName StreamingQuerySubscriber streamSubscriberId -> ES.removeStreamingQuery logger serverMetrics prometheusMetrics subscriptionsState streamSubscriberId granularPrometheusMetricsState operationName
where where
opMap = _wscOpMap $ WS.getData wsConn opMap = _wscOpMap $ WS.getData wsConn
newtype WebsocketCloseOnMetadataChangeAction = WebsocketCloseOnMetadataChangeAction
{ runWebsocketCloseOnMetadataChangeAction :: IO ()
}
-- | By default, we close all the websocket connections when the metadata changes. This function is used to create the
-- action that will be run when the metadata changes.
mkCloseWebsocketsOnMetadataChangeAction :: WS.WSServer WS.WSConnData -> WebsocketCloseOnMetadataChangeAction
mkCloseWebsocketsOnMetadataChangeAction wsServer =
WebsocketCloseOnMetadataChangeAction
$ WS.closeAllConnectionsWithReason
wsServer
"Closing all websocket connections as the metadata has changed"
"Server state changed, restarting the server"
id

View File

@ -24,6 +24,7 @@ module Hasura.GraphQL.Transport.WebSocket.Server
sendMsgAndCloseConn, sendMsgAndCloseConn,
createServerApp, createServerApp,
createWSServer, createWSServer,
closeAllConnectionsWithReason,
getData, getData,
getRawWebSocketConnection, getRawWebSocketConnection,
getWSId, getWSId,
@ -278,6 +279,22 @@ closeAllWith ::
closeAllWith closer msg conns = closeAllWith closer msg conns =
void $ A.mapConcurrently (closer msg . snd) conns void $ A.mapConcurrently (closer msg . snd) conns
closeAllConnectionsWithReason ::
WSServer a ->
String ->
BL.ByteString ->
(SecuritySensitiveUserConfig -> SecuritySensitiveUserConfig) ->
IO ()
closeAllConnectionsWithReason (WSServer (L.Logger writeLog) userConfRef serverStatus) logMsg reason updateConf = do
writeLog
$ WSReaperThreadLog
$ fromString
$ logMsg
conns <- STM.atomically $ do
STM.modifyTVar' userConfRef updateConf
flushConnMap serverStatus
closeAllWith (flip forceConnReconnect) reason conns
-- | Resets the current connections map to an empty one if the server is -- | Resets the current connections map to an empty one if the server is
-- running and returns the list of connections that were in the map -- running and returns the list of connections that were in the map
-- before flushing it. -- before flushing it.
@ -354,7 +371,7 @@ data WSHandlers m a = WSHandlers
-- stringify big query numeric, experimental features and invalidates/closes all -- stringify big query numeric, experimental features and invalidates/closes all
-- connections if there are any changes. -- connections if there are any changes.
websocketConnectionReaper :: IO (AuthMode, AllowListStatus, CorsPolicy, SQLGenCtx, Set.HashSet ExperimentalFeature, NamingCase) -> IO SchemaCache -> WSServer a -> IO Void websocketConnectionReaper :: IO (AuthMode, AllowListStatus, CorsPolicy, SQLGenCtx, Set.HashSet ExperimentalFeature, NamingCase) -> IO SchemaCache -> WSServer a -> IO Void
websocketConnectionReaper getLatestConfig getSchemaCache (WSServer (L.Logger writeLog) userConfRef serverStatus) = websocketConnectionReaper getLatestConfig getSchemaCache ws@(WSServer _ userConfRef _) =
forever $ do forever $ do
(currAuthMode, currEnableAllowlist, currCorsPolicy, currSqlGenCtx, currExperimentalFeatures, currDefaultNamingCase) <- getLatestConfig (currAuthMode, currEnableAllowlist, currCorsPolicy, currSqlGenCtx, currExperimentalFeatures, currDefaultNamingCase) <- getLatestConfig
currAllowlist <- scAllowlist <$> getSchemaCache currAllowlist <- scAllowlist <$> getSchemaCache
@ -370,21 +387,6 @@ websocketConnectionReaper getLatestConfig getSchemaCache (WSServer (L.Logger wri
(currDefaultNamingCase, prevDefaultNamingCase) (currDefaultNamingCase, prevDefaultNamingCase)
sleep $ seconds 1 sleep $ seconds 1
where where
closeAllConnectionsWithReason ::
String ->
BL.ByteString ->
(SecuritySensitiveUserConfig -> SecuritySensitiveUserConfig) ->
IO ()
closeAllConnectionsWithReason logMsg reason updateConf = do
writeLog
$ WSReaperThreadLog
$ fromString
$ logMsg
conns <- STM.atomically $ do
STM.modifyTVar' userConfRef updateConf
flushConnMap serverStatus
closeAllWith (flip forceConnReconnect) reason conns
-- Close all connections based on - -- Close all connections based on -
-- if CorsPolicy changed -> close -- if CorsPolicy changed -> close
-- if AuthMode changed -> close -- if AuthMode changed -> close
@ -419,12 +421,14 @@ websocketConnectionReaper getLatestConfig getSchemaCache (WSServer (L.Logger wri
-- if CORS policy has changed, close all connections -- if CORS policy has changed, close all connections
| hasCorsPolicyChanged -> | hasCorsPolicyChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the cors policy changed" "closing all websocket connections as the cors policy changed"
"cors policy changed" "cors policy changed"
(\conf -> conf {ssucCorsPolicy = currCorsPolicy}) (\conf -> conf {ssucCorsPolicy = currCorsPolicy})
-- if any auth config has changed, close all connections -- if any auth config has changed, close all connections
| hasAuthModeChanged -> | hasAuthModeChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the auth mode changed" "closing all websocket connections as the auth mode changed"
"auth mode changed" "auth mode changed"
(\conf -> conf {ssucAuthMode = currAuthMode}) (\conf -> conf {ssucAuthMode = currAuthMode})
@ -434,6 +438,7 @@ websocketConnectionReaper getLatestConfig getSchemaCache (WSServer (L.Logger wri
-- connections. -- connections.
| hasAllowlistEnabled -> | hasAllowlistEnabled ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as allow list is enabled" "closing all websocket connections as allow list is enabled"
"allow list enabled" "allow list enabled"
(\conf -> conf {ssucEnableAllowlist = currEnableAllowlist}) (\conf -> conf {ssucEnableAllowlist = currEnableAllowlist})
@ -441,42 +446,49 @@ websocketConnectionReaper getLatestConfig getSchemaCache (WSServer (L.Logger wri
-- allowlist, we need to close all the connections. -- allowlist, we need to close all the connections.
| hasAllowlistUpdated -> | hasAllowlistUpdated ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the allow list has been updated" "closing all websocket connections as the allow list has been updated"
"allow list updated" "allow list updated"
(\conf -> conf {ssucAllowlist = currAllowlist}) (\conf -> conf {ssucAllowlist = currAllowlist})
-- if HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES has changed, close all connections -- if HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES has changed, close all connections
| hasStringifyNumChanged -> | hasStringifyNumChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES setting changed" "closing all websocket connections as the HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES setting changed"
"HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES env var changed" "HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES env var changed"
(\conf -> conf {ssucSQLGenCtx = currSqlGenCtx}) (\conf -> conf {ssucSQLGenCtx = currSqlGenCtx})
-- if HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE has changed, close all connections -- if HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE has changed, close all connections
| hasDangerousBooleanCollapseChanged -> | hasDangerousBooleanCollapseChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE setting changed" "closing all websocket connections as the HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE setting changed"
"HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE env var changed" "HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE env var changed"
(\conf -> conf {ssucSQLGenCtx = currSqlGenCtx}) (\conf -> conf {ssucSQLGenCtx = currSqlGenCtx})
-- if 'bigquery_string_numeric_input' option added/removed from experimental features, close all connections -- if 'bigquery_string_numeric_input' option added/removed from experimental features, close all connections
| hasBigqueryStringNumericInputChanged -> | hasBigqueryStringNumericInputChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the 'bigquery_string_numeric_input' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES" "closing all websocket connections as the 'bigquery_string_numeric_input' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES"
"'bigquery_string_numeric_input' removed/added in HASURA_GRAPHQL_EXPERIMENTAL_FEATURES env var" "'bigquery_string_numeric_input' removed/added in HASURA_GRAPHQL_EXPERIMENTAL_FEATURES env var"
(\conf -> conf {ssucSQLGenCtx = currSqlGenCtx}) (\conf -> conf {ssucSQLGenCtx = currSqlGenCtx})
-- if 'hide_aggregation_predicates' option added/removed from experimental features, close all connections -- if 'hide_aggregation_predicates' option added/removed from experimental features, close all connections
| hasHideAggregationPredicatesChanged -> | hasHideAggregationPredicatesChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the 'hide-aggregation-predicates' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES" "closing all websocket connections as the 'hide-aggregation-predicates' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES"
"'hide-aggregation-predicates' removed/added in HASURA_GRAPHQL_EXPERIMENTAL_FEATURES env var" "'hide-aggregation-predicates' removed/added in HASURA_GRAPHQL_EXPERIMENTAL_FEATURES env var"
(\conf -> conf {ssucExperimentalFeatures = currExperimentalFeatures}) (\conf -> conf {ssucExperimentalFeatures = currExperimentalFeatures})
-- if 'hide_stream_fields' option added/removed from experimental features, close all connections -- if 'hide_stream_fields' option added/removed from experimental features, close all connections
| hasHideStreamFieldsChanged -> | hasHideStreamFieldsChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the 'hide-stream-fields' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES" "closing all websocket connections as the 'hide-stream-fields' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES"
"'hide-stream-fields' removed/added in HASURA_GRAPHQL_EXPERIMENTAL_FEATURES env var" "'hide-stream-fields' removed/added in HASURA_GRAPHQL_EXPERIMENTAL_FEATURES env var"
(\conf -> conf {ssucExperimentalFeatures = currExperimentalFeatures}) (\conf -> conf {ssucExperimentalFeatures = currExperimentalFeatures})
-- if naming convention has been changed, close all connections -- if naming convention has been changed, close all connections
| hasDefaultNamingCaseChanged -> | hasDefaultNamingCaseChanged ->
closeAllConnectionsWithReason closeAllConnectionsWithReason
ws
"closing all websocket connections as the 'naming_convention' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES and the HASURA_GRAPHQL_DEFAULT_NAMING_CONVENTION has changed" "closing all websocket connections as the 'naming_convention' option has been added/removed from HASURA_GRAPHQL_EXPERIMENTAL_FEATURES and the HASURA_GRAPHQL_DEFAULT_NAMING_CONVENTION has changed"
"naming convention has been changed" "naming convention has been changed"
(\conf -> conf {ssucExperimentalFeatures = currExperimentalFeatures, ssucDefaultNamingCase = currDefaultNamingCase}) (\conf -> conf {ssucExperimentalFeatures = currExperimentalFeatures, ssucDefaultNamingCase = currDefaultNamingCase})

View File

@ -72,6 +72,7 @@ data CacheDynamicConfig = CacheDynamicConfig
_cdcExperimentalFeatures :: HashSet ExperimentalFeature, _cdcExperimentalFeatures :: HashSet ExperimentalFeature,
_cdcDefaultNamingConvention :: NamingCase, _cdcDefaultNamingConvention :: NamingCase,
_cdcMetadataDefaults :: MetadataDefaults, _cdcMetadataDefaults :: MetadataDefaults,
_cdcApolloFederationStatus :: ApolloFederationStatus _cdcApolloFederationStatus :: ApolloFederationStatus,
_cdcCloseWebsocketsOnMetadataChangeStatus :: CloseWebsocketsOnMetadataChangeStatus
} }
deriving (Eq) deriving (Eq)

View File

@ -18,6 +18,7 @@ import Hasura.Base.Error
import Hasura.EncJSON import Hasura.EncJSON
import Hasura.Eventing.Backend import Hasura.Eventing.Backend
import Hasura.Function.API qualified as Functions import Hasura.Function.API qualified as Functions
import Hasura.GraphQL.Transport.WebSocket qualified as WS
import Hasura.Logging qualified as L import Hasura.Logging qualified as L
import Hasura.LogicalModel.API qualified as LogicalModel import Hasura.LogicalModel.API qualified as LogicalModel
import Hasura.Metadata.Class import Hasura.Metadata.Class
@ -105,9 +106,10 @@ runMetadataQuery ::
) => ) =>
AppContext -> AppContext ->
RebuildableSchemaCache -> RebuildableSchemaCache ->
WS.WebsocketCloseOnMetadataChangeAction ->
RQLMetadata -> RQLMetadata ->
m (EncJSON, RebuildableSchemaCache) m (EncJSON, RebuildableSchemaCache)
runMetadataQuery appContext schemaCache RQLMetadata {..} = do runMetadataQuery appContext schemaCache closeWebsocketsOnMetadataChange RQLMetadata {..} = do
AppEnv {..} <- askAppEnv AppEnv {..} <- askAppEnv
let logger = _lsLogger appEnvLoggers let logger = _lsLogger appEnvLoggers
MetadataWithResourceVersion metadata currentResourceVersion <- Tracing.newSpan "fetchMetadata" $ liftEitherM fetchMetadata MetadataWithResourceVersion metadata currentResourceVersion <- Tracing.newSpan "fetchMetadata" $ liftEitherM fetchMetadata
@ -184,6 +186,13 @@ runMetadataQuery appContext schemaCache RQLMetadata {..} = do
$ setMetadataResourceVersionInSchemaCache newResourceVersion $ setMetadataResourceVersionInSchemaCache newResourceVersion
& runCacheRWT dynamicConfig modSchemaCache & runCacheRWT dynamicConfig modSchemaCache
-- Close all subscriptions with 1012 code (subscribers should reconnect)
-- and close poller threads
when ((_cdcCloseWebsocketsOnMetadataChangeStatus dynamicConfig) == CWMCEnabled)
$ Tracing.newSpan "closeWebsocketsOnMetadataChange"
$ liftIO
$ WS.runWebsocketCloseOnMetadataChangeAction closeWebsocketsOnMetadataChange
pure (r, modSchemaCache') pure (r, modSchemaCache')
(MaintenanceModeEnabled (), ReadOnlyModeDisabled) -> (MaintenanceModeEnabled (), ReadOnlyModeDisabled) ->
throw500 "metadata cannot be modified in maintenance mode" throw500 "metadata cannot be modified in maintenance mode"

View File

@ -66,6 +66,7 @@ import Hasura.GraphQL.Logging (MonadExecutionLog, MonadQueryLog)
import Hasura.GraphQL.Transport.HTTP qualified as GH import Hasura.GraphQL.Transport.HTTP qualified as GH
import Hasura.GraphQL.Transport.HTTP.Protocol qualified as GH import Hasura.GraphQL.Transport.HTTP.Protocol qualified as GH
import Hasura.GraphQL.Transport.WSServerApp qualified as WS import Hasura.GraphQL.Transport.WSServerApp qualified as WS
import Hasura.GraphQL.Transport.WebSocket qualified as WS
import Hasura.GraphQL.Transport.WebSocket.Server qualified as WS import Hasura.GraphQL.Transport.WebSocket.Server qualified as WS
import Hasura.GraphQL.Transport.WebSocket.Types qualified as WS import Hasura.GraphQL.Transport.WebSocket.Types qualified as WS
import Hasura.HTTP import Hasura.HTTP
@ -495,9 +496,10 @@ v1MetadataHandler ::
UserInfoM m UserInfoM m
) => ) =>
((RebuildableSchemaCache -> m (EncJSON, RebuildableSchemaCache)) -> m EncJSON) -> ((RebuildableSchemaCache -> m (EncJSON, RebuildableSchemaCache)) -> m EncJSON) ->
WS.WebsocketCloseOnMetadataChangeAction ->
RQLMetadata -> RQLMetadata ->
m (HttpResponse EncJSON) m (HttpResponse EncJSON)
v1MetadataHandler schemaCacheRefUpdater query = Tracing.newSpan "Metadata" $ do v1MetadataHandler schemaCacheRefUpdater closeWebsocketsOnMetadataChangeAction query = Tracing.newSpan "Metadata" $ do
(liftEitherM . authorizeV1MetadataApi query) =<< ask (liftEitherM . authorizeV1MetadataApi query) =<< ask
appContext <- asks hcAppContext appContext <- asks hcAppContext
r <- r <-
@ -505,6 +507,7 @@ v1MetadataHandler schemaCacheRefUpdater query = Tracing.newSpan "Metadata" $ do
runMetadataQuery runMetadataQuery
appContext appContext
schemaCache schemaCache
closeWebsocketsOnMetadataChangeAction
query query
pure $ HttpResponse r [] pure $ HttpResponse r []
@ -805,6 +808,7 @@ mkWaiApp setupHook appStateRef consoleType ekgStore wsServerEnv = do
Spock.spockAsApp Spock.spockAsApp
$ Spock.spockT lowerIO $ Spock.spockT lowerIO
$ httpApp setupHook appStateRef appEnv consoleType ekgStore $ httpApp setupHook appStateRef appEnv consoleType ekgStore
$ WS.mkCloseWebsocketsOnMetadataChangeAction (WS._wseServer wsServerEnv)
let wsServerApp = WS.createWSServerApp (_lsEnabledLogTypes appEnvLoggingSettings) wsServerEnv appEnvWebSocketConnectionInitTimeout appEnvLicenseKeyCache let wsServerApp = WS.createWSServerApp (_lsEnabledLogTypes appEnvLoggingSettings) wsServerEnv appEnvWebSocketConnectionInitTimeout appEnvLicenseKeyCache
stopWSServer = WS.stopWSServerApp wsServerEnv stopWSServer = WS.stopWSServerApp wsServerEnv
@ -846,8 +850,9 @@ httpApp ::
AppEnv -> AppEnv ->
ConsoleType m -> ConsoleType m ->
EKG.Store EKG.EmptyMetrics -> EKG.Store EKG.EmptyMetrics ->
WS.WebsocketCloseOnMetadataChangeAction ->
Spock.SpockT m () Spock.SpockT m ()
httpApp setupHook appStateRef AppEnv {..} consoleType ekgStore = do httpApp setupHook appStateRef AppEnv {..} consoleType ekgStore closeWebsocketsOnMetadataChangeAction = do
-- Additional spock action to run -- Additional spock action to run
setupHook appStateRef setupHook appStateRef
@ -970,7 +975,7 @@ httpApp setupHook appStateRef AppEnv {..} consoleType ekgStore = do
$ spockAction encodeQErr id $ spockAction encodeQErr id
$ mkPostHandler $ mkPostHandler
$ fmap (emptyHttpLogGraphQLInfo,) $ fmap (emptyHttpLogGraphQLInfo,)
<$> mkAPIRespHandler (v1MetadataHandler schemaCacheUpdater) <$> mkAPIRespHandler (v1MetadataHandler schemaCacheUpdater closeWebsocketsOnMetadataChangeAction)
Spock.post "v2/query" $ do Spock.post "v2/query" $ do
onlyWhenApiEnabled isMetadataEnabled appStateRef onlyWhenApiEnabled isMetadataEnabled appStateRef

View File

@ -214,6 +214,8 @@ mkServeOptions sor@ServeOptionsRaw {..} = do
soApolloFederationStatus <- do soApolloFederationStatus <- do
apolloFederationStatusOptionM <- withOptionDefault (pure <$> rsoApolloFederationStatus) apolloFederationStatusOption apolloFederationStatusOptionM <- withOptionDefault (pure <$> rsoApolloFederationStatus) apolloFederationStatusOption
pure $ getApolloFederationStatus soExperimentalFeatures apolloFederationStatusOptionM pure $ getApolloFederationStatus soExperimentalFeatures apolloFederationStatusOptionM
soCloseWebsocketsOnMetadataChangeStatus <- do
withOptionDefault rsoCloseWebsocketsOnMetadataChangeStatus closeWebsocketsOnMetadataChangeOption
pure ServeOptions {..} pure ServeOptions {..}
-- | Fetch Postgres 'Query.ConnParams' components from the environment -- | Fetch Postgres 'Query.ConnParams' components from the environment

View File

@ -60,6 +60,7 @@ module Hasura.Server.Init.Arg.Command.Serve
parseMetadataDefaults, parseMetadataDefaults,
metadataDefaultsOption, metadataDefaultsOption,
apolloFederationStatusOption, apolloFederationStatusOption,
closeWebsocketsOnMetadataChangeOption,
-- * Pretty Printer -- * Pretty Printer
serveCmdFooter, serveCmdFooter,
@ -146,6 +147,7 @@ serveCommandParser =
<*> parseExtensionsSchema <*> parseExtensionsSchema
<*> parseMetadataDefaults <*> parseMetadataDefaults
<*> parseApolloFederationStatus <*> parseApolloFederationStatus
<*> parseEnableCloseWebsocketsOnMetadataChange
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Serve Options -- Serve Options
@ -1158,6 +1160,22 @@ parseApolloFederationStatus =
<> Opt.help (Config._helpMessage apolloFederationStatusOption) <> Opt.help (Config._helpMessage apolloFederationStatusOption)
) )
closeWebsocketsOnMetadataChangeOption :: Config.Option (Types.CloseWebsocketsOnMetadataChangeStatus)
closeWebsocketsOnMetadataChangeOption =
Config.Option
{ Config._default = Types.CWMCEnabled,
Config._envVar = "HASURA_GRAPHQL_CLOSE_WEBSOCKETS_ON_METADATA_CHANGE",
Config._helpMessage = "Close all the websocket connections (with error code 1012) on metadata change (default: true)."
}
parseEnableCloseWebsocketsOnMetadataChange :: Opt.Parser (Maybe Types.CloseWebsocketsOnMetadataChangeStatus)
parseEnableCloseWebsocketsOnMetadataChange =
(bool Nothing (Just Types.CWMCDisabled))
<$> Opt.switch
( Opt.long "disable-close-websockets-on-metadata-change"
<> Opt.help (Config._helpMessage closeWebsocketsOnMetadataChangeOption)
)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Pretty Printer -- Pretty Printer
@ -1256,6 +1274,7 @@ serveCmdFooter =
Config.optionPP enableMetadataQueryLoggingOption, Config.optionPP enableMetadataQueryLoggingOption,
Config.optionPP defaultNamingConventionOption, Config.optionPP defaultNamingConventionOption,
Config.optionPP metadataDBExtensionsSchemaOption, Config.optionPP metadataDBExtensionsSchemaOption,
Config.optionPP apolloFederationStatusOption Config.optionPP apolloFederationStatusOption,
Config.optionPP closeWebsocketsOnMetadataChangeOption
] ]
eventEnvs = [Config.optionPP graphqlEventsHttpPoolSizeOption, Config.optionPP graphqlEventsFetchIntervalOption] eventEnvs = [Config.optionPP graphqlEventsHttpPoolSizeOption, Config.optionPP graphqlEventsFetchIntervalOption]

View File

@ -320,7 +320,8 @@ data ServeOptionsRaw impl = ServeOptionsRaw
rsoDefaultNamingConvention :: Maybe NamingCase, rsoDefaultNamingConvention :: Maybe NamingCase,
rsoExtensionsSchema :: Maybe MonadTx.ExtensionsSchema, rsoExtensionsSchema :: Maybe MonadTx.ExtensionsSchema,
rsoMetadataDefaults :: Maybe MetadataDefaults, rsoMetadataDefaults :: Maybe MetadataDefaults,
rsoApolloFederationStatus :: Maybe Server.Types.ApolloFederationStatus rsoApolloFederationStatus :: Maybe Server.Types.ApolloFederationStatus,
rsoCloseWebsocketsOnMetadataChangeStatus :: Maybe Server.Types.CloseWebsocketsOnMetadataChangeStatus
} }
-- | Whether or not to serve Console assets. -- | Whether or not to serve Console assets.
@ -618,7 +619,8 @@ data ServeOptions impl = ServeOptions
soDefaultNamingConvention :: NamingCase, soDefaultNamingConvention :: NamingCase,
soExtensionsSchema :: MonadTx.ExtensionsSchema, soExtensionsSchema :: MonadTx.ExtensionsSchema,
soMetadataDefaults :: MetadataDefaults, soMetadataDefaults :: MetadataDefaults,
soApolloFederationStatus :: Server.Types.ApolloFederationStatus soApolloFederationStatus :: Server.Types.ApolloFederationStatus,
soCloseWebsocketsOnMetadataChangeStatus :: Server.Types.CloseWebsocketsOnMetadataChangeStatus
} }
-- | 'ResponseInternalErrorsConfig' represents the encoding of the -- | 'ResponseInternalErrorsConfig' represents the encoding of the

View File

@ -372,3 +372,6 @@ instance FromEnv Server.Types.ApolloFederationStatus where
instance FromEnv GranularPrometheusMetricsState where instance FromEnv GranularPrometheusMetricsState where
fromEnv = fmap (bool GranularMetricsOff GranularMetricsOn) . fromEnv @Bool fromEnv = fmap (bool GranularMetricsOff GranularMetricsOn) . fromEnv @Bool
instance FromEnv Server.Types.CloseWebsocketsOnMetadataChangeStatus where
fromEnv = fmap (bool Server.Types.CWMCDisabled Server.Types.CWMCEnabled) . fromEnv @Bool

View File

@ -18,6 +18,8 @@ module Hasura.Server.Types
ApolloFederationStatus (..), ApolloFederationStatus (..),
isApolloFederationEnabled, isApolloFederationEnabled,
GranularPrometheusMetricsState (..), GranularPrometheusMetricsState (..),
CloseWebsocketsOnMetadataChangeStatus (..),
isCloseWebsocketsOnMetadataChangeStatusEnabled,
MonadGetPolicies (..), MonadGetPolicies (..),
) )
where where
@ -180,6 +182,25 @@ instance ToJSON GranularPrometheusMetricsState where
GranularMetricsOff -> Bool False GranularMetricsOff -> Bool False
GranularMetricsOn -> Bool True GranularMetricsOn -> Bool True
-- | Whether or not to close websocket connections on metadata change.
data CloseWebsocketsOnMetadataChangeStatus = CWMCEnabled | CWMCDisabled
deriving stock (Show, Eq, Ord, Generic)
instance NFData CloseWebsocketsOnMetadataChangeStatus
instance Hashable CloseWebsocketsOnMetadataChangeStatus
instance FromJSON CloseWebsocketsOnMetadataChangeStatus where
parseJSON = fmap (bool CWMCDisabled CWMCEnabled) . parseJSON
isCloseWebsocketsOnMetadataChangeStatusEnabled :: CloseWebsocketsOnMetadataChangeStatus -> Bool
isCloseWebsocketsOnMetadataChangeStatusEnabled = \case
CWMCEnabled -> True
CWMCDisabled -> False
instance ToJSON CloseWebsocketsOnMetadataChangeStatus where
toJSON = toJSON . isCloseWebsocketsOnMetadataChangeStatusEnabled
class (Monad m) => MonadGetPolicies m where class (Monad m) => MonadGetPolicies m where
runGetApiTimeLimit :: runGetApiTimeLimit ::
m (Maybe MaxTime) m (Maybe MaxTime)

View File

@ -2096,3 +2096,16 @@ serveParserSpec =
Opt.Success enableApolloFederation -> enableApolloFederation == (Just Types.ApolloFederationEnabled) Opt.Success enableApolloFederation -> enableApolloFederation == (Just Types.ApolloFederationEnabled)
Opt.Failure _pf -> False Opt.Failure _pf -> False
Opt.CompletionInvoked _cr -> False Opt.CompletionInvoked _cr -> False
Hspec.it "It accepts '--disable-close-websockets-on-metadata-change'" $ do
let -- Given
parserInfo = Opt.info (UUT.serveCommandParser @Logging.Hasura Opt.<**> Opt.helper) Opt.fullDesc
-- When
argInput = ["--disable-close-websockets-on-metadata-change"]
-- Then
result = Opt.execParserPure Opt.defaultPrefs parserInfo argInput
fmap UUT.rsoCloseWebsocketsOnMetadataChangeStatus result `Hspec.shouldSatisfy` \case
Opt.Success disableCloseWebsocketsOnMetadataChange -> disableCloseWebsocketsOnMetadataChange == (Just Types.CWMCDisabled)
Opt.Failure _pf -> False
Opt.CompletionInvoked _cr -> False

View File

@ -91,7 +91,8 @@ emptyServeOptionsRaw =
rsoDefaultNamingConvention = Nothing, rsoDefaultNamingConvention = Nothing,
rsoExtensionsSchema = Nothing, rsoExtensionsSchema = Nothing,
rsoMetadataDefaults = Nothing, rsoMetadataDefaults = Nothing,
rsoApolloFederationStatus = Nothing rsoApolloFederationStatus = Nothing,
rsoCloseWebsocketsOnMetadataChangeStatus = Nothing
} }
mkServeOptionsSpec :: Hspec.Spec mkServeOptionsSpec :: Hspec.Spec

View File

@ -90,7 +90,8 @@ serveOptions =
soDefaultNamingConvention = Init._default Init.defaultNamingConventionOption, soDefaultNamingConvention = Init._default Init.defaultNamingConventionOption,
soExtensionsSchema = ExtensionsSchema "public", soExtensionsSchema = ExtensionsSchema "public",
soMetadataDefaults = emptyMetadataDefaults, soMetadataDefaults = emptyMetadataDefaults,
soApolloFederationStatus = ApolloFederationDisabled soApolloFederationStatus = ApolloFederationDisabled,
soCloseWebsocketsOnMetadataChangeStatus = Init._default Init.closeWebsocketsOnMetadataChangeOption
} }
-- | What log level should be used by the engine; this is not exported, and -- | What log level should be used by the engine; this is not exported, and

View File

@ -126,6 +126,7 @@ main = do
(_default defaultNamingConventionOption) (_default defaultNamingConventionOption)
emptyMetadataDefaults emptyMetadataDefaults
ApolloFederationDisabled ApolloFederationDisabled
(_default closeWebsocketsOnMetadataChangeOption)
cacheBuildParams = CacheBuildParams httpManager (mkPgSourceResolver print) mkMSSQLSourceResolver staticConfig cacheBuildParams = CacheBuildParams httpManager (mkPgSourceResolver print) mkMSSQLSourceResolver staticConfig
(_appInit, appEnv) <- (_appInit, appEnv) <-

View File

@ -193,7 +193,7 @@ streamingSubscriptionPollingSpec srcConfig = do
-- well create a new function with the other variables being set to `mempty` -- well create a new function with the other variables being set to `mempty`
mkCohortVariables' = mkCohortVariables mempty mempty mempty mempty mkCohortVariables' = mkCohortVariables mempty mempty mempty mempty
describe "Streaming subcription poll" $ do describe "Streaming subscription poll" $ do
cohortId1 <- runIO newCohortId cohortId1 <- runIO newCohortId
(subscriberId1, subscriberId2) <- runIO $ (,) <$> newSubscriberId <*> newSubscriberId (subscriberId1, subscriberId2) <- runIO $ (,) <$> newSubscriberId <*> newSubscriberId
let subscriber1 = mkSubscriber subscriberId1 let subscriber1 = mkSubscriber subscriberId1

View File

@ -78,13 +78,20 @@ class GQLWsClient():
def get_ws_query_event(self, query_id, timeout): def get_ws_query_event(self, query_id, timeout):
return self.ws_id_query_queues[query_id].get(timeout=timeout) return self.ws_id_query_queues[query_id].get(timeout=timeout)
def send(self, frame): def send(self, frame, count=0):
self.wait_for_connection() self.wait_for_connection()
if frame.get('type') == 'stop': if frame.get('type') == 'stop':
self.ws_active_query_ids.discard( frame.get('id') ) self.ws_active_query_ids.discard( frame.get('id') )
elif frame.get('type') == 'start' and 'id' in frame: elif frame.get('type') == 'start' and 'id' in frame:
self.ws_id_query_queues[frame['id']] = queue.Queue(maxsize=-1) self.ws_id_query_queues[frame['id']] = queue.Queue(maxsize=-1)
self._ws.send(json.dumps(frame)) try:
self._ws.send(json.dumps(frame))
except websocket.WebSocketConnectionClosedException:
if count > 2:
raise websocket.WebSocketConnectionClosedException("Connection is already closed and cannot be recreated even after 3 attempts")
# Connection closed, try to recreate the connection and send the frame again
self.recreate_conn()
self.send(frame, count+1)
def init_as_admin(self): def init_as_admin(self):
headers={} headers={}

View File

@ -264,7 +264,7 @@ class TestWebsocketLogging:
tests for the `websocket-log` type. currently tests presence of operation_name tests for the `websocket-log` type. currently tests presence of operation_name
""" """
def _get_ws_server_logs(x): def _get_ws_server_logs(x):
return x['type'] == 'ws-server' and 'metadata' in x['detail'] return x['type'] == 'ws-server' and 'metadata' in x['detail'] and type(x['detail']) != str
ws_logs = list(filter(_get_ws_server_logs, logs_from_requests)) ws_logs = list(filter(_get_ws_server_logs, logs_from_requests))
assert len(ws_logs) > 0 assert len(ws_logs) > 0