Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: minimal health check #2092

Merged
merged 7 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- #1933, Add a minimal health check endpoint on an admin port at the `<host>:<admin_server_port>/health` endpoint - @steve-chavez
+ For enabling this, the `admin-server-port` config must be set explictly

### Fixed

- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther
Expand Down
26 changes: 24 additions & 2 deletions src/PostgREST/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import qualified Hasql.DynamicStatements.Snippet as SQL
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
import qualified Hasql.Pool as SQL
import qualified Hasql.Transaction as SQL
import qualified Hasql.Session as SQL (sql)
import qualified Hasql.Transaction as SQL hiding (sql)
import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
Expand Down Expand Up @@ -113,6 +114,11 @@ run installHandlers maybeRunWithSocket appState = do
when configDbChannelEnabled $ listener appState

let app = postgrest configLogLevel appState (connectionWorker appState)
adminApp = postgrestAdmin appState configDbChannelEnabled

whenJust configAdminServerPort $ \adminPort -> do
AppState.logWithZTime appState $ "Admin server listening on port " <> show adminPort
void . forkIO $ Warp.runSettings (serverSettings conf & setPort adminPort) adminApp

case configServerUnixSocket of
Just socket ->
Expand All @@ -127,6 +133,9 @@ run installHandlers maybeRunWithSocket appState = do
do
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
Warp.runSettings (serverSettings conf) app
where
whenJust :: Applicative m => Maybe a -> (a -> m ()) -> m ()
whenJust mg f = maybe (pure ()) f mg

serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} =
Expand All @@ -135,6 +144,19 @@ serverSettings AppConfig{..} =
& setPort configServerPort
& setServerName ("postgrest/" <> prettyVersion)

-- | PostgREST admin application
postgrestAdmin :: AppState.AppState -> Bool -> Wai.Application
postgrestAdmin appState configDbChannelEnabled req respond =
case Wai.pathInfo req of
["health"] ->
if configDbChannelEnabled then do
listenerOn <- AppState.getIsListenerOn appState
respond $ Wai.responseLBS (if listenerOn then HTTP.status200 else HTTP.status503) [] mempty
else do
result <- SQL.use (AppState.getPool appState) $ SQL.sql "SELECT 1"
respond $ Wai.responseLBS (if isRight result then HTTP.status200 else HTTP.status503) [] mempty
_ -> respond $ Wai.responseLBS HTTP.status404 [] mempty

-- | PostgREST application
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
postgrest logLev appState connWorker =
Expand Down
11 changes: 11 additions & 0 deletions src/PostgREST/AppState.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module PostgREST.AppState
( AppState
, getConfig
, getDbStructure
, getIsListenerOn
, getIsWorkerOn
, getJsonDbS
, getMainThreadId
Expand All @@ -16,6 +17,7 @@ module PostgREST.AppState
, logWithZTime
, putConfig
, putDbStructure
, putIsListenerOn
, putIsWorkerOn
, putJsonDbS
, putPgVersion
Expand Down Expand Up @@ -53,6 +55,8 @@ data AppState = AppState
, stateIsWorkerOn :: IORef Bool
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
, stateListener :: MVar ()
-- | State of the LISTEN channel, used for health checks
, stateIsListenerOn :: IORef Bool
-- | Config that can change at runtime
, stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration
Expand All @@ -78,6 +82,7 @@ initWithPool newPool conf =
<*> newIORef mempty
<*> newIORef False
<*> newEmptyMVar
<*> newIORef False
<*> newIORef conf
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
Expand Down Expand Up @@ -153,3 +158,9 @@ waitListener = takeMVar . stateListener
-- the connectionWorker is the only mvar producer.
signalListener :: AppState -> IO ()
signalListener appState = void $ tryPutMVar (stateListener appState) ()

getIsListenerOn :: AppState -> IO Bool
getIsListenerOn = readIORef . stateIsListenerOn

putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
3 changes: 3 additions & 0 deletions src/PostgREST/CLI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ exampleConfigFile =
|## when none is provided, 660 is applied by default
|# server-unix-socket-mode = "660"
|
|## admin server for health checks, it's disabled by default unless a port is specified
|# admin-server-port = 3001
|
|## determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely
|## admitted values: follow-privileges, ignore-privileges, disabled
|openapi-mode = "follow-privileges"
Expand Down
3 changes: 3 additions & 0 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ data AppConfig = AppConfig
, configServerPort :: Int
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerPort :: Maybe Int
}

