Added a healthcheck endpoint to the Data Connector Agent API spec

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4890
GitOrigin-RevId: 605408bddf03bef66eb03be8c242797e8fcf89bb
This commit is contained in:
Daniel Chambers 2022-06-30 18:36:54 +10:00 committed by hasura-bot
parent 27398f40f4
commit 3f405915e9
11 changed files with 97 additions and 20 deletions

View File

@ -110,6 +110,7 @@ The entry point to the reference agent application is a Fastify HTTP server. Raw
- `GET /capabilities`, which returns the capabilities of the agent and a schema that describes the type of the configuration expected to be sent on the `X-Hasura-DataConnector-Config` header
- `GET /schema`, which returns information about the provided _data schema_, its tables and their columns
- `POST /query`, which receives a query structure to be executed, encoded as the JSON request body, and returns JSON conforming to the schema described by the `/schema` endpoint, and contining the requested fields.
- `GET /health`, which can be used to either check if the agent is running, or if a particular data source is healthy
The `/schema` and `/query` endpoints require the request to have the `X-Hasura-DataConnector-Config` header set. That header contains configuration information that agent can use to configure itself. For example, the header could contain a connection string to the database, if the agent requires a connection string to know how to connect to a specific database. The header must be a JSON object, but the specific properties that are required are up to the agent to define.
@ -673,3 +674,8 @@ The key point of interest here is in the `where` field where we are comparing be
#### Type Definitions
The `QueryRequest` TypeScript type in the [reference implementation](./reference/src/types/index.ts) describes the valid request body payloads which may be passed to the `POST /query` endpoint. The response body structure is captured by the `QueryResponse` type.
### Health endpoint
Agents must expose a `/health` endpoint which must return a 204 No Content HTTP response code if the agent is up and running. This does not mean that the agent is able to connect to any data source it performs queries against, only that the agent is running and can accept requests, even if some of those requests might fail because a dependant service is unavailable.
However, this endpoint can also be used to check whether the ability of the agent to talk to a particular data source is healthy. If the endpoint is sent the `X-Hasura-DataConnector-Config` and `X-Hasura-DataConnector-SourceName` headers, then the agent is expected to check that it can successfully talk to whatever data source is being specified by those headers. If it can do so, then it must return a 204 No Content response code.

View File

@ -37,6 +37,11 @@ server.post<{ Body: QueryRequest, Reply: QueryResponse }>("/query", async (reque
return queryData(data, request.body);
});
server.get("/health", async (request, response) => {
server.log.info({ headers: request.headers, query: request.body, }, "health.request");
response.statusCode = 204;
});
process.on('SIGINT', () => {
server.log.info("interrupted");
process.exit(0);

View File

@ -107,6 +107,38 @@
}
}
}
},
"/health": {
"get": {
"parameters": [
{
"in": "header",
"name": "X-Hasura-DataConnector-SourceName",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "header",
"name": "X-Hasura-DataConnector-Config",
"required": false,
"schema": {
"additionalProperties": true,
"nullable": false,
"type": "object"
}
}
],
"responses": {
"204": {
"description": ""
},
"400": {
"description": "Invalid `X-Hasura-DataConnector-Config` or `X-Hasura-DataConnector-SourceName`"
}
}
}
}
},
"components": {

View File

@ -1293,6 +1293,7 @@ test-suite tests-dc-api
, Paths_graphql_engine
, Test.Data
, Test.CapabilitiesSpec
, Test.HealthSpec
, Test.QuerySpec
, Test.QuerySpec.BasicSpec
, Test.QuerySpec.RelationshipsSpec

View File

@ -31,20 +31,26 @@ type CapabilitiesApi =
type SchemaApi =
"schema"
:> SourceNameHeader
:> ConfigHeader
:> SourceNameHeader Required
:> ConfigHeader Required
:> Get '[JSON] V0.SchemaResponse
type QueryApi =
"query"
:> SourceNameHeader
:> ConfigHeader
:> SourceNameHeader Required
:> ConfigHeader Required
:> ReqBody '[JSON] V0.QueryRequest
:> Post '[JSON] V0.QueryResponse
type ConfigHeader = Header' '[Required, Strict] "X-Hasura-DataConnector-Config" V0.Config
type HealthApi =
"health"
:> SourceNameHeader Optional
:> ConfigHeader Optional
:> GetNoContent
type SourceNameHeader = Header' '[Required, Strict] "X-Hasura-DataConnector-SourceName" SourceName
type ConfigHeader optionality = Header' '[optionality, Strict] "X-Hasura-DataConnector-Config" V0.Config
type SourceNameHeader optionality = Header' '[optionality, Strict] "X-Hasura-DataConnector-SourceName" SourceName
type SourceName = Text
@ -54,13 +60,15 @@ data Routes mode = Routes
-- | 'GET /schema'
_schema :: mode :- SchemaApi,
-- | 'POST /query'
_query :: mode :- QueryApi
_query :: mode :- QueryApi,
-- | 'GET /health'
_health :: mode :- HealthApi
}
deriving stock (Generic)
-- | servant-openapi3 does not (yet) support NamedRoutes so we need to compose the
-- API the old-fashioned way using :<|> for use by @toOpenApi@
type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi
type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi :<|> HealthApi
-- | Provide an OpenApi 3.0 schema for the API
openApiSchema :: OpenApi

