feat: allow reloading config with NOTIFY

Enables reloading the config by doing:
NOTIFY pgrst, 'reload config'

Adds an alias for reloading the schema cache:
NOTIFY pgrst, 'reload schema'
This commit is contained in:
steve-chavez
2021-01-19 13:49:40 -05:00
committed by Steve Chavez
parent 9c005fc683
commit 125ea8f6d9
5 changed files with 111 additions and 34 deletions
+22 -28
View File
@@ -1,4 +1,6 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main (main) where module Main (main) where
@@ -13,10 +15,6 @@ import qualified Hasql.Transaction.Sessions as HT
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
updateAction) updateAction)
import Control.Debounce (debounceAction, debounceEdge,
debounceFreq,
defaultDebounceSettings, mkDebounce,
trailingEdge)
import Control.Retry (RetryStatus, capDelay, import Control.Retry (RetryStatus, capDelay,
exponentialBackoff, retrying, exponentialBackoff, retrying,
rsPreviousDelay) rsPreviousDelay)
@@ -64,11 +62,11 @@ main = do
-- read PGRST_ env variables -- read PGRST_ env variables
env <- readEnvironment env <- readEnvironment
-- read path from commad line -- read command/path from commad line
opts <- readCLIShowHelp env CLI{cliCommand, cliPath} <- readCLIShowHelp env
-- build the 'AppConfig' from the config file path -- build the 'AppConfig' from the config file path
conf <- readValidateConfig mempty env $ cliPath opts conf <- readValidateConfig mempty env cliPath
-- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server. -- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
let let
@@ -105,11 +103,12 @@ main = do
-- Config that can change at runtime -- Config that can change at runtime
refConf <- newIORef conf refConf <- newIORef conf
-- re-read and override the config if db-load-guc-config is true let configRereader = reReadConfig pool gucConfigEnabled env cliPath refConf
when gucConfigEnabled $
reReadConfig pool gucConfigEnabled env (cliPath opts) refConf
case cliCommand opts of -- re-read and override the config if db-load-guc-config is true
when gucConfigEnabled configRereader
case cliCommand of
CmdDumpConfig -> CmdDumpConfig ->
do do
dumpedConfig <- dumpAppConfig <$> readIORef refConf dumpedConfig <- dumpAppConfig <$> readIORef refConf
@@ -149,13 +148,13 @@ main = do
-- Re-read the config on SIGUSR2 -- Re-read the config on SIGUSR2
void $ installHandler sigUSR2 ( void $ installHandler sigUSR2 (
Catch $ reReadConfig pool gucConfigEnabled env (cliPath opts) refConf >> putStrLn ("Config reloaded" :: Text) Catch $ configRereader >> putStrLn ("Config reloaded" :: Text)
) Nothing ) Nothing
#endif #endif
-- reload schema cache on NOTIFY -- reload schema cache + config on NOTIFY
when dbChannelEnabled $ when dbChannelEnabled $
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker configRereader
-- ask for the OS time at most once per second -- ask for the OS time at most once per second
getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime} getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime}
@@ -310,32 +309,27 @@ loadSchemaCache pool actualPgVersion refConf refDbStructure = do
When a NOTIFY <db-channel> - with an empty payload - is done, it refills the schema cache. When a NOTIFY <db-channel> - with an empty payload - is done, it refills the schema cache.
It uses the connectionWorker in case the LISTEN connection dies. It uses the connectionWorker in case the LISTEN connection dies.
-} -}
listener :: ByteString -> Text -> P.Pool -> IORef AppConfig -> IORef (Maybe DbStructure) -> MVar ConnectionStatus -> IO () -> IO () listener :: ByteString -> Text -> P.Pool -> IORef AppConfig -> IORef (Maybe DbStructure) -> MVar ConnectionStatus -> IO () -> IO () -> IO ()
listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker = start listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWorker configRereader = start
where where
start = do start = do
connStatus <- takeMVar mvarConnectionStatus -- takeMVar makes the thread wait if the MVar is empty(until there's a connection). connStatus <- takeMVar mvarConnectionStatus -- takeMVar makes the thread wait if the MVar is empty(until there's a connection).
case connStatus of case connStatus of
Connected actualPgVersion -> void $ forkFinally (do -- forkFinally allows to detect if the thread dies Connected actualPgVersion -> void $ forkFinally (do -- forkFinally allows to detect if the thread dies
dbOrError <- C.acquire dbUri dbOrError <- C.acquire dbUri
-- Debounce in case too many NOTIFYs arrive. Could happen on a migration(assuming a pg EVENT TRIGGER is set up).
-- This might not be needed according to pg docs https://www.postgresql.org/docs/12/sql-notify.html:
-- "If the same channel name is signaled multiple times from the same transaction with identical payload strings, the database server can decide to deliver a single notification only."
-- But we do it to be extra safe.
scFiller <- mkDebounce (defaultDebounceSettings {
-- It's not necessary to check the loadSchemaCache success here. If the connection drops, the thread will die and proceed to recover below.
debounceAction = void $ loadSchemaCache pool actualPgVersion refConf refDbStructure,
debounceEdge = trailingEdge, -- wait until the function hasnt been called in _1s
debounceFreq = _1s })
case dbOrError of case dbOrError of
Right db -> do Right db -> do
putStrLn $ "Listening for notifications on the " <> dbChannel <> " channel" putStrLn $ "Listening for notifications on the " <> dbChannel <> " channel"
let channelToListen = N.toPgIdentifier dbChannel let channelToListen = N.toPgIdentifier dbChannel
cfLoader = configRereader >> putStrLn ("Config reloaded" :: Text)
scLoader = void $ loadSchemaCache pool actualPgVersion refConf refDbStructure -- It's not necessary to check the loadSchemaCache success here. If the connection drops, the thread will die and proceed to recover below.
N.listen db channelToListen N.listen db channelToListen
N.waitForNotifications (\_ msg -> N.waitForNotifications (\_ msg ->
if BS.null msg if | BS.null msg -> scLoader -- reload the schema cache
then scFiller -- reload the schema cache | msg == "reload schema" -> scLoader -- reload the schema cache
else pure ()) db -- Do nothing if anything else than an empty message is sent | msg == "reload config" -> cfLoader -- reload the config
| otherwise -> pure () -- Do nothing if anything else than an empty message is sent
) db
_ -> die errorMessage) _ -> die errorMessage)
(\_ -> do -- if the thread dies, we try to recover (\_ -> do -- if the thread dies, we try to recover
putStrLn retryMessage putStrLn retryMessage
+1 -1
View File
@@ -17,7 +17,7 @@ ALTER ROLE postgrest_test_authenticator SET pgrst."db-tx-end" = 'commit-allow-ov
ALTER ROLE postgrest_test_authenticator SET pgrst."db-schemas" = 'test, tenant1, tenant2'; ALTER ROLE postgrest_test_authenticator SET pgrst."db-schemas" = 'test, tenant1, tenant2';
ALTER ROLE postgrest_test_authenticator SET pgrst."db-root-spec" = 'root'; ALTER ROLE postgrest_test_authenticator SET pgrst."db-root-spec" = 'root';
ALTER ROLE postgrest_test_authenticator SET pgrst."db-prepared-statements" = 'false'; ALTER ROLE postgrest_test_authenticator SET pgrst."db-prepared-statements" = 'false';
ALTER ROLE postgrest_test_authenticator SET pgrst."db-pre-request" = 'custom_headers'; ALTER ROLE postgrest_test_authenticator SET pgrst."db-pre-request" = 'test.custom_headers';
ALTER ROLE postgrest_test_authenticator SET pgrst."db-max-rows" = '1000'; ALTER ROLE postgrest_test_authenticator SET pgrst."db-max-rows" = '1000';
ALTER ROLE postgrest_test_authenticator SET pgrst."db-extra-search-path" = 'public, extensions'; ALTER ROLE postgrest_test_authenticator SET pgrst."db-extra-search-path" = 'public, extensions';
+25 -1
View File
@@ -1925,9 +1925,33 @@ $$ language sql;
create view prepared_statements as create view prepared_statements as
select * from pg_catalog.pg_prepared_statements; select * from pg_catalog.pg_prepared_statements;
create or replace function change_max_rows_config(val int) returns void as $_$ create or replace function change_max_rows_config(val int, notify bool default false) returns void as $_$
begin begin
execute format($$ execute format($$
alter role postgrest_test_authenticator set pgrst."db-max-rows" = %L; alter role postgrest_test_authenticator set pgrst."db-max-rows" = %L;
$$, val); $$, val);
if notify then
perform pg_notify('pgrst', 'reload config');
end if;
end $_$ volatile security definer language plpgsql ;
create or replace function reset_max_rows_config() returns void as $_$
begin
alter role postgrest_test_authenticator set pgrst."db-max-rows" = '1000';
end $_$ volatile security definer language plpgsql ;
create or replace function change_db_schema_and_full_reload(schemas text) returns void as $_$
begin
execute format($$
alter role postgrest_test_authenticator set pgrst."db-schemas" = %L;
$$, schemas);
perform pg_notify('pgrst', 'reload config');
perform pg_notify('pgrst', 'reload schema');
end $_$ volatile security definer language plpgsql ;
create or replace function v1.reset_db_schema_config() returns void as $_$
begin
alter role postgrest_test_authenticator set pgrst."db-schemas" = 'test';
perform pg_notify('pgrst', 'reload config');
perform pg_notify('pgrst', 'reload schema');
end $_$ volatile security definer language plpgsql ; end $_$ volatile security definer language plpgsql ;
@@ -5,7 +5,7 @@ db-extra-search-path = "public,extensions"
db-max-rows = 1000 db-max-rows = 1000
db-pool = 1 db-pool = 1
db-pool-timeout = 100 db-pool-timeout = 100
db-pre-request = "custom_headers" db-pre-request = "test.custom_headers"
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "root" db-root-spec = "root"
db-schemas = "test,tenant1,tenant2" db-schemas = "test,tenant1,tenant2"
+61 -2
View File
@@ -87,7 +87,7 @@ def defaultenv():
"PGRST_DB_URI": os.environ["PGRST_DB_URI"], "PGRST_DB_URI": os.environ["PGRST_DB_URI"],
"PGRST_DB_SCHEMAS": os.environ["PGRST_DB_SCHEMAS"], "PGRST_DB_SCHEMAS": os.environ["PGRST_DB_SCHEMAS"],
"PGRST_DB_ANON_ROLE": os.environ["PGRST_DB_ANON_ROLE"], "PGRST_DB_ANON_ROLE": os.environ["PGRST_DB_ANON_ROLE"],
"PGRST_DB_LOAD_GUC_CONFIG": "false" "PGRST_DB_LOAD_GUC_CONFIG": "false",
} }
@@ -187,7 +187,6 @@ def wait_until_ready(url):
for _ in range(10): for _ in range(10):
try: try:
response = session.get(url, timeout=1) response = session.get(url, timeout=1)
if response.status_code == 200: if response.status_code == 200:
return return
except (requests.ConnectionError, requests.ReadTimeout): except (requests.ConnectionError, requests.ReadTimeout):
@@ -508,6 +507,34 @@ def test_db_schema_reload(tmp_path, defaultenv):
assert response.status_code == 200 assert response.status_code == 200
def test_db_schema_notify_reload(defaultenv):
"DB schema and config should be reloaded when PostgREST is sent a NOTIFY"
env = {
**defaultenv,
"PGRST_DB_LOAD_GUC_CONFIG": "true",
"PGRST_DB_CHANNEL_ENABLED": "true",
"PGRST_DB_SCHEMAS": "test",
}
with run(env=env) as postgrest:
response = postgrest.session.get("/parents")
assert response.status_code == 404
# change db-schemas config on the db and reload config and cache with notify
postgrest.session.post(
"/rpc/change_db_schema_and_full_reload", data={"schemas": "v1"}
)
time.sleep(0.5)
response = postgrest.session.get("/parents?select=*,children(*)")
assert response.status_code == 200
# reset db-schemas config on the db
postgrest.session.post("/rpc/reset_db_schema_config")
def test_max_rows_reload(defaultenv): def test_max_rows_reload(defaultenv):
"max-rows should be reloaded from role settings when PostgREST receives a SIGUSR2." "max-rows should be reloaded from role settings when PostgREST receives a SIGUSR2."
config = CONFIGSDIR / "sigusr2-settings.config" config = CONFIGSDIR / "sigusr2-settings.config"
@@ -530,4 +557,36 @@ def test_max_rows_reload(defaultenv):
time.sleep(0.1) time.sleep(0.1)
response = postgrest.session.head("/projects") response = postgrest.session.head("/projects")
assert response.headers["Content-Range"] == "0-0/*" assert response.headers["Content-Range"] == "0-0/*"
# reset max-rows config on the db
postgrest.session.post("/rpc/reset_max_rows_config")
def test_max_rows_notify_reload(defaultenv):
"max-rows should be reloaded from role settings when PostgREST receives a NOTIFY"
env = {
**defaultenv,
"PGRST_DB_LOAD_GUC_CONFIG": "true",
"PGRST_DB_CHANNEL_ENABLED": "true",
}
with run(env=env) as postgrest:
response = postgrest.session.head("/projects")
assert response.headers["Content-Range"] == "0-4/*"
# change max-rows config on the db and reload with notify
postgrest.session.post(
"/rpc/change_max_rows_config", data={"val": 1, "notify": True}
)
time.sleep(0.1)
response = postgrest.session.head("/projects")
assert response.headers["Content-Range"] == "0-0/*"
# reset max-rows config on the db
postgrest.session.post("/rpc/reset_max_rows_config")