myriad/src/Myriad/Docker.hs

206 lines
8.0 KiB
Haskell
Raw Normal View History

2019-07-09 11:19:41 +02:00
module Myriad.Docker
( EvalResult(..)
, buildImage
, buildAllImages
, startCleanup
, setupContainer
, killContainer
2019-07-12 10:01:18 +02:00
, killContainers
2019-07-09 11:19:41 +02:00
, evalCode
) where
import Control.Monad.Reader
import qualified Data.Map.Strict as M
2020-06-17 04:37:38 +02:00
import Data.Snowflake
import Data.String.Conversions
2019-07-09 11:19:41 +02:00
import Control.Concurrent.Async.Lifted
import Control.Concurrent.Lifted (fork, threadDelay)
import Control.Concurrent.MVar.Lifted
2019-07-09 11:19:41 +02:00
import Control.Concurrent.QSem.Lifted
import Control.Exception.Lifted
import System.FilePath ((</>))
2019-07-09 11:19:41 +02:00
import System.Process.Typed
2020-06-17 04:29:12 +02:00
import Myriad.Config
2020-06-17 04:37:38 +02:00
import Myriad.Core
2019-07-09 11:19:41 +02:00
2020-06-17 04:08:26 +02:00
type Myriad = MyriadT IO
2020-06-17 04:29:12 +02:00
buildImage :: Language -> Myriad ()
buildImage lang@Language { name, concurrent } = do
Env { config = Config { prepareContainers }, languagesDir } <- ask
2019-07-18 13:40:29 +02:00
logInfo ["Building image ", cs $ imageName lang]
exec_ ["docker build -t ", imageName lang, " ", cs languagesDir </> cs name]
2019-07-12 10:01:18 +02:00
setupQSems
2019-07-18 13:40:29 +02:00
logInfo ["Built image ", cs $ imageName lang]
2019-07-09 11:19:41 +02:00
when_ prepareContainers $ setupContainer lang
2019-07-12 10:01:18 +02:00
where
2020-06-17 04:08:26 +02:00
setupQSems :: Myriad ()
2019-07-12 10:01:18 +02:00
setupQSems = do
2020-06-17 04:29:12 +02:00
Env { containerSems, evalSems } <- ask
2019-07-12 10:01:18 +02:00
csem <- newQSem 1 -- We only want one container to be set up at a time
2020-06-17 04:29:12 +02:00
esem <- newQSem $ fromIntegral concurrent
2019-07-12 10:01:18 +02:00
mapMVar containerSems $ M.insert name csem
mapMVar evalSems $ M.insert name esem
2019-07-09 11:19:41 +02:00
2020-06-17 04:08:26 +02:00
buildAllImages :: Myriad ()
2019-07-09 11:19:41 +02:00
buildAllImages = do
2020-06-17 04:29:12 +02:00
Config { languages, buildConcurrently } <- asks config
2019-07-09 11:19:41 +02:00
if buildConcurrently
then forConcurrently_ languages buildImage
else forM_ languages buildImage
2020-06-17 04:08:26 +02:00
startCleanup :: Myriad ()
2019-07-09 11:19:41 +02:00
startCleanup = do
2020-06-17 04:29:12 +02:00
Config { cleanupInterval } <- asks config
2019-07-09 11:19:41 +02:00
when_ (cleanupInterval > 0) do
let t = fromIntegral cleanupInterval * 60000000
fork $ timer t
where
2020-06-17 04:08:26 +02:00
timer :: Int -> Myriad ()
2019-07-09 11:19:41 +02:00
timer t = forever do
threadDelay t
2019-07-12 10:01:18 +02:00
n <- killContainers
2019-07-18 13:40:29 +02:00
logInfo ["Cleaned up ", cs $ show n, " containers"]
2019-07-09 11:19:41 +02:00
timer t
2020-06-17 04:29:12 +02:00
setupContainer :: Language -> Myriad ContainerName
setupContainer lang@Language { name, memory, cpus } = do
2019-07-12 10:01:18 +02:00
cnts <- asks containers >>= readMVar
2019-07-09 11:19:41 +02:00
case cnts M.!? name of
2019-07-12 10:01:18 +02:00
Nothing -> setup
Just cnt -> pure cnt
where
2020-06-17 04:08:26 +02:00
setup :: Myriad ContainerName
2019-07-12 10:01:18 +02:00
setup = do
2020-06-17 04:29:12 +02:00
ref <- asks containers
2019-07-09 11:19:41 +02:00
cnt <- newContainerName lang
2019-07-12 10:01:18 +02:00
exec_
[ "docker run --rm --name="
2019-07-18 13:40:29 +02:00
, cs cnt
2019-07-12 10:01:18 +02:00
-- User 1000 will be for setting up the environment
, " -u1000:1000 -w/tmp/ -dt --net=none --cpus="
2020-06-17 04:29:12 +02:00
, show cpus
2019-07-12 10:01:18 +02:00
, " -m="
2020-06-17 04:29:12 +02:00
, cs memory
2019-07-12 10:01:18 +02:00
, " --memory-swap="
2020-06-17 04:29:12 +02:00
, cs memory
2019-07-12 10:01:18 +02:00
, " "
, imageName lang
, " /bin/sh"
]
2019-07-09 11:19:41 +02:00
-- The `eval` directory is where all the eval work is done
-- 711 so that users can't traverse into other people's code
2019-07-12 10:01:18 +02:00
exec_ ["docker exec ", cnt, " mkdir eval"]
exec_ ["docker exec ", cnt, " chmod 711 eval"]
mapMVar ref $ M.insert name cnt
2019-07-18 13:40:29 +02:00
logInfo ["Started container ", cs cnt]
2019-07-09 11:19:41 +02:00
pure cnt
2020-06-17 04:29:12 +02:00
killContainer :: LanguageName -> Myriad Bool
2019-07-09 11:19:41 +02:00
killContainer lang = do
containers <- asks containers >>= readMVar
2019-07-09 11:19:41 +02:00
case containers M.!? lang of
Nothing -> pure False
Just cnt -> do
2019-07-12 10:01:18 +02:00
res <- kill cnt
2019-07-09 11:19:41 +02:00
case res of
2019-07-12 10:01:18 +02:00
Nothing -> pure True
Just err -> do
2019-07-18 13:40:29 +02:00
logError ["An exception occured when killing ", cs cnt, ":\n", cs $ show err]
2019-07-09 11:19:41 +02:00
pure False
2019-07-12 10:01:18 +02:00
where
2020-06-17 04:08:26 +02:00
kill :: ContainerName -> Myriad (Maybe SomeException)
2019-07-12 10:01:18 +02:00
kill cnt = do
ref <- asks containers
mapMVar ref $ M.delete lang
res <- try $ exec_ ["docker kill ", cnt]
case res of
Left err -> pure $ Just err
Right _ -> do
2019-07-18 13:40:29 +02:00
logInfo ["Killed container ", cs cnt]
2019-07-12 10:01:18 +02:00
pure Nothing
2019-07-09 11:19:41 +02:00
2020-06-17 04:08:26 +02:00
killContainers :: Myriad [ContainerName]
2019-07-12 10:01:18 +02:00
killContainers = do
containers <- asks containers >>= readMVar
2019-07-12 10:01:18 +02:00
xs <- forConcurrently (M.toList containers) \(k, v) -> (v,) <$> killContainer k
2019-07-11 08:20:36 +02:00
pure . map fst $ filter snd xs
2019-07-09 11:19:41 +02:00
2020-06-17 04:29:12 +02:00
evalCode :: Language -> Int -> String -> Myriad EvalResult
evalCode lang@Language { name, timeout, retries } numRetries code = withContainer \cnt -> do
2019-07-12 10:01:18 +02:00
doneRef <- newMVar False -- For keeping track of if the evaluation is done, i.e. succeeded or timed out.
void . fork $ timer doneRef -- `race` could not have been used here since some evals can't be cancelled.
2020-06-17 04:29:12 +02:00
snowflakeGen <- asks snowflakeGen
2019-07-12 10:01:18 +02:00
snowflake <- liftIO $ nextSnowflake snowflakeGen
res <- try $ eval cnt snowflake
case res of
Left (SomeException err) -> do
void $ killContainer name
done <- readMVar doneRef
if done
-- If we find the eval is done from an exception, then it was timed out.
then do
2019-07-18 13:40:29 +02:00
logError ["Code timed out in container ", cs cnt, ", evaluation ", cs $ show snowflake]
2019-07-12 10:01:18 +02:00
pure EvalTimedOut
-- Otherwise, the container was killed from another eval, so we should retry.
else do
writeMVar doneRef True
2020-06-17 04:29:12 +02:00
if numRetries < fromIntegral retries
2019-07-12 10:01:18 +02:00
then do
2019-07-18 13:40:29 +02:00
logError ["An exception occured in ", cs cnt, ", evaluation ", cs $ show snowflake, ", retrying:\n", cs $ show err]
2019-07-12 10:01:18 +02:00
evalCode lang (numRetries + 1) code
else do
2019-07-18 13:40:29 +02:00
logError ["An exception occured in ", cs cnt, ", evaluation ", cs $ show snowflake, ":\n", cs $ show err]
2019-07-12 10:01:18 +02:00
pure EvalErrored
Right x -> do
writeMVar doneRef True
pure x
2019-07-09 11:19:41 +02:00
where
2020-06-17 04:08:26 +02:00
withContainer :: (ContainerName -> Myriad a) -> Myriad a
2019-07-12 10:01:18 +02:00
withContainer f = do
Env { containerSems, evalSems } <- ask
csem <- (M.! name) <$> readMVar containerSems
esem <- (M.! name) <$> readMVar evalSems
bracket_ (waitQSem esem) (signalQSem esem) do
cnt <- bracket_ (waitQSem csem) (signalQSem csem) $ setupContainer lang
f cnt
2020-06-17 04:08:26 +02:00
timer :: MVar Bool -> Myriad ()
2019-07-09 11:19:41 +02:00
timer doneRef = do
2020-06-17 04:29:12 +02:00
threadDelay $ fromIntegral timeout * 1000000
done <- readMVar doneRef
2019-07-09 11:19:41 +02:00
unless_ done do
2019-07-12 10:01:18 +02:00
writeMVar doneRef True
killContainer name
2019-07-09 11:19:41 +02:00
2020-06-17 04:08:26 +02:00
eval :: ContainerName -> Snowflake -> Myriad EvalResult
2019-07-12 10:01:18 +02:00
eval cnt snowflake = do
2019-07-18 13:40:29 +02:00
logInfo ["Running code in container ", cs cnt, ", evaluation ", cs $ show snowflake, ":\n", cs code]
exec_ ["docker exec ", cs cnt, " mkdir eval/", show snowflake]
exec_ ["docker exec ", cs cnt, " chmod 777 eval/", show snowflake]
2019-07-09 11:19:41 +02:00
-- User 1001 will be used for the actual execution so that they can't access `eval` itself
let cmd = concat ["docker exec -i -u1001:1001 -w/tmp/eval/", show snowflake, " ", cnt, " /bin/sh /var/run/run.sh | head -c 4K"]
pr = setStdin (byteStringInput $ cs code) $ shell cmd
output <- readProcessInterleaved_ pr
2019-07-12 10:01:18 +02:00
exec_ ["docker exec ", cnt, " rm -rf eval/", show snowflake]
2019-07-18 13:40:29 +02:00
logInfo ["Ran code in container ", cs cnt, ", evaluation ", cs $ show snowflake]
2019-07-09 11:19:41 +02:00
pure $ EvalOk output
2019-07-18 13:40:29 +02:00
2020-06-17 04:29:12 +02:00
newContainerName :: Language -> Myriad ContainerName
newContainerName Language { name } = do
2019-07-18 13:40:29 +02:00
snowflakeGen <- asks snowflakeGen
snowflake <- liftIO $ nextSnowflake snowflakeGen
pure $ "comp_iler-" <> cs name <> "-" <> show snowflake
2020-06-17 04:29:12 +02:00
imageName :: Language -> ImageName
imageName Language { name } = "1computer1/comp_iler:" <> cs name
2019-07-18 13:40:29 +02:00
when_ :: Applicative f => Bool -> f a -> f ()
when_ p = when p . void
unless_ :: Applicative f => Bool -> f a -> f ()
unless_ p = unless p . void