data LogLevel = LogCrit | LogError | LogWarn | LogInfo
Expand Down Expand Up @@ -147,6 +148,7 @@ toText conf =
,("server-port", show . configServerPort)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
]

-- quote all app.settings
Expand Down Expand Up @@ -242,6 +244,7 @@ parser optPath env dbSettings =
<*> (fromMaybe 3000 <$> optInt "server-port")
<*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> optInt "admin-server-port"
where
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
Expand Down
2 changes: 2 additions & 0 deletions src/PostgREST/Workers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ listener appState = do
case dbOrError of
Right db -> do
AppState.logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
AppState.putIsListenerOn appState True
SQL.listen db $ SQL.toPgIdentifier dbChannel
SQL.waitForNotifications handleNotification db
_ ->
Expand All @@ -208,6 +209,7 @@ listener appState = do
handleFinally dbChannel _ = do
-- if the thread dies, we try to recover
AppState.logWithZTime appState $ "Retrying listening for notifications on the " <> dbChannel <> " channel.."
AppState.putIsListenerOn appState False
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codecov warning should be gone when #1766 (comment) is done.

-- assume the pool connection was also lost, call the connection worker
connectionWorker appState
-- retry the listener
Expand Down
1 change: 1 addition & 0 deletions test/SpecHelper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configServerUnixSocketMode = 432
, configDbTxAllowOverride = True
, configDbTxRollbackAll = True
, configAdminServerPort = Nothing
}

testCfg :: Text -> AppConfig
Expand Down
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/aliases.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/boolean-numeric.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/boolean-string.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/no-defaults-with-db.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
1 change: 1 addition & 0 deletions test/io-tests/configs/expected/types.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ server-host = "!4"
server-port = 3000
server-unix-socket = ""
server-unix-socket-mode = "660"
admin-server-port = ""
app.settings.test = "Bool False"
1 change: 1 addition & 0 deletions test/io-tests/configs/no-defaults-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ PGRST_SERVER_HOST: 0.0.0.0
PGRST_SERVER_PORT: 80
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
PGRST_SERVER_UNIX_SOCKET_MODE: 777
PGRST_ADMIN_SERVER_PORT: 3001
1 change: 1 addition & 0 deletions test/io-tests/configs/no-defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ server-host = "0.0.0.0"
server-port = 80
server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777"
admin-server-port = 3001
app.settings.test = "test"
app.settings.test2 = "test"
41 changes: 41 additions & 0 deletions test/io-tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,44 @@ def test_db_prepared_statements_disable(defaultenv):
with run(env=env) as postgrest:
response = postgrest.session.post("/rpc/uses_prepared_statements")
assert response.text == "false"


def test_admin_healthy_w_channel(defaultenv):
"Should get a success response from the admin server health endpoint when the LISTEN channel is enabled"

env = {
**defaultenv,
"PGRST_ADMIN_SERVER_PORT": "3001",
"PGRST_DB_CHANNEL_ENABLED": "true",
}

with run(env=env) as postgrest:
response = requests.get(f"http://localhost:{env['PGRST_ADMIN_SERVER_PORT']}/health")
assert response.status_code == 200


def test_admin_healthy_wo_channel(defaultenv):
"Should get a success response from the admin server health endpoint when the LISTEN channel is disabled"

env = {
**defaultenv,
"PGRST_ADMIN_SERVER_PORT": "3001",
"PGRST_DB_CHANNEL_ENABLED": "false",
}

with run(env=env) as postgrest:
response = requests.get(f"http://localhost:{env['PGRST_ADMIN_SERVER_PORT']}/health")
assert response.status_code == 200


def test_admin_not_found(defaultenv):
"Should get a not found from the admin server"

env = {
**defaultenv,
"PGRST_ADMIN_SERVER_PORT": "3001",
}

with run(env=env) as postgrest:
response = requests.get(f"http://localhost:{env['PGRST_ADMIN_SERVER_PORT']}/notfound")
assert response.status_code == 404