tmp-postgres-1.7.0.0: src/Database/Postgres/Temp/Internal/Core.hs
{-# OPTIONS_HADDOCK prune #-}
{-|
This module provides the low level functionality for running @initdb@, @postgres@ and @createdb@ to make a database.
See 'startPlan' for more details.
-}
module Database.Postgres.Temp.Internal.Core where
import qualified Database.PostgreSQL.Simple.Options as Client
import qualified Database.PostgreSQL.Simple as PG
import System.Process.Internals
import System.Exit (ExitCode(..))
import Data.String
import System.Posix.Signals (sigINT, signalProcess)
import Control.Exception
import Data.Foldable (for_)
import Control.Concurrent.Async (race_)
import Control.Monad (forever, (>=>))
import Control.Concurrent (threadDelay)
import Data.Typeable
import System.IO
import System.Process
import Data.Monoid
import Text.PrettyPrint.ANSI.Leijen hiding ((<$>))
import qualified Data.ByteString.Char8 as BSC
-- | Internal events for debugging
data Event
= StartPostgres
| WaitForDB
| StartPlan String
| TryToConnect
deriving (Show, Eq, Ord)
-- | A list of failures that can occur when starting. This is not
-- and exhaustive list but covers the errors that the system
-- catches for the user.
data StartError
= StartPostgresFailed ExitCode
-- ^ @postgres@ failed before a connection succeeded. Most likely this
-- is due to invalid configuration
| InitDbFailed ExitCode
-- ^ @initdb@ failed. This can be from invalid configuration or using a
-- non-empty data directory
| CreateDbFailed ExitCode
-- ^ @createdb@ failed. This can be from invalid configuration or
-- the database might already exist.
| CompletePlanFailed String [String]
-- ^ The 'Database.Postgres.Temp.Partial.PartialPlan' was missing info and a complete 'Plan' could
-- not be created.
deriving (Show, Eq, Ord, Typeable)
instance Exception StartError
-- | A way to log internal 'Event's
type Logger = Event -> IO ()
-- TODO. Add a Retrying Event
-- | @postgres@ is not ready until we are able to successfully connect.
-- 'waitForDB' attempts to connect over and over again and returns
-- after the first successful connection.
waitForDB :: Logger -> Client.Options -> IO ()
waitForDB logger options = do
logger TryToConnect
let theConnectionString = Client.toConnectionString options
startAction = PG.connectPostgreSQL theConnectionString
try (bracket startAction PG.close mempty) >>= \case
Left (_ :: IOError) -> threadDelay 10000 >> waitForDB logger options
Right () -> return ()
-- | 'ProcessConfig' contains the configuration necessary for starting a
-- process. It is essentially a stripped down 'System.Process.CreateProcess'.
data ProcessConfig = ProcessConfig
{ processConfigEnvVars :: [(String, String)]
-- ^ Environment variables
, processConfigCmdLine :: [String]
-- ^ Command line arguements
, processConfigStdIn :: Handle
-- ^ The 'Handle' for standard input
, processConfigStdOut :: Handle
-- ^ The 'Handle' for standard output
, processConfigStdErr :: Handle
-- ^ The 'Handle' for standard error
}
prettyKeyPair ::(Pretty a, Pretty b) => a -> b -> Doc
prettyKeyPair k v = pretty k <> text ": " <> pretty v
instance Pretty ProcessConfig where
pretty ProcessConfig {..}
= text "processConfigEnvVars:"
<> softline
<> indent 2 (vsep (map (uncurry prettyKeyPair) processConfigEnvVars))
<> hardline
<> text "processConfigCmdLine:"
<> softline
<> text (unwords processConfigCmdLine)
-- | Start a process interactively and return the 'ProcessHandle'
startProcess
:: String
-- ^ Process name
-> ProcessConfig
-- ^ Process config
-> IO ProcessHandle
startProcess name ProcessConfig {..} = (\(_, _, _, x) -> x) <$>
createProcess_ name (proc name processConfigCmdLine)
{ std_err = UseHandle processConfigStdErr
, std_out = UseHandle processConfigStdOut
, std_in = UseHandle processConfigStdIn
, env = Just processConfigEnvVars
}
-- | Start a process and block until it finishes return the 'ExitCode'.
executeProcess
:: String
-- ^ Process name
-> ProcessConfig
-- ^ Process config
-> IO ExitCode
executeProcess name = startProcess name >=> waitForProcess
-------------------------------------------------------------------------------
-- PostgresProcess Life cycle management
-------------------------------------------------------------------------------
-- | 'PostgresPlan' is used be 'startPostgresProcess' to start the @postgres@
-- and then attempt to connect to it.
data PostgresPlan = PostgresPlan
{ postgresPlanProcessConfig :: ProcessConfig
-- ^ The process config for @postgres@
, postgresPlanClientOptions :: Client.Options
-- ^ Connection options. Used to verify that @postgres@ is ready.
}
instance Pretty PostgresPlan where
pretty PostgresPlan {..}
= text "postgresPlanProcessConfig:"
<> softline
<> indent 2 (pretty postgresPlanProcessConfig)
<> hardline
<> text "postgresPlanClientOptions:"
<+> prettyOptions postgresPlanClientOptions
prettyOptions :: Client.Options -> Doc
prettyOptions = text . BSC.unpack . Client.toConnectionString
-- | The output of calling 'startPostgresProcess'.
data PostgresProcess = PostgresProcess
{ postgresProcessClientOptions :: Client.Options
-- ^ Connection options
, postgresProcessHandle :: ProcessHandle
-- ^ @postgres@ process handle
}
instance Pretty PostgresProcess where
pretty PostgresProcess {..}
= text "postgresProcessClientOptions:"
<+> prettyOptions postgresProcessClientOptions
-- Force all connections to the database to close.
-- Called during shutdown as well.
terminateConnections :: Client.Options-> IO ()
terminateConnections options = do
let theConnectionString = Client.toConnectionString options
terminationQuery = fromString $ unlines
[ "SELECT pg_terminate_backend(pid)"
, "FROM pg_stat_activity"
, "WHERE datname=?;"
]
e <- try $ bracket (PG.connectPostgreSQL theConnectionString) PG.close $
\conn -> PG.execute conn terminationQuery
[getLast $ Client.dbname options]
case e of
Left (_ :: IOError) -> pure ()
Right _ -> pure ()
-- | Stop the @postgres@ process after attempting to terminate all the
-- connections.
stopPostgresProcess :: PostgresProcess -> IO ExitCode
stopPostgresProcess PostgresProcess{..} = do
withProcessHandle postgresProcessHandle $ \case
OpenHandle p -> do
-- try to terminate the connects first. If we can't terminate still
-- keep shutting down
terminateConnections postgresProcessClientOptions
signalProcess sigINT p
OpenExtHandle {} -> pure () -- TODO log windows is not supported
ClosedHandle _ -> return ()
waitForProcess postgresProcessHandle
-- | Start the @postgres@ process and block until a successful connection
-- occurs. A separate thread we continously check to see if the @postgres@
-- process has crashed.
startPostgresProcess :: Logger -> PostgresPlan -> IO PostgresProcess
startPostgresProcess logger PostgresPlan {..} = do
logger StartPostgres
let startAction = PostgresProcess postgresPlanClientOptions
<$> startProcess "postgres" postgresPlanProcessConfig
-- Start postgres and stop if an exception occurs
bracketOnError startAction stopPostgresProcess $
\result@PostgresProcess {..} -> do
-- A helper to check if the process has died
let checkForCrash = do
mExitCode <- getProcessExitCode postgresProcessHandle
for_ mExitCode (throwIO . StartPostgresFailed)
logger WaitForDB
-- We assume that 'template1' exist and make connection
-- options to test if postgres is ready.
let options = postgresPlanClientOptions
{ Client.dbname = pure "template1"
}
-- Block until a connection succeeds or postgres crashes
waitForDB logger options
`race_` forever (checkForCrash >> threadDelay 100000)
-- Postgres is now ready so return
return result
-------------------------------------------------------------------------------
-- Plan
-------------------------------------------------------------------------------
-- | 'Plan' is the low level configuration necessary for creating a database
-- starting @postgres@ and creating a database. There is no validation done
-- on the 'Plan'. It is recommend that one use the higher level functions
-- such as 'Database.Postgres.Temp.start' which will generate plans that
-- are valid. 'Plan's are used internally but are exposed if the higher
-- level plan generation is not sufficent.
data Plan = Plan
{ planLogger :: Logger
, planInitDb :: Maybe ProcessConfig
, planCreateDb :: Maybe ProcessConfig
, planPostgres :: PostgresPlan
, planConfig :: String
, planDataDirectory :: FilePath
}
instance Pretty Plan where
pretty Plan {..}
= text "planInitDb:"
<> softline
<> indent 2 (pretty planInitDb)
<> hardline
<> text "planCreateDb:"
<> softline
<> indent 2 (pretty planCreateDb)
<> hardline
<> text "planPostgres:"
<> softline
<> indent 2 (pretty planPostgres)
<> hardline
<> text "planConfig:"
<> softline
<> indent 2 (pretty planConfig)
<> hardline
<> text "planDataDirectory:"
<+> pretty planDataDirectory
-- A simple helper to throw 'ExitCode's when they are 'ExitFailure'.
throwIfNotSuccess :: Exception e => (ExitCode -> e) -> ExitCode -> IO ()
throwIfNotSuccess f = \case
ExitSuccess -> pure ()
e -> throwIO $ f e
-- | 'startPlan' optionally calls @initdb@, optionally calls @createdb@ and
-- unconditionally calls @postgres@.
-- Additionally it writes a \"postgresql.conf\" and does not return until
-- the @postgres@ process is ready. See 'startPostgresProcess' for more
-- details.
startPlan :: Plan -> IO PostgresProcess
startPlan plan@Plan {..} = do
planLogger $ StartPlan $ show $ pretty plan
for_ planInitDb $ executeProcess "initdb" >=>
throwIfNotSuccess InitDbFailed
-- We must provide a config file before we can start postgres.
writeFile (planDataDirectory <> "/postgresql.conf") planConfig
let startAction = startPostgresProcess planLogger planPostgres
bracketOnError startAction stopPostgresProcess $ \result -> do
for_ planCreateDb $ executeProcess "createdb" >=>
throwIfNotSuccess CreateDbFailed
pure result
-- | Stop the @postgres@ process. See 'stopPostgresProcess' for more details.
stopPlan :: PostgresProcess -> IO ExitCode
stopPlan = stopPostgresProcess