refactor: move AppState type and update functions to AppState/Types.hs

`AppState.hs` is imported by many modules. Some of those modules contain
important functions that need to be imported back to `AppState.hs`, causing
circular dependency problem.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
This commit is contained in:
Taimoor Zaeem
2026-07-25 14:39:48 +05:00
parent e820cc7370
commit a108d66968
3 changed files with 106 additions and 81 deletions
+1
View File
@@ -46,6 +46,7 @@ library
exposed-modules: PostgREST.Admin exposed-modules: PostgREST.Admin
PostgREST.App PostgREST.App
PostgREST.AppState PostgREST.AppState
PostgREST.AppState.Types
PostgREST.Auth PostgREST.Auth
PostgREST.Auth.Jwt PostgREST.Auth.Jwt
PostgREST.Auth.JwtCache PostgREST.Auth.JwtCache
+7 -81
View File
@@ -47,12 +47,12 @@ import PostgREST.Version (prettyVersion)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction) import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay, import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
exponentialBackoff, retrying, rsPreviousDelay) exponentialBackoff, retrying, rsPreviousDelay)
import Data.IORef (IORef, atomicWriteIORef, newIORef, readIORef) import Data.IORef (newIORef, readIORef)
import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Time.Clock (getCurrentTime)
import Control.Concurrent.STM (TMVar, newEmptyTMVarIO, putTMVar, import Control.Concurrent.STM (newEmptyTMVarIO, putTMVar, readTMVar,
readTMVar, tryReadTMVar, tryTakeTMVar) tryReadTMVar, tryTakeTMVar)
import PostgREST.Auth.JwtCache (JwtCacheState, update) import PostgREST.Auth.JwtCache (update)
import PostgREST.Config (AppConfig (..), readAppConfig, import PostgREST.Config (AppConfig (..), readAppConfig,
toConnectionSettings) toConnectionSettings)
import PostgREST.Config.Database (queryDbSettings, queryPgVersion, import PostgREST.Config.Database (queryDbSettings, queryPgVersion,
@@ -63,45 +63,10 @@ import PostgREST.SchemaCache (SchemaCache (..), querySchemaCache,
showSummary) showSummary)
import PostgREST.SchemaCache.Identifiers (quoteQi) import PostgREST.SchemaCache.Identifiers (quoteQi)
import PostgREST.AppState.Types
import Protolude import Protolude
data AppState = AppState
-- | Database connection pool
{ statePool :: SQL.Pool
-- | Database server version
, statePgVersion :: IORef PgVersion
-- | Schema cache
, stateSchemaCache :: IORef (Maybe SchemaCache)
-- | The schema cache status
, stateSCacheStatus :: SchemaCacheStatus
-- | State of the LISTEN channel
, stateIsListenerOn :: IORef Bool
-- | starts the connection worker with a debounce
, debouncedSCacheLoader :: IO ()
-- | Config that can change at runtime
, stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration
, stateGetTime :: IO UTCTime
-- | Used for killing the main thread in case a subthread fails
, stateKillApp :: IO ()
-- | Keeps track of the next delay for db connection retry
, stateNextDelay :: IORef Int
-- | Observation handler
, stateObserver :: ObservationHandler
-- | JWT Cache
, stateJwtCache :: JwtCache.JwtCacheState
, stateLogger :: Logger.LoggerState
, stateMetrics :: Metrics.MetricsState
}
-- | Schema cache status.
-- Empty means initial loading on startup, False means pending and True means loaded.
-- "Initial" state is needed so that we can wait with application socket listening
-- until after initial schema cache querying.
newtype SchemaCacheStatus = SchemaCacheStatus
{ getSCStatusTMVar :: TMVar Bool
}
init :: AppConfig -> IO () -> IO AppState init :: AppConfig -> IO () -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller = do init conf@AppConfig{configLogLevel, configDbPoolSize} appKiller = do
loggerState <- Logger.init loggerState <- Logger.init
@@ -221,39 +186,6 @@ flushPool AppState{..} = do
SQL.release statePool SQL.release statePool
stateObserver PoolFlushed stateObserver PoolFlushed
getPgVersion :: AppState -> IO PgVersion
getPgVersion = readIORef . statePgVersion
putPgVersion :: AppState -> PgVersion -> IO ()
putPgVersion = atomicWriteIORef . statePgVersion
getSchemaCache :: AppState -> IO (Maybe SchemaCache)
getSchemaCache = readIORef . stateSchemaCache
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
schemaCacheLoader :: AppState -> IO ()
schemaCacheLoader = debouncedSCacheLoader
getNextDelay :: AppState -> IO Int
getNextDelay = readIORef . stateNextDelay
getConfig :: AppState -> IO AppConfig
getConfig = readIORef . stateConf
putConfig :: AppState -> AppConfig -> IO ()
putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime
getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache
killApp :: AppState -> IO ()
killApp = stateKillApp
isConnEstablished :: AppState -> IO Bool isConnEstablished :: AppState -> IO Bool
isConnEstablished appState = do isConnEstablished appState = do
AppConfig{..} <- getConfig appState AppConfig{..} <- getConfig appState
@@ -262,9 +194,6 @@ isConnEstablished appState = do
else -- otherwise the only way to check the connection is to make a query else -- otherwise the only way to check the connection is to make a query
isRight <$> usePool appState (SQL.sql "SELECT 1") isRight <$> usePool appState (SQL.sql "SELECT 1")
putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
isLoaded :: AppState -> IO Bool isLoaded :: AppState -> IO Bool
isLoaded x = do isLoaded x = do
scacheLoaded <- isSchemaCacheLoaded x scacheLoaded <- isSchemaCacheLoaded x
@@ -277,9 +206,6 @@ isPending x = do
connEstablished <- isConnEstablished x connEstablished <- isConnEstablished x
return $ not scacheLoaded || not connEstablished return $ not scacheLoaded || not connEstablished
getObserver :: AppState -> ObservationHandler
getObserver = stateObserver
-- | Try to load the schema cache and retry if it fails. -- | Try to load the schema cache and retry if it fails.
-- --
-- This is done by repeatedly: 1) flushing the pool, 2) querying the version and validating that the postgres version is supported by us, and 3) loading the schema cache. -- This is done by repeatedly: 1) flushing the pool, 2) querying the version and validating that the postgres version is supported by us, and 3) loading the schema cache.
+98
View File
@@ -0,0 +1,98 @@
{-|
Module : PostgREST.AppState.Types
Description : AppState data type and stateful functions
-}
module PostgREST.AppState.Types where
import qualified Hasql.Pool as SQL
import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation
import Data.IORef (IORef, atomicWriteIORef, readIORef)
import Data.Time.Clock (UTCTime)
import Control.Concurrent.STM (TMVar)
import PostgREST.Auth.JwtCache (JwtCacheState)
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.SchemaCache (SchemaCache (..))
import Protolude
data AppState = AppState
-- | Database connection pool
{ statePool :: SQL.Pool
-- | Database server version
, statePgVersion :: IORef PgVersion
-- | Schema cache
, stateSchemaCache :: IORef (Maybe SchemaCache)
-- | The schema cache status
, stateSCacheStatus :: SchemaCacheStatus
-- | State of the LISTEN channel
, stateIsListenerOn :: IORef Bool
-- | starts the connection worker with a debounce
, debouncedSCacheLoader :: IO ()
-- | Config that can change at runtime
, stateConf :: IORef AppConfig
-- | Time used for verifying JWT expiration
, stateGetTime :: IO UTCTime
-- | Used for killing the main thread in case a subthread fails
, stateKillApp :: IO ()
-- | Keeps track of the next delay for db connection retry
, stateNextDelay :: IORef Int
-- | Observation handler
, stateObserver :: ObservationHandler
-- | JWT Cache
, stateJwtCache :: JwtCache.JwtCacheState
, stateLogger :: Logger.LoggerState
, stateMetrics :: Metrics.MetricsState
}
-- | Schema cache status.
-- Empty means initial loading on startup, False means pending and True means loaded.
-- "Initial" state is needed so that we can wait with application socket listening
-- until after initial schema cache querying.
newtype SchemaCacheStatus = SchemaCacheStatus
{ getSCStatusTMVar :: TMVar Bool
}
getPgVersion :: AppState -> IO PgVersion
getPgVersion = readIORef . statePgVersion
putPgVersion :: AppState -> PgVersion -> IO ()
putPgVersion = atomicWriteIORef . statePgVersion
getSchemaCache :: AppState -> IO (Maybe SchemaCache)
getSchemaCache = readIORef . stateSchemaCache
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
schemaCacheLoader :: AppState -> IO ()
schemaCacheLoader = debouncedSCacheLoader
getNextDelay :: AppState -> IO Int
getNextDelay = readIORef . stateNextDelay
getConfig :: AppState -> IO AppConfig
getConfig = readIORef . stateConf
putConfig :: AppState -> AppConfig -> IO ()
putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime
getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache
killApp :: AppState -> IO ()
killApp = stateKillApp
putIsListenerOn :: AppState -> Bool -> IO ()
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
getObserver :: AppState -> ObservationHandler
getObserver = stateObserver