Skip to content

Commit 6fd483f

Browse files
committed
add: load schema cache dump from URI at startup
1 parent 6191243 commit 6fd483f

13 files changed

Lines changed: 339 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. From versio
1515
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
1616
- Add GHC runtime metrics to the metrics endpoint by @mkleczek in #4862
1717
- Support running the admin server on a unix socket by @wolfgangwalther in #5003
18+
- Add `--schema-cache-uri` to asynchronously load an initial schema cache dump from `http(s)` or local `file` URIs by @mkleczek in #5031
1819

1920
### Fixed
2021

docs/references/cli.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ PostgREST provides a CLI with the options listed below:
88
.. code:: text
99
1010
Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema | --ready]
11-
[FILENAME]
11+
[--schema-cache-uri URI] [FILENAME]
1212
1313
PostgREST / create a REST API to an existing Postgres
1414
database
@@ -22,6 +22,8 @@ PostgREST provides a CLI with the options listed below:
2222
output structure is unstable)
2323
--ready Checks the health of PostgREST by doing a request on
2424
the admin server /ready endpoint
25+
--schema-cache-uri URI
26+
Try pre-loading schema cache from provided URI
2527
FILENAME Path to configuration file
2628
2729
FILENAME
@@ -74,6 +76,15 @@ Dump Schema
7476
7577
Dumps the schema cache in JSON format.
7678

79+
Schema Cache Load URI
80+
---------------------
81+
82+
.. code:: bash
83+
84+
$ postgrest --schema-cache-uri http://localhost/schema-cache.json
85+
86+
Loads the initial schema cache from the given URI when starting PostgREST.
87+
7788
Ready Flag
7889
----------
7990

