Skip to content

Commit 8925211

Browse files
committed
Introduce concurrent run strategy for launching containers
1 parent 5a249cf commit 8925211

8 files changed

Lines changed: 277 additions & 120 deletions

File tree

src/TestContainers.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module TestContainers
3535
M.setExpose,
3636
M.setWaitingFor,
3737
M.withFollowLogs,
38+
M.withDependencies,
3839

3940
-- * Logs
4041
M.LogConsumer,

src/TestContainers/Config.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
module TestContainers.Config
2-
( Config (..),
2+
( RunStrategy (..),
3+
Config (..),
34
defaultConfig,
45
defaultDockerConfig,
56
determineConfig,
67
)
78
where
89

910
import {-# SOURCE #-} TestContainers.Docker (createRyukReaper)
10-
import TestContainers.Monad (Config (..))
11+
import TestContainers.Monad (Config (..), RunStrategy (..))
1112

1213
-- | Default configuration.
1314
--
@@ -17,7 +18,8 @@ defaultConfig =
1718
Config
1819
{ configDefaultWaitTimeout = Just 60,
1920
configTracer = mempty,
20-
configCreateReaper = createRyukReaper
21+
configCreateReaper = createRyukReaper,
22+
configRunStrategy = SequentialRunStrategy
2123
}
2224

2325
-- | Default configuration.

src/TestContainers/Docker.hs

Lines changed: 124 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{-# LANGUAGE NamedFieldPuns #-}
99
{-# LANGUAGE OverloadedStrings #-}
1010
{-# LANGUAGE RankNTypes #-}
11+
{-# LANGUAGE RecordWildCards #-}
1112
{-# LANGUAGE ScopedTypeVariables #-}
1213
{-# LANGUAGE StandaloneDeriving #-}
1314
{-# LANGUAGE TypeFamilies #-}
@@ -94,6 +95,7 @@ module TestContainers.Docker
9495
setLink,
9596
setExpose,
9697
setWaitingFor,
98+
withDependencies,
9799
run,
98100

99101
-- * Following logs
@@ -158,7 +160,8 @@ module TestContainers.Docker
158160
where
159161

160162
import Control.Concurrent (threadDelay)
161-
import Control.Exception (IOException, throw)
163+
import qualified Control.Concurrent.Async
164+
import Control.Exception (IOException, evaluate, throw)
162165
import Control.Monad (forM_, replicateM, unless)
163166
import Control.Monad.Catch
164167
( Exception,
@@ -169,11 +172,12 @@ import Control.Monad.Catch
169172
try,
170173
)
171174
import Control.Monad.IO.Class (MonadIO (liftIO))
172-
import Control.Monad.IO.Unlift (MonadUnliftIO (withRunInIO))
175+
import Control.Monad.IO.Unlift (MonadUnliftIO (withRunInIO), askRunInIO)
173176
import Control.Monad.Reader (MonadReader (..))
174177
import Control.Monad.Trans.Resource
175178
( ReleaseKey,
176179
ResIO,
180+
allocate,
177181
register,
178182
runResourceT,
179183
)
@@ -209,12 +213,13 @@ import Optics.Optic ((%), (<&>))
209213
import System.Directory (doesFileExist)
210214
import System.Environment (lookupEnv)
211215
import System.IO (Handle, hClose)
212-
import System.IO.Unsafe (unsafePerformIO)
216+
import System.IO.Unsafe (unsafeInterleaveIO, unsafePerformIO)
213217
import qualified System.Process as Process
214218
import qualified System.Random as Random
215219
import System.Timeout (timeout)
216220
import TestContainers.Config
217221
( Config (..),
222+
RunStrategy (..),
218223
defaultDockerConfig,
219224
determineConfig,
220225
)
@@ -263,6 +268,7 @@ import TestContainers.Docker.State
263268
import TestContainers.Monad
264269
( MonadDocker,
265270
TestContainer,
271+
defer,
266272
)
267273
import TestContainers.Trace (Trace (..), Tracer, newTracer, withTrace)
268274
import Prelude hiding (error, id)
@@ -288,7 +294,8 @@ data ContainerRequest = ContainerRequest
288294
labels :: [(Text, Text)],
289295
noReaper :: Bool,
290296
followLogs :: Maybe LogConsumer,
291-
workDirectory :: Maybe Text
297+
workDirectory :: Maybe Text,
298+
dependencies :: [Container]
292299
}
293300

294301
instance WithoutReaper ContainerRequest where
@@ -324,7 +331,8 @@ containerRequest image =
324331
labels = mempty,
325332
noReaper = False,
326333
followLogs = Nothing,
327-
workDirectory = Nothing
334+
workDirectory = Nothing,
335+
dependencies = []
328336
}
329337

330338
-- | Set the name of a Docker container. This is equivalent to invoking @docker run@
@@ -453,6 +461,13 @@ withFollowLogs :: LogConsumer -> ContainerRequest -> ContainerRequest
453461
withFollowLogs logConsumer request =
454462
request {followLogs = Just logConsumer}
455463

464+
-- |
465+
--
466+
-- @since x.x.x
467+
withDependencies :: [Container] -> ContainerRequest -> ContainerRequest
468+
withDependencies dependencies request =
469+
request {dependencies}
470+
456471
-- | Defintion of a 'Port'. Allows for specifying ports using various protocols. Due to the
457472
-- 'Num' and 'IsString' instance allows for convenient Haskell literals.
458473
--
@@ -556,7 +571,8 @@ run request = do
556571
labels,
557572
noReaper,
558573
followLogs,
559-
workDirectory
574+
workDirectory,
575+
dependencies
560576
} = request
561577

562578
config@Config {configTracer, configCreateReaper} <-
@@ -568,8 +584,6 @@ run request = do
568584
pure []
569585
else reaperLabels <$> configCreateReaper
570586

571-
image@Image {tag} <- runToImage toImage
572-
573587
name <-
574588
case naming of
575589
RandomName -> return Nothing
@@ -578,59 +592,110 @@ run request = do
578592
Just . (prefix <>) . ("-" <>) . pack
579593
<$> replicateM 6 (Random.randomRIO ('a', 'z'))
580594

581-
let dockerRun :: [Text]
582-
dockerRun =
583-
concat $
584-
[["run"]]
585-
++ [["--detach"]]
586-
++ [["--name", containerName] | Just containerName <- [name]]
587-
++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]
588-
++ [["--env", variable <> "=" <> value] | (variable, value) <- env]
589-
++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]
590-
++ [["--network", networkName] | Just (Right networkName) <- [network]]
591-
++ [["--network", networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
592-
++ [["--network-alias", alias] | Just alias <- [networkAlias]]
593-
++ [["--link", container] | container <- links]
594-
++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]
595-
++ [["--rm"] | rmOnExit]
596-
++ [["--workdir", workdir] | Just workdir <- [workDirectory]]
597-
++ [["--memory", value] | Just value <- [memory]]
598-
++ [["--cpus", value] | Just value <- [cpus]]
599-
++ [[tag]]
600-
++ [command | Just command <- [cmd]]
601-
602-
stdout <- docker configTracer dockerRun
603-
604-
let id :: ContainerId
605-
!id =
606-
-- N.B. Force to not leak STDOUT String
607-
strip (pack stdout)
608-
609-
-- Careful, this is really meant to be lazy
610-
~inspectOutput =
611-
unsafePerformIO $
612-
internalInspect configTracer id
595+
(actuallyRunDocker, waitOnContainer) <- applyRunStrategy $ do
596+
image@Image {tag} <- runToImage toImage
613597

614-
-- We don't issue 'ReleaseKeys' for cleanup anymore. Ryuk takes care of cleanup
615-
-- for us once the session has been closed.
616-
releaseKey <- register (pure ())
598+
liftIO $
599+
forM_ dependencies $
600+
\Container {wait} -> wait
601+
602+
let dockerRun :: [Text]
603+
dockerRun =
604+
concat $
605+
[["run"]]
606+
++ [["--detach"]]
607+
++ [["--name", containerName] | Just containerName <- [name]]
608+
++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]
609+
++ [["--env", variable <> "=" <> value] | (variable, value) <- env]
610+
++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]
611+
++ [["--network", networkName] | Just (Right networkName) <- [network]]
612+
++ [["--network", networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]
613+
++ [["--network-alias", alias] | Just alias <- [networkAlias]]
614+
++ [["--link", container] | container <- links]
615+
++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]
616+
++ [["--rm"] | rmOnExit]
617+
++ [["--workdir", workdir] | Just workdir <- [workDirectory]]
618+
++ [["--memory", value] | Just value <- [memory]]
619+
++ [["--cpus", value] | Just value <- [cpus]]
620+
++ [[tag]]
621+
++ [command | Just command <- [cmd]]
622+
623+
stdout <- docker configTracer dockerRun
624+
625+
let id :: ContainerId
626+
!id =
627+
-- N.B. Force to not leak STDOUT String
628+
strip (pack stdout)
629+
630+
-- Careful, this is really meant to be lazy
631+
~inspectOutput =
632+
unsafePerformIO $
633+
internalInspect configTracer id
634+
635+
-- We don't issue 'ReleaseKeys' for cleanup anymore. Ryuk takes care of cleanup
636+
-- for us once the session has been closed.
637+
releaseKey <- register (pure ())
638+
639+
forM_ followLogs $
640+
dockerFollowLogs configTracer id
641+
642+
let container =
643+
Container
644+
{ id,
645+
releaseKey,
646+
image,
647+
inspectOutput,
648+
config,
649+
wait = pure ()
650+
}
651+
652+
-- Last but not least, execute the WaitUntilReady checks
653+
waitUntilReady container readiness
654+
655+
pure container
656+
657+
-- We want to be very gentle and not force the container if possible so that we can
658+
-- install the wait action.
659+
~container <-
660+
actuallyRunDocker
661+
662+
pure $
663+
let ~Container {..} = container
664+
in Container
665+
{ wait = waitOnContainer,
666+
..
667+
}
617668

618-
forM_ followLogs $
619-
dockerFollowLogs configTracer id
669+
applyRunStrategy ::
670+
TestContainer a ->
671+
TestContainer (TestContainer a, IO ())
672+
applyRunStrategy action = do
673+
Config {configTracer, configRunStrategy} <-
674+
ask
675+
case configRunStrategy of
676+
SequentialRunStrategy ->
677+
pure (action, pure ())
678+
ConcurrentRunStrategy _limit -> do
679+
runInIO <-
680+
askRunInIO
620681

621-
let container =
622-
Container
623-
{ id,
624-
releaseKey,
625-
image,
626-
inspectOutput,
627-
config
628-
}
682+
(_releaseKey, handle) <-
683+
allocate
684+
(Control.Concurrent.Async.async (runInIO action))
685+
Control.Concurrent.Async.cancel
686+
687+
let returnAction =
688+
liftIO $ unsafeInterleaveIO $ do
689+
Control.Concurrent.Async.wait handle
690+
691+
waitAction = do
692+
withTrace configTracer TraceWaitOnDependency
693+
_ <- Control.Concurrent.Async.wait handle
694+
pure ()
629695

630-
-- Last but not least, execute the WaitUntilReady checks
631-
waitUntilReady container readiness
696+
defer waitAction
632697

633-
pure container
698+
pure (returnAction, waitAction)
634699

635700
-- | Sets up a Ryuk 'Reaper'.
636701
--
@@ -1109,7 +1174,9 @@ data Container = Container
11091174
-- | Configuration used to create and run this container.
11101175
config :: Config,
11111176
-- | Memoized output of `docker inspect`. This is being calculated lazily.
1112-
inspectOutput :: InspectOutput
1177+
inspectOutput :: InspectOutput,
1178+
-- | Wait on the container as a dependency.
1179+
wait :: IO ()
11131180
}
11141181

11151182
-- | Returns the id of the container.

0 commit comments

Comments
 (0)