funflow-1.5.0: app/FFExecutorD.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
import Control.Applicative
import Control.Concurrent (myThreadId)
import Control.Concurrent.MVar
import Control.Exception.Base (AsyncException (UserInterrupt))
import Control.Exception.Safe
import qualified Control.Funflow.ContentStore as CS
import Control.Funflow.External.Coordinator
import Control.Funflow.External.Coordinator.Redis
import Control.Funflow.External.Coordinator.SQLite
import Control.Funflow.External.Executor
import Control.Monad (void)
import Data.Monoid ((<>))
import qualified Database.Redis as R
import Network.Socket (PortNumber)
import qualified Options.Applicative as Opt
import Path
import System.Clock
import System.IO
import qualified System.Posix.Signals as Signals
data UseCoord
= UseRedis (Config Redis)
| UseSQLite FilePath
data Args = Args
{ storePath :: FilePath
, coordinator :: UseCoord
}
argsParser :: Opt.Parser Args
argsParser = Opt.subparser
$ Opt.command "redis"
(Opt.info
(redisParser Opt.<**> Opt.helper)
(Opt.progDesc "Use Redis coordinator."))
<> Opt.command "sqlite"
(Opt.info
(sqliteParser Opt.<**> Opt.helper)
(Opt.progDesc "Use SQLite coordinator."))
<> Opt.metavar "COORDINATOR"
<> Opt.commandGroup "Available coordinators:"
where
storeParser = Opt.strArgument
$ Opt.metavar "STOREDIR"
<> Opt.help "Path to the root of the content store."
redisParser = useRedis
<$> storeParser
<*> Opt.strArgument
( Opt.metavar "HOSTNAME"
<> Opt.help "Host for the Redis instance." )
<*> Opt.argument Opt.auto
( Opt.metavar "PORT"
<> Opt.help "Port for the Redis instance." )
<*> optional (Opt.argument Opt.auto
( Opt.metavar "PASSWORD"
<> Opt.help "Password for the Redis instance, if needed." ))
useRedis store host port pw = Args store $ UseRedis R.defaultConnectInfo
{ R.connectHost = host
, R.connectPort = R.PortNumber (port :: PortNumber)
, R.connectAuth = pw
}
sqliteParser = useSQLite
<$> storeParser
<*> Opt.strArgument
( Opt.metavar "SQLFILE"
<> Opt.help "Path to SQLite database file." )
useSQLite store sqlite = Args store $ UseSQLite sqlite
parseArgs :: IO Args
parseArgs = Opt.execParser $ Opt.info (argsParser Opt.<**> Opt.helper)
$ Opt.fullDesc
<> Opt.progDesc
"Await and execute funflow external tasks on the given coordinator."
<> Opt.header "ffexecutord - Funflow task executor"
data HandlerState
= Initial
| Terminating TimeSpec TimeSpec
data HandlerInstruction
= Terminate
| Ignore
| Message
main :: IO ()
main = do
config <- parseArgs
-- Configure custom interrupt handler.
-- GHC's default interrupt handler works only one time. On the second
-- interrupt the process will terminate immediately without any cleanup.
-- This could leave the funflow store and coordinator in an invalid state.
-- This installs a handler that works on multiple interrupts.
-- On the first interrupt it will send an async `UserInterrupt` exception to
-- the main-thread. On further interrupts it will print a message clarifying
-- that clean-up is ongoing.
mainThreadId <- myThreadId
mvState <- newMVar Initial
let handler name = Signals.Catch $ do
let msgTime = fromNanoSecs 1000000000
instr <- modifyMVar mvState $ \case
Initial -> do
t <- getTime Monotonic
return (Terminating t t, Terminate)
Terminating tsig tmsg -> do
t' <- getTime Monotonic
return $
if t' `diffTimeSpec` tmsg > msgTime then
(Terminating tsig 0, Message)
else
(Terminating tsig tmsg, Ignore)
case instr of
Terminate -> throwTo mainThreadId UserInterrupt
Ignore -> return ()
Message -> void $ tryAny $ hPutStrLn stderr $
"Handling " ++ name ++ " signal. Awaiting cleanup."
installHandler signal name = void $
Signals.installHandler signal (handler name) Nothing
installHandler Signals.sigINT "interrupt"
installHandler Signals.sigQUIT "quit"
installHandler Signals.sigTERM "terminate"
-- XXX: Improve handling of invalid paths.
storePath' <- parseAbsDir (storePath config)
case coordinator config of
UseRedis redisConfig -> CS.withStore storePath' $
executeLoop Redis redisConfig
UseSQLite sqlitePath -> do
sqlitePath' <- parseAbsDir sqlitePath
CS.withStore storePath' $
executeLoop SQLite sqlitePath'