SciFlow-drmaa-0.1.0: src/Control/Workflow/Coordinator/Drmaa.hs
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
module Control.Workflow.Coordinator.Drmaa
( Drmaa
, DrmaaConfig(..)
, getDefaultDrmaaConfig
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad (replicateM)
import Network.HostName (getHostName)
import qualified Data.HashMap.Strict as M
import qualified DRMAA as D
import Control.Distributed.Process
import Data.List (foldl')
import Control.Concurrent.STM
import Control.Concurrent (threadDelay)
import Control.Monad (forever)
import System.Environment (getExecutablePath, getEnv)
import Network.Transport.TCP (createTransport, defaultTCPAddr, defaultTCPParameters)
import Control.Distributed.Process.Node
import Text.Printf (printf)
import Data.Maybe (fromMaybe)
import System.Random (randomRIO)
import Control.Workflow.Coordinator
import Control.Workflow
import Control.Workflow.Utils
import Control.Workflow.Types
data DrmaaConfig = DrmaaConfig
{ _queue_size :: Int
, _cmd :: (FilePath, [String]) -- ^ Command to start the worker process
, _cpu_format :: String -- ^ How to specify cpu number, default: "--ntasks-per-node=%d"
, _memory_format :: String -- ^ How to specify memory, default: "--mem=%dG"
, _queue_format :: String
, _drmaa_parameters :: Maybe String -- ^ additional drmaa parameters
}
getDefaultDrmaaConfig :: [String] -- ^ Parameters of the executable
-- that runs on the workers
-> IO DrmaaConfig
getDefaultDrmaaConfig params = do
exePath <- getExecutablePath
return $ DrmaaConfig
{ _queue_size = 100
, _cmd = (exePath, params)
, _cpu_format = "--ntasks-per-node=%d"
, _memory_format = "--mem=%d000"
, _queue_format = "-p %s"
, _drmaa_parameters = Nothing }
data Drmaa = Drmaa
{ _worker_pool :: TMVar WorkerPool
, _config :: DrmaaConfig }
instance Coordinator Drmaa where
type Config Drmaa = DrmaaConfig
withCoordinator config f = D.withSession $ liftIO drmaa >>= f
where
drmaa = Drmaa <$> newTMVarIO (WorkerPool 0 M.empty) <*> return config
initiate coord = do
getSelfPid >>= register "SciFlow_master"
-- Kill idle worker periodically.
forever $ liftIO (threadDelay 5000000) >> killIdleWorkers
where
killIdleWorkers = liftIO (atomically getIdleWorkers) >>= \case
Nothing -> return ()
Just (workers, pool) -> do
mapM_ (flip send Shutdown) workers
liftIO $ atomically $ putTMVar (_worker_pool coord) $
foldl' (flip removeWorker) pool workers
getIdleWorkers = filter ((==Idle) . _worker_status) <$>
getWorkers coord >>= \case
[] -> return Nothing
x -> do
pool <- takeTMVar $ _worker_pool coord
return $ Just (map _worker_id x, pool)
shutdown coord = do
liftIO (atomically $ getWorkers coord) >>=
mapM_ (\worker -> send (_worker_id worker) Shutdown)
liftIO $ threadDelay 1000000
startClient _ serverAddr rf = do
host <- getHostName
transport <- tryCreateTransport host ([8000..8200] :: [Int])
nd <- newLocalNode transport $ _rtable rf
runProcess nd $ do
-- Link to the main process
linkNode serverAddr
serverPid <- liftIO (getEnv "master_id") >>= searchServer serverAddr
getSelfPid >>= send serverPid
(expect :: Process Signal) >>= \case
Shutdown -> return ()
where
searchServer :: NodeId -> String -> Process ProcessId
searchServer server name = do
whereisRemoteAsync server name
expectTimeout 1000000 >>= \case
Just (WhereIsReply _ (Just sid)) -> return sid
_ -> liftIO (putStrLn "Server not found") >> terminate
tryCreateTransport host (p:ports) = createTransport
(defaultTCPAddr host (show p)) defaultTCPParameters >>= \case
Left _ -> tryCreateTransport host ports
Right trsp -> return trsp
tryCreateTransport _ _ = error "Failed to create transport"
getWorkers Drmaa{..} = M.elems . _cur_workers <$> readTMVar _worker_pool
reserve coord@Drmaa{..} wc = tryReserve >>= \case
-- Queue is full
Nothing -> liftIO (threadDelay 1000000) >> reserve coord wc
-- Idle worker exist
Just (Right nd) -> return nd
-- Try to get a new worker
Just (Left pool) -> do
liftIO $ atomically $ putTMVar _worker_pool $
pool{_len_waitlist = _len_waitlist pool + 1}
worker <- spawnWorker _config wc
liftIO $ atomically $ do
pool' <- addWorker (_worker_id worker)
worker{_worker_status = Working } <$> takeTMVar _worker_pool
putTMVar _worker_pool $
pool'{_len_waitlist = _len_waitlist pool' - 1}
infoS $ "Found a new worker: " ++ show (_worker_id worker)
return $ _worker_id worker
where
-- Try reserving a work, return the worker id if succeed; return Nothing
-- if no worker is available; return the coordinator lock if a new worker
-- is going to be spawned.
tryReserve = liftIO $ atomically $ do
workers <- getWorkers coord
waiting <- _len_waitlist <$> readTMVar _worker_pool
case filter isQualified workers of
(w:_) -> do
setWorkerStatus coord (_worker_id w) Working
return $ Just $ Right $ _worker_id w
[] -> if length workers + waiting < _queue_size _config
then Just . Left <$> takeTMVar _worker_pool
else return Nothing
isQualified Worker{..} = _worker_status == Idle && wc == _worker_config
freeWorker coord worker = liftIO $ atomically $ setWorkerStatus coord worker Idle
data WorkerPool = WorkerPool
{ _len_waitlist :: Int
, _cur_workers :: M.HashMap ProcessId Worker }
addWorker :: ProcessId -> Worker -> WorkerPool -> WorkerPool
addWorker pid worker (WorkerPool n p) = WorkerPool n $ M.insert pid worker p
{-# INLINE addWorker #-}
removeWorker :: ProcessId -> WorkerPool -> WorkerPool
removeWorker pid (WorkerPool n p) = WorkerPool n $ M.delete pid p
{-# INLINE removeWorker #-}
setStatus :: ProcessId -> WorkerStatus -> WorkerPool -> WorkerPool
setStatus pid status (WorkerPool n p) = WorkerPool n $
M.adjust (\x -> x {_worker_status = status}) pid p
{-# INLINE setStatus #-}
setWorkerStatus :: Drmaa -> ProcessId -> WorkerStatus -> STM ()
setWorkerStatus Drmaa{..} host status = (setStatus host status <$> takeTMVar _worker_pool) >>=
putTMVar _worker_pool
{-# INLINE setWorkerStatus #-}
spawnWorker :: DrmaaConfig -> Maybe Resource -> Process Worker
spawnWorker config wc = do
procName <- liftIO $ replicateM 16 $ randomRIO ('a', 'z')
getSelfPid >>= register procName
let attr = D.defaultJobAttributes
{ D._env = [("master_id", procName)]
, D._native_specification = Just paras }
liftIO $ D.runJob exe args attr >>= \case
Left err -> error err
Right _ -> return ()
pid <- expect
return $ Worker pid Idle wc
where
(exe, args) = _cmd config
cpu = fromMaybe [] $ fmap (return . printf (_cpu_format config)) $
wc >>= _num_cpu
mem = fromMaybe [] $ fmap (return . printf (_memory_format config)) $
wc >>= _total_memory
q = fromMaybe [] $ fmap (return . printf (_queue_format config)) $
wc >>= _submit_queue
paras = unwords $ maybe [] return (_drmaa_parameters config) ++ cpu ++ mem ++ q
{-# INLINE spawnWorker #-}