View File

@ -12,6 +12,7 @@ import Network.HTTP.Client (defaultManagerSettings, newManager)
import Servant.API (NamedRoutes)
import Servant.Client (Client, ClientError, hoistClient, mkClientEnv, runClientM, (//))
import Test.CapabilitiesSpec qualified
import Test.HealthSpec qualified
import Test.Hspec (Spec)
import Test.Hspec.Core.Runner (runSpec)
import Test.Hspec.Core.Util (filterPredicate)
@ -25,6 +26,7 @@ testSourceName = "dc-api-tests"
tests :: Client IO (NamedRoutes Routes) -> API.SourceName -> API.Config -> API.Capabilities -> Spec
tests api sourceName agentConfig capabilities = do
Test.HealthSpec.spec api sourceName agentConfig
Test.CapabilitiesSpec.spec api agentConfig capabilities
Test.SchemaSpec.spec api sourceName agentConfig
Test.QuerySpec.spec api sourceName agentConfig capabilities

View File

@ -383,7 +383,7 @@
"name": "TrackId",
"type": "number",
"nullable": false,
"description": "The ID of the playlist"
"description": "The ID of the track"
},
{
"name": "Name",

View File

@ -0,0 +1,18 @@
module Test.HealthSpec (spec) where
import Hasura.Backends.DataConnector.API (Config, Routes (..), SourceName)
import Servant.API (NamedRoutes, NoContent (..))
import Servant.Client (Client, (//))
import Test.Hspec (Spec, describe, it)
import Test.Hspec.Expectations.Pretty (shouldBe)
import Prelude
spec :: Client IO (NamedRoutes Routes) -> SourceName -> Config -> Spec
spec api sourceName config = describe "health API" $ do
it "returns a successful HTTP status code for a plain healthcheck" $ do
response <- (api // _health) Nothing Nothing
response `shouldBe` NoContent
it "returns a successful HTTP status code for a data source healthcheck" $ do
response <- (api // _health) (Just sourceName) (Just config)
response `shouldBe` NoContent

View File

@ -116,7 +116,7 @@ mkLocalTestEnvironmentMock _ = do
maeConfig <- I.newIORef chinookMock
maeQuery <- I.newIORef Nothing
maeThreadId <- forkIO $ runMockServer maeConfig maeQuery
healthCheck $ "http://127.0.0.1:" <> show mockAgentPort <> "/healthz"
healthCheck $ "http://127.0.0.1:" <> show mockAgentPort <> "/health"
pure $ MockAgentEnvironment {..}
-- | Load the agent schema into HGE.

View File

@ -223,20 +223,16 @@ mockQueryHandler mcfg mquery _sourceName _cfg query = liftIO $ do
I.writeIORef mquery (Just query)
pure $ handler query
type HealthcheckApi =
"healthz"
:> Get '[JSON] ()
healthcheckHandler :: Maybe API.SourceName -> Maybe API.Config -> Handler NoContent
healthcheckHandler _sourceName _config = pure NoContent
healthcheckHandler :: Handler ()
healthcheckHandler = pure ()
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> Server (API.Api :<|> HealthcheckApi)
dcMockableServer mcfg mquery = (mockCapabilitiesHandler mcfg :<|> mockSchemaHandler mcfg :<|> mockQueryHandler mcfg mquery) :<|> healthcheckHandler
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> Server API.Api
dcMockableServer mcfg mquery = mockCapabilitiesHandler mcfg :<|> mockSchemaHandler mcfg :<|> mockQueryHandler mcfg mquery :<|> healthcheckHandler
mockAgentPort :: Warp.Port
mockAgentPort = 65006
runMockServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> IO ()
runMockServer mcfg mquery = do
let app = serve (Proxy :: Proxy (API.Api :<|> HealthcheckApi)) $ dcMockableServer mcfg mquery
let app = serve (Proxy :: Proxy API.Api) $ dcMockableServer mcfg mquery
Warp.run mockAgentPort app

View File

@ -1,6 +1,7 @@
-- | Helper functions for HTTP requests.
module Harness.Http
( get_,
getWithStatus,
post,
postValue,
postValueWithStatus,
@ -31,6 +32,14 @@ get_ url = do
unless (Http.getResponseStatusCode response == 200) $
error ("Non-200 response code from HTTP request: " ++ url)
-- | Performs get, doesn't return the result. Simply throws if there's
-- not an expected response status code.
getWithStatus :: HasCallStack => [Int] -> String -> IO ()
getWithStatus acceptableStatusCodes url = do
response <- Http.httpNoBody (fromString url)
unless (Http.getResponseStatusCode response `elem` acceptableStatusCodes) $
error ("Unexpected response code from HTTP request: " ++ url ++ ". Expected: " ++ show acceptableStatusCodes)
-- | Post the JSON to the given URL, and produces a very descriptive
-- exception on failure.
postValue :: HasCallStack => String -> Http.RequestHeaders -> Value -> IO Value
@ -94,7 +103,7 @@ healthCheck url = loop [] Constants.httpHealthCheckAttempts
)
loop failures attempts =
catch
(get_ url)
(getWithStatus [200, 204] url)
( \(failure :: Http.HttpException) -> do
threadDelay Constants.httpHealthCheckIntervalMicroseconds
loop (failure : failures) (attempts - 1)