src/PostgREST/AppState.hs

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
{-# LANGUAGE LambdaCase #-}
2-
{-# LANGUAGE NamedFieldPuns #-}
3-
{-# LANGUAGE RecordWildCards #-}
4-
{-# LANGUAGE RecursiveDo #-}
1+
{-# LANGUAGE LambdaCase #-}
2+
{-# LANGUAGE NamedFieldPuns #-}
3+
{-# LANGUAGE RecordWildCards #-}
4+
{-# LANGUAGE RecursiveDo #-}
5+
{-# LANGUAGE TupleSections #-}
6+
{-# LANGUAGE TypeApplications #-}
57

68
module PostgREST.AppState
79
( AppState
@@ -48,13 +50,14 @@ import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
4850
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
4951
exponentialBackoff, retrying,
5052
rsPreviousDelay)
51-
import Data.IORef (IORef, atomicWriteIORef, newIORef,
52-
readIORef)
53+
import Data.IORef (IORef, atomicModifyIORef, atomicWriteIORef,
54+
newIORef, readIORef)
5355
import Data.Time.Clock (UTCTime, getCurrentTime)
5456

5557
import Control.Concurrent.STM (TMVar, newEmptyTMVarIO,
5658
putTMVar, readTMVar,
57-
tryReadTMVar, tryTakeTMVar)
59+
tryPutTMVar, tryReadTMVar,
60+
tryTakeTMVar)
5861
import PostgREST.Auth.JwtCache (JwtCacheState, update)
5962
import PostgREST.Config (AppConfig (..),
6063
readAppConfig,
@@ -70,7 +73,13 @@ import PostgREST.SchemaCache (SchemaCache (..),
7073
showSummary)
7174
import PostgREST.SchemaCache.Identifiers (quoteQi)
7275

73-
import Protolude
76+
import qualified Data.Aeson as JSON
77+
import qualified Data.ByteString.Lazy as LBS
78+
import qualified Network.HTTP.Client as HC
79+
import Network.URI (URI (..), URIAuth (..),
80+
parseURI, unEscapeString)
81+
import Protolude
82+
import System.IO.Error (IOError, userError)
7483

7584
data AppState = AppState
7685
-- | Database connection pool
@@ -109,16 +118,56 @@ newtype SchemaCacheStatus = SchemaCacheStatus
109118
{ getSCStatusTMVar :: TMVar Bool
110119
}
111120

112-
init :: AppConfig -> IO () -> IO AppState
113-
init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller = do
121+
init :: AppConfig -> IO () -> Maybe Text -> IO AppState
122+
init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller schemaCacheLoadUri = do
114123
loggerState <- Logger.init
115124
metricsState <- Metrics.init configDbPoolSize
116125
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
117126

118127
observer $ AppStartObs prettyVersion
119128

120129
pool <- initPool conf observer
121-
initWithPool pool conf loggerState metricsState observer appKiller
130+
appState <- initWithPool pool conf loggerState metricsState observer appKiller
131+
132+
runInitialSchemaCacheLoader observer schemaCacheLoadUri appState
133+
134+
pure appState
135+
136+
runInitialSchemaCacheLoader :: ObservationHandler -> Maybe Text -> AppState -> IO ()
137+
runInitialSchemaCacheLoader observer schemaCacheLoadUri AppState{stateSchemaCache, stateSCacheStatus=SchemaCacheStatus{getSCStatusTMVar}} = do
138+
void $ forkIO $
139+
traverse (fetchInitialSchemaCache observer) schemaCacheLoadUri >>= foldMap setInitialSchemaCache . join
140+
where
141+
setInitialSchemaCache sc =
142+
whenM (atomically $ tryPutTMVar getSCStatusTMVar True) $
143+
atomicModifyIORef stateSchemaCache $ (, ()) . maybe (Just sc) Just
144+
145+
fetchInitialSchemaCache :: ObservationHandler -> Text -> IO (Maybe SchemaCache)
146+
fetchInitialSchemaCache observer uri = flip catches [
147+
Handler (handleError @IOError),
148+
Handler (handleError @HC.HttpException),
149+
Handler (handleError @JSON.AesonException)
150+
] $ maybe (throwIO $ userError "Invalid schema cache dump URI") pure =<< traverse fetchURI (parseURI $ toS uri)
151+
where
152+
handleError :: Show e => e -> IO (Maybe a)
153+
handleError = (Nothing <$) . observer . SchemaCacheDumpFailureObs uri . show
154+
155+
fetchURI URI{uriScheme, ..}
156+
| uriScheme == "file:" = do
157+
path <- fileURIPath uriAuthority uriPath
158+
Just <$> (JSON.throwDecode =<< LBS.readFile path)
159+
| uriScheme `elem` ["http:", "https:"] = do
160+
request <- HC.parseUrlThrow $ toS uri
161+
manager <- HC.newManager HC.defaultManagerSettings
162+
HC.withResponse request manager $ \response ->
163+
Just <$> (JSON.throwDecode . LBS.fromChunks =<< HC.brConsume (HC.responseBody response))
164+
| otherwise =
165+
throwIO $ userError $ "Unsupported schema cache dump URI scheme: " <> uriScheme
166+
167+
fileURIPath Nothing path = pure $ unEscapeString path
168+
fileURIPath (Just URIAuth{uriRegName=""}) path = pure $ unEscapeString path
169+
fileURIPath (Just URIAuth{uriRegName="localhost"}) path = pure $ unEscapeString path
170+
fileURIPath _ _ = throwIO $ userError "Only local file URIs are supported"
122171

123172
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO () -> IO AppState
124173
initWithPool pool conf loggerState metricsState observer appKiller = mdo

src/PostgREST/CLI.hs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
{-# LANGUAGE NamedFieldPuns #-}
2-
{-# LANGUAGE RecordWildCards #-}
1+
{-# LANGUAGE MonadComprehensions #-}
2+
{-# LANGUAGE NamedFieldPuns #-}
3+
{-# LANGUAGE RecordWildCards #-}
34
module PostgREST.CLI
45
( main
56
, CLI (..)
@@ -47,7 +48,7 @@ runAppCommand conf@AppConfig{..} runCmd = do
4748
-- explicitly close the connections to PostgreSQL on shutdown.
4849
-- 'AppState.destroy' takes care of that.
4950
bracket
50-
(AppState.init conf (killThread mainThreadId))
51+
(AppState.init conf (killThread mainThreadId) [schemaCacheLoadUri | CmdRun (Just schemaCacheLoadUri) <- Just runCmd])
5152
AppState.destroy
5253
(\appState -> case runCmd of
5354
CmdDumpConfig -> do
@@ -56,7 +57,7 @@ runAppCommand conf@AppConfig{..} runCmd = do
5657
CmdDumpSchema -> do
5758
when configDbConfig $ AppState.readInDbConfig True appState
5859
putStrLn =<< dumpSchema appState
59-
CmdRun -> App.run appState)
60+
CmdRun _ -> App.run appState)
6061

6162
-- | Dump SchemaCache schema to JSON
6263
dumpSchema :: AppState -> IO LBS.ByteString
@@ -85,7 +86,7 @@ data ClientCommand
8586
= CmdReady
8687

8788
data RunCommand
88-
= CmdRun
89+
= CmdRun (Maybe Text)
8990
| CmdDumpConfig
9091
| CmdDumpSchema
9192

@@ -119,25 +120,34 @@ readCLIShowHelp =
119120
cliParser :: O.Parser CLI
120121
cliParser =
121122
CLI
122-
<$> (dumpConfigFlag <|> dumpSchemaFlag <|> readyFlag)
123+
<$> (dumpConfigFlag <|> dumpSchemaFlag <|> readyFlag <|> runCommand)
123124
<*> O.optional configFileOption
124125

126+
runCommand =
127+
Run . CmdRun <$> O.optional schemaCacheUriOption
128+
129+
schemaCacheUriOption =
130+
O.strOption $
131+
O.long "schema-cache-uri"
132+
<> O.metavar "URI"
133+
<> O.help "Try pre-loading schema cache from provided URI"
134+
125135
configFileOption =
126136
O.strArgument $
127137
O.metavar "FILENAME"
128138
<> O.help "Path to configuration file"
129139

130140
dumpConfigFlag =
131-
O.flag (Run CmdRun) (Run CmdDumpConfig) $
141+
O.flag' (Run CmdDumpConfig) $
132142
O.long "dump-config"
133143
<> O.help "Dump loaded configuration and exit"
134144

135145
dumpSchemaFlag =
136-
O.flag (Run CmdRun) (Run CmdDumpSchema) $
146+
O.flag' (Run CmdDumpSchema) $
137147
O.long "dump-schema"
138148
<> O.help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)"
139149

140150
readyFlag =
141-
O.flag (Run CmdRun) (Client CmdReady) $
151+
O.flag' (Client CmdReady) $
142152
O.long "ready"
143153
<> O.help "Checks the health of PostgREST by doing a request on the admin server /ready endpoint"

src/PostgREST/Logger.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ observationMessages = \case
160160
<> " and "
161161
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
162162
<> ". " <> jsonMessage usageErr
163+
SchemaCacheDumpFailureObs uri err ->
164+
pure $ "Failed to load schema cache dump from " <> uri <> ". " <> err
163165
SchemaCacheQueriedObs resultTime timings ->
164166
[ "Schema cache queried in " <> showMillis resultTime <> " milliseconds " ] <>
165167
let showTimings qt = [ T.intercalate ", " $ (\(l, v) -> T.decodeUtf8 l <> ": " <> v <> " ms") <$> queryTimingsWLabels qt ] in

src/PostgREST/MediaType.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ data MediaType
3939
| MTVndSingularJSON Bool
4040
-- TODO MTVndPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
4141
| MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]
42-
deriving (Eq, Show, Generic, JSON.ToJSON)
42+
deriving (Eq, Show, Generic, JSON.FromJSON, JSON.ToJSON)
4343
instance Hashable MediaType
4444

4545
data MTVndPlanOption
4646
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
47-
deriving (Eq, Show, Generic, JSON.ToJSON)
47+
deriving (Eq, Show, Generic, JSON.FromJSON, JSON.ToJSON)
4848
instance Hashable MTVndPlanOption
4949

5050
data MTVndPlanFormat
5151
= PlanJSON | PlanText
52-
deriving (Eq, Show, Generic, JSON.ToJSON)
52+
deriving (Eq, Show, Generic, JSON.FromJSON, JSON.ToJSON)
5353
instance Hashable MTVndPlanFormat
5454

5555
-- | Convert MediaType to a Content-Type HTTP Header

src/PostgREST/Observation.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ data Observation
3434
| DBConnectedObs Text
3535
| SchemaCacheEmptyObs
3636
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
37+
| SchemaCacheDumpFailureObs Text Text
3738
| SchemaCacheQueriedObs Double (Maybe QueryTimings)
3839
| SchemaCacheLoadedObs Double Text
3940
| ConnectionRetryObs Int

src/PostgREST/SchemaCache.hs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ module PostgREST.SchemaCache
2828
, queryTimingsWLabels
2929
) where
3030

31-
import Data.Aeson ((.=))
31+
import Data.Aeson ((.:), (.=))
3232
import qualified Data.Aeson as JSON
3333

3434
import qualified Data.ByteString.Char8 as BS
@@ -101,6 +101,17 @@ instance JSON.ToJSON SchemaCache where
101101
, "dbTimezones" .= JSON.toJSON tzs
102102
]
103103

104+
instance JSON.FromJSON SchemaCache where
105+
parseJSON = JSON.withObject "SchemaCache" $ \o -> do
106+
tabs <- o .: "dbTables"
107+
SchemaCache tabs
108+
<$> o .: "dbRelationships"
109+
<*> o .: "dbRoutines"
110+
<*> o .: "dbRepresentations"
111+
<*> o .: "dbMediaHandlers"
112+
<*> o .: "dbTimezones"
113+
<*> pure (tablesFuzzyIndex tabs)
114+
104115
showSummary :: SchemaCache -> Text
105116
showSummary (SchemaCache tbls rels routs reps mediaHdlrs tzs _) =
106117
T.intercalate ", "
@@ -191,14 +202,20 @@ querySchemaCache conf@AppConfig{..} = do
191202
, dbTablesFuzzyIndex =
192203
-- Only build fuzzy index for schemas with a reasonable number of tables
193204
-- Fuzzy.FuzzySet is memory heavy we just don't use it for large schemas
194-
Fuzzy.fromList <$> HM.filter ((< maxDbTablesForFuzzySearch) . length) (HM.fromListWith (<>) ((qiSchema &&& pure . qiName) <$> HM.keys tabsWViewsPks))
205+
tablesFuzzyIndex tabsWViewsPks
195206
}, qsTime)
196207
where
197208
schemas = toList configDbSchemas
198209
isLogDebug = configLogLevel == LogDebug
199210
sqlTimedStmt = sqlTimedStatement isLogDebug
200211
sleepCall = SQL.Statement "select pg_sleep($1 / 1000.0)" (param HE.int4) HD.noResult True
201212

213+
tablesFuzzyIndex :: TablesMap -> TablesFuzzyIndex
214+
tablesFuzzyIndex tabs =
215+
Fuzzy.fromList <$>
216+
HM.filter ((< maxDbTablesForFuzzySearch) . length)
217+
(HM.fromListWith (<>) ((qiSchema &&& pure . qiName) <$> HM.keys tabs))
218+
202219
-- | overrides detected relationships with the computed relationships and gets the RelationshipsMap
203220
getOverrideRelationshipsMap :: [Relationship] -> [Relationship] -> RelationshipsMap
204221
getOverrideRelationshipsMap rels cRels =

src/PostgREST/SchemaCache/Identifiers.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import qualified Data.Text as T
2020
import Protolude
2121

2222
data RelIdentifier = RelId QualifiedIdentifier | RelAnyElement
23-
deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey, Show)
23+
deriving (Eq, Ord, Generic, JSON.FromJSON, JSON.FromJSONKey, JSON.ToJSON, JSON.ToJSONKey, Show)
2424
instance Hashable RelIdentifier
2525

2626
-- | Represents a pg identifier with a prepended schema name "schema.table".
@@ -30,7 +30,7 @@ data QualifiedIdentifier = QualifiedIdentifier
3030
{ qiSchema :: Schema
3131
, qiName :: TableName
3232
}
33-
deriving (Eq, Show, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)
33+
deriving (Eq, Show, Ord, Generic, JSON.FromJSON, JSON.FromJSONKey, JSON.ToJSON, JSON.ToJSONKey)
3434

3535
instance Hashable QualifiedIdentifier
3636

src/PostgREST/SchemaCache/Relationship.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ data Relationship = Relationship
3535
, relToOne :: Bool
3636
, relIsSelf :: Bool
3737
}
38-
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)
38+
deriving (Eq, Show, Ord, Generic, JSON.FromJSON, JSON.ToJSON)
3939

4040
-- | The relationship cardinality
4141
-- | https://en.wikipedia.org/wiki/Cardinality_(data_modeling)
@@ -48,7 +48,7 @@ data Cardinality
4848
-- ^ one-to-one, this is a refinement over M2O, operating on it is pretty much the same as M2O when isParent == False
4949
| M2M Junction
5050
-- ^ many-to-many
51-
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)
51+
deriving (Eq, Show, Ord, Generic, JSON.FromJSON, JSON.ToJSON)
5252

5353
type FKConstraint = Text
5454

@@ -60,7 +60,7 @@ data Junction = Junction
6060
, junColsSource :: [(FieldName, FieldName)]
6161
, junColsTarget :: [(FieldName, FieldName)]
6262
}
63-
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)
63+
deriving (Eq, Show, Ord, Generic, JSON.FromJSON, JSON.ToJSON)
6464

6565
-- | Key based on the source table and the foreign table schema
6666
type RelationshipsMap = HM.HashMap (QualifiedIdentifier, Schema) [Relationship]

0 commit comments

Comments
 (0)