Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file. From versio
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
- Add GHC runtime metrics to the metrics endpoint by @mkleczek in #4862
- Support running the admin server on a unix socket by @wolfgangwalther in #5003
- Add `--schema-cache-uri` to asynchronously load an initial schema cache dump from `http(s)` or local `file` URIs by @mkleczek in #5031
- Add `schema-cache-dump-path` to automatically dump the schema cache after each reload for fast restarts and scale-to-zero setups by @mkleczek in #5082

### Fixed

Expand Down
72 changes: 71 additions & 1 deletion docs/references/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PostgREST provides a CLI with the options listed below:
.. code:: text

Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema | --ready]
[FILENAME]
[--schema-cache-uri URI] [FILENAME]

PostgREST / create a REST API to an existing Postgres
database
Expand All @@ -22,6 +22,8 @@ PostgREST provides a CLI with the options listed below:
output structure is unstable)
--ready Checks the health of PostgREST by doing a request on
the admin server /ready endpoint
--schema-cache-uri URI
Try pre-loading schema cache from provided URI
FILENAME Path to configuration file

FILENAME
Expand Down Expand Up @@ -74,6 +76,74 @@ Dump Schema

Dumps the schema cache in JSON format.

.. _cli_schema_cache_uri:

Schema Cache URI
----------------

.. code:: bash

$ postgrest --schema-cache-uri http://localhost/schema-cache.json

Tries to load the initial :ref:`schema_cache` from the given URI when starting PostgREST.
The load is asynchronous and does not block application startup. If the URI cannot be
loaded, PostgREST logs the failure and continues with the regular database-backed
schema cache load.

The supported URI schemes are:

``file:``
Loads a schema cache dump from a local file. The URI must point to a local path, for
example ``file:///var/lib/postgrest/schema-cache.json``. Remote file authorities are
not supported.

``http:`` and ``https:``
Loads a schema cache dump over HTTP(S). The response body must be a schema cache JSON
dump, such as the output of ``postgrest --dump-schema`` or the runtime cache exposed
by another PostgREST instance's :ref:`admin_server`.

Other schemes are rejected as unsupported.

Load From a Local File
~~~~~~~~~~~~~~~~~~~~~~

For a one-off dump, write the schema cache to a file:

.. code:: bash

$ postgrest --dump-schema > /var/lib/postgrest/schema-cache.json

Then start PostgREST with a ``file:`` URI:

.. code:: bash

$ postgrest --schema-cache-uri file:///var/lib/postgrest/schema-cache.json

For repeated restarts, configure :ref:`schema-cache-dump-path` so a running
instance refreshes this file after each successful schema cache reload. See
:ref:`schema_cache_dumping`.

Load From Another PostgREST Instance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :ref:`admin_server` exposes the current runtime schema cache on the
``/schema_cache`` endpoint. If one PostgREST instance exposes its admin server on
``localhost:3001``, a new instance can load from it with:

.. code:: bash

$ postgrest --schema-cache-uri http://localhost:3001/schema_cache

In Kubernetes, the same pattern can use an internal Service that points at ready
PostgREST pods:

.. code:: bash

$ postgrest --schema-cache-uri http://postgrest-admin:3001/schema_cache

The admin endpoint should stay internal to your deployment or be protected by your
platform's networking controls.

Ready Flag
----------

Expand Down
27 changes: 27 additions & 0 deletions docs/references/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,33 @@ openapi-server-proxy-uri
]
}

.. _schema-cache-dump-path:

schema-cache-dump-path
----------------------

=============== ==========================
**Type** String
**Default** `n/a`
**Reloadable** Y
**Environment** PGRST_SCHEMA_CACHE_DUMP_PATH
**In-Database** `n/a`
=============== ==========================

Specifies a local file path where PostgREST writes a JSON dump of the runtime
:ref:`schema_cache` after each successful schema cache load or reload.

The parent directory must already exist and be writable by the PostgREST
process. If writing the dump fails, PostgREST logs the failure and continues
serving with the loaded schema cache.

The dump can be loaded on a later start with :ref:`cli_schema_cache_uri`. See
:ref:`schema_cache_dumping` for a startup workflow.

.. code:: bash

schema-cache-dump-path = "/var/lib/postgrest/schema-cache.json"

.. _server_cors_allowed_origins:

server-cors-allowed-origins
Expand Down
33 changes: 33 additions & 0 deletions docs/references/schema_cache.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,39 @@ Getting this metadata requires expensive queries. To avoid repeating this work,
- If the schema cache queries are slow, the most likely cause is *system catalog bloat*, see `issue#3212 <https://github.com/PostgREST/postgrest/issues/3212>`_ for more details.
- You can turn the :ref:`log-level` to ``debug`` to see the time of each schema cache query.

.. _schema_cache_dumping:

Schema Cache Dumps
------------------

PostgREST can write the runtime schema cache to a JSON dump. A new instance can
then use this dump as its initial schema cache with :ref:`cli_schema_cache_uri`,
which helps avoid waiting for expensive catalog queries during fast restarts or
scale-to-zero startup.

To keep a local dump updated automatically, configure :ref:`schema-cache-dump-path`:

.. code:: bash

schema-cache-dump-path = "/var/lib/postgrest/schema-cache.json"

After each successful schema cache load or reload, PostgREST writes the current
schema cache to this path. The parent directory must already exist and be writable
by the PostgREST process. If writing the dump fails, PostgREST logs the failure
and continues serving with the loaded schema cache.

On the next start, use the dump file as an initial schema cache:

.. code:: bash

postgrest --schema-cache-uri file:///var/lib/postgrest/schema-cache.json /path/to/postgrest.conf

The dump should be reused only by instances that serve the same database and
compatible schema cache configuration, such as :ref:`db-schemas` and
:ref:`db-extra-search-path`. PostgREST still performs the regular database-backed
schema cache load, so the dump is a startup acceleration path rather than a
replacement for database metadata loading.

.. _schema_reloading:

Schema Cache Reloading
Expand Down
81 changes: 71 additions & 10 deletions src/library/PostgREST/AppState.hs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}

module PostgREST.AppState
( AppState
Expand Down Expand Up @@ -29,6 +31,7 @@
) where

import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Either.Combinators (whenLeft)
import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Config as SQL
Expand All @@ -48,13 +51,14 @@
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
exponentialBackoff, retrying,
rsPreviousDelay)
import Data.IORef (IORef, atomicWriteIORef, newIORef,
readIORef)
import Data.IORef (IORef, atomicModifyIORef, atomicWriteIORef,
newIORef, readIORef)
import Data.Time.Clock (UTCTime, getCurrentTime)

import Control.Concurrent.STM (TMVar, newEmptyTMVarIO,
putTMVar, readTMVar,
tryReadTMVar, tryTakeTMVar)
tryPutTMVar, tryReadTMVar,
tryTakeTMVar)
import PostgREST.Auth.JwtCache (JwtCacheState, update)
import PostgREST.Config (AppConfig (..),
readAppConfig,
Expand All @@ -66,10 +70,17 @@
minimumPgVersion)
import PostgREST.Debounce (makeDebouncer)
import PostgREST.SchemaCache (SchemaCache (..),
dumpSchemaCache,
querySchemaCache,
showSummary)
import PostgREST.SchemaCache.Identifiers (quoteQi)

import qualified Data.Aeson as JSON
import qualified Network.HTTP.Client as HC
import Network.URI (URI (..), URIAuth (..),
parseURI, unEscapeString)
import System.IO.Error (userError)

import Protolude

data AppState = AppState
Expand Down Expand Up @@ -109,16 +120,64 @@
{ getSCStatusTMVar :: TMVar Bool
}

init :: AppConfig -> IO () -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller = do
init :: AppConfig -> IO () -> Maybe Text -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller schemaCacheLoadUri = do
loggerState <- Logger.init
metricsState <- Metrics.init configDbPoolSize
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)

observer $ AppStartObs prettyVersion

pool <- initPool conf observer
initWithPool pool conf loggerState metricsState observer appKiller
appState <- initWithPool pool conf loggerState metricsState observer appKiller

runInitialSchemaCacheLoader observer schemaCacheLoadUri appState

pure appState

runInitialSchemaCacheLoader :: ObservationHandler -> Maybe Text -> AppState -> IO ()
runInitialSchemaCacheLoader observer schemaCacheLoadUri AppState{stateSchemaCache, stateSCacheStatus=SchemaCacheStatus{getSCStatusTMVar}} = do
void $ forkIO $
traverse (fetchInitialSchemaCache observer) schemaCacheLoadUri >>= foldMap setInitialSchemaCache . join
where
setInitialSchemaCache sc =
whenM (atomically $ tryPutTMVar getSCStatusTMVar True) $
atomicModifyIORef stateSchemaCache $ (, ()) . maybe (Just sc) Just

fetchInitialSchemaCache :: ObservationHandler -> Text -> IO (Maybe SchemaCache)
fetchInitialSchemaCache observer uri = flip catches [
Handler (handleError @IOException),
Handler (handleError @HC.HttpException),
Handler (handleError @JSON.AesonException)

Check warning on line 151 in src/library/PostgREST/AppState.hs

View check run for this annotation

Codecov / codecov/patch

src/library/PostgREST/AppState.hs#L151

Added line #L151 was not covered by tests
] $ maybe (throwIO $ userError "Invalid schema cache dump URI") pure =<< traverse fetchURI (parseURI $ toS uri)
where
handleError :: Show e => e -> IO (Maybe a)
handleError = (Nothing <$) . observer . SchemaCacheInitialLoadFailureObs uri . show

fetchURI URI{uriScheme, ..}
| uriScheme == "file:" = do
path <- fileURIPath uriAuthority uriPath
Just <$> (JSON.throwDecode =<< LBS.readFile path)
| uriScheme `elem` ["http:", "https:"] = do
request <- HC.parseUrlThrow $ toS uri
manager <- HC.newManager HC.defaultManagerSettings
HC.withResponse request manager $ \response ->
Just <$> (JSON.throwDecode . LBS.fromChunks =<< HC.brConsume (HC.responseBody response))
| otherwise =
throwIO $ userError $ "Unsupported schema cache dump URI scheme: " <> uriScheme

Check warning on line 167 in src/library/PostgREST/AppState.hs

View check run for this annotation

Codecov / codecov/patch

src/library/PostgREST/AppState.hs#L166-L167

Added lines #L166 - L167 were not covered by tests

fileURIPath Nothing path = pure $ unEscapeString path
fileURIPath (Just URIAuth{uriRegName=""}) path = pure $ unEscapeString path
fileURIPath (Just URIAuth{uriRegName="localhost"}) path = pure $ unEscapeString path
fileURIPath _ _ = throwIO $ userError "Only local file URIs are supported"

Check warning on line 172 in src/library/PostgREST/AppState.hs

View check run for this annotation

Codecov / codecov/patch

src/library/PostgREST/AppState.hs#L171-L172

Added lines #L171 - L172 were not covered by tests

writeSchemaCacheDump :: ObservationHandler -> FilePath -> SchemaCache -> IO ()
writeSchemaCacheDump observer path sCache =
LBS.writeFile path (dumpSchemaCache sCache) `catch` handleWriteError
where
handleWriteError :: IOException -> IO ()
handleWriteError =
observer . SchemaCacheDumpFailureObs (toS path)

Check warning on line 180 in src/library/PostgREST/AppState.hs

View check run for this annotation

Codecov / codecov/patch

src/library/PostgREST/AppState.hs#L179-L180

Added lines #L179 - L180 were not covered by tests

initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO () -> IO AppState
initWithPool pool conf loggerState metricsState observer appKiller = mdo
Expand Down Expand Up @@ -347,6 +406,8 @@
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.
markSchemaCachePending appState
putSchemaCache appState $ Just sCache
for_ configSchemaCacheDumpPath $ \path ->
writeSchemaCacheDump observer path sCache
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache)
-- Flush the pool after loading the schema cache to reset any stale session cache entries
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
Expand Down
Loading