✨ A6
This commit is contained in:
30
a6/a6-handout/a6.cabal
Normal file
30
a6/a6-handout/a6.cabal
Normal file
@ -0,0 +1,30 @@
|
||||
cabal-version: 3.0
|
||||
name: a6
|
||||
version: 1.0.0.0
|
||||
build-type: Simple
|
||||
|
||||
common common
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
|
||||
library
|
||||
import: common
|
||||
hs-source-dirs: src
|
||||
build-depends:
|
||||
base
|
||||
, tasty
|
||||
, tasty-hunit
|
||||
, clock
|
||||
exposed-modules:
|
||||
SPC
|
||||
SPC_Tests
|
||||
GenServer
|
||||
|
||||
test-suite a6-tests
|
||||
import: common
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: runtests.hs
|
||||
build-depends:
|
||||
base
|
||||
, tasty
|
||||
, a6
|
5
a6/a6-handout/runtests.hs
Normal file
5
a6/a6-handout/runtests.hs
Normal file
@ -0,0 +1,5 @@
|
||||
import qualified SPC_Tests
|
||||
import Test.Tasty (defaultMain)
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain SPC_Tests.tests
|
45
a6/a6-handout/src/GenServer.hs
Normal file
45
a6/a6-handout/src/GenServer.hs
Normal file
@ -0,0 +1,45 @@
|
||||
module GenServer
|
||||
( Chan,
|
||||
Server,
|
||||
receive,
|
||||
send,
|
||||
sendTo,
|
||||
spawn,
|
||||
ReplyChan,
|
||||
requestReply,
|
||||
reply,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (Chan)
|
||||
import qualified Control.Concurrent as CC
|
||||
|
||||
data Server msg = Server CC.ThreadId (Chan msg)
|
||||
|
||||
data ReplyChan a = ReplyChan (Chan a)
|
||||
|
||||
send :: Chan a -> a -> IO ()
|
||||
send chan msg =
|
||||
CC.writeChan chan msg
|
||||
|
||||
sendTo :: Server a -> a -> IO ()
|
||||
sendTo (Server _tid input) msg =
|
||||
send input msg
|
||||
|
||||
receive :: Chan a -> IO a
|
||||
receive = CC.readChan
|
||||
|
||||
spawn :: (Chan a -> IO ()) -> IO (Server a)
|
||||
spawn server = do
|
||||
input <- CC.newChan
|
||||
tid <- CC.forkIO $ server input
|
||||
pure $ Server tid input
|
||||
|
||||
requestReply :: Server a -> (ReplyChan b -> a) -> IO b
|
||||
requestReply serv con = do
|
||||
reply_chan <- CC.newChan
|
||||
sendTo serv $ con $ ReplyChan reply_chan
|
||||
receive reply_chan
|
||||
|
||||
reply :: ReplyChan a -> a -> IO ()
|
||||
reply (ReplyChan chan) x = send chan x
|
255
a6/a6-handout/src/SPC.hs
Normal file
255
a6/a6-handout/src/SPC.hs
Normal file
@ -0,0 +1,255 @@
|
||||
module SPC
|
||||
( -- * SPC startup
|
||||
SPC,
|
||||
startSPC,
|
||||
|
||||
-- * Job functions
|
||||
Job (..),
|
||||
JobId,
|
||||
JobStatus (..),
|
||||
JobDoneReason (..),
|
||||
jobAdd,
|
||||
jobStatus,
|
||||
jobWait,
|
||||
jobCancel,
|
||||
|
||||
-- * Worker functions
|
||||
WorkerName,
|
||||
workerAdd,
|
||||
workerStop,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent
|
||||
( forkIO,
|
||||
killThread,
|
||||
threadDelay,
|
||||
)
|
||||
import Control.Monad (ap, forever, liftM, void)
|
||||
import GenServer
|
||||
import System.Clock.Seconds (Clock (Monotonic), Seconds, getTime)
|
||||
|
||||
-- First some general utility functions.
|
||||
|
||||
-- | Retrieve Unix time using a monotonic clock. You cannot use this
|
||||
-- to measure the actual world time, but you can use it to measure
|
||||
-- elapsed time.
|
||||
getSeconds :: IO Seconds
|
||||
getSeconds = getTime Monotonic
|
||||
|
||||
-- | Remove mapping from association list.
|
||||
removeAssoc :: (Eq k) => k -> [(k, v)] -> [(k, v)]
|
||||
removeAssoc needle ((k, v) : kvs) =
|
||||
if k == needle
|
||||
then kvs
|
||||
else (k, v) : removeAssoc needle kvs
|
||||
removeAssoc _ [] = []
|
||||
|
||||
-- Then the definition of the glorious SPC.
|
||||
|
||||
-- | A job that is to be enqueued in the glorious SPC.
|
||||
data Job = Job
|
||||
{ -- | The IO action that comprises the actual action of the job.
|
||||
jobAction :: IO (),
|
||||
-- | The maximum allowed runtime of the job, counting from when
|
||||
-- the job begins executing (not when it is enqueued).
|
||||
jobMaxSeconds :: Int
|
||||
}
|
||||
|
||||
-- | A unique identifier of a job that has been enqueued.
|
||||
newtype JobId = JobId Int
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
-- | How a job finished.
|
||||
data JobDoneReason
|
||||
= -- | Normal termination.
|
||||
Done
|
||||
| -- | The job was killed because it ran for too long.
|
||||
DoneTimeout
|
||||
| -- | The job was explicitly cancelled, or the worker
|
||||
-- it was running on was stopped.
|
||||
DoneCancelled
|
||||
| -- | The job crashed due to an exception.
|
||||
DoneCrashed
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
-- | The status of a job.
|
||||
data JobStatus
|
||||
= -- | The job is done and this is why.
|
||||
JobDone JobDoneReason
|
||||
| -- | The job is still running.
|
||||
JobRunning
|
||||
| -- | The job is enqueued, but is waiting for an idle worker.
|
||||
JobPending
|
||||
| -- | A job with this ID is not known to this SPC instance.
|
||||
JobUnknown
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
-- | A worker decides its own human-readable name. This is useful for
|
||||
-- debugging.
|
||||
type WorkerName = String
|
||||
|
||||
-- | Messages sent to workers. These are sent both by SPC and by
|
||||
-- processes spawned by the workes.
|
||||
data WorkerMsg -- TODO: add messages.
|
||||
|
||||
-- Messages sent to SPC.
|
||||
data SPCMsg
|
||||
= -- | Add the job, and reply with the job ID.
|
||||
MsgJobAdd Job (ReplyChan JobId)
|
||||
| -- | Cancel the given job.
|
||||
MsgJobCancel JobId
|
||||
| -- | Immediately reply the status of the job.
|
||||
MsgJobStatus JobId (ReplyChan JobStatus)
|
||||
| -- | Reply when the job is done.
|
||||
MsgJobWait JobId (ReplyChan JobDoneReason)
|
||||
| -- | Some time has passed.
|
||||
MsgTick
|
||||
|
||||
-- | A handle to the SPC instance.
|
||||
data SPC = SPC (Server SPCMsg)
|
||||
|
||||
-- | A handle to a worker.
|
||||
data Worker = Worker (Server WorkerMsg)
|
||||
|
||||
-- | The central state. Must be protected from the bourgeoisie.
|
||||
data SPCState = SPCState
|
||||
{ spcJobsPending :: [(JobId, Job)],
|
||||
spcJobsRunning :: [(JobId, Job)],
|
||||
spcJobsDone :: [(JobId, JobDoneReason)],
|
||||
spcJobCounter :: JobId
|
||||
-- TODO: you will need to add more fields.
|
||||
}
|
||||
|
||||
-- | The monad in which the main SPC thread runs. This is a state
|
||||
-- monad with support for IO.
|
||||
newtype SPCM a = SPCM (SPCState -> IO (a, SPCState))
|
||||
|
||||
instance Functor SPCM where
|
||||
fmap = liftM
|
||||
|
||||
instance Applicative SPCM where
|
||||
pure x = SPCM $ \state -> pure (x, state)
|
||||
(<*>) = ap
|
||||
|
||||
instance Monad SPCM where
|
||||
SPCM m >>= f = SPCM $ \state -> do
|
||||
(x, state') <- m state
|
||||
let SPCM f' = f x
|
||||
f' state'
|
||||
|
||||
-- | Retrieve the state.
|
||||
get :: SPCM SPCState
|
||||
get = SPCM $ \state -> pure (state, state)
|
||||
|
||||
-- | Overwrite the state.
|
||||
put :: SPCState -> SPCM ()
|
||||
put state = SPCM $ \_ -> pure ((), state)
|
||||
|
||||
-- | Modify the state.
|
||||
modify :: (SPCState -> SPCState) -> SPCM ()
|
||||
modify f = do
|
||||
state <- get
|
||||
put $ f state
|
||||
|
||||
-- | Lift an 'IO' action into 'SPCM'.
|
||||
io :: IO a -> SPCM a
|
||||
io m = SPCM $ \state -> do
|
||||
x <- m
|
||||
pure (x, state)
|
||||
|
||||
-- | Run the SPCM monad.
|
||||
runSPCM :: SPCState -> SPCM a -> IO a
|
||||
runSPCM state (SPCM f) = fst <$> f state
|
||||
|
||||
schedule :: SPCM ()
|
||||
schedule = undefined
|
||||
|
||||
jobDone :: JobId -> JobDoneReason -> SPCM ()
|
||||
jobDone = undefined
|
||||
|
||||
workerIsIdle :: WorkerName -> Worker -> SPCM ()
|
||||
workerIsIdle = undefined
|
||||
|
||||
workerIsGone :: WorkerName -> SPCM ()
|
||||
workerIsGone = undefined
|
||||
|
||||
checkTimeouts :: SPCM ()
|
||||
checkTimeouts = pure () -- change in Task 4
|
||||
|
||||
workerExists :: WorkerName -> SPCM Bool
|
||||
workerExists = undefined
|
||||
|
||||
handleMsg :: Chan SPCMsg -> SPCM ()
|
||||
handleMsg c = do
|
||||
checkTimeouts
|
||||
schedule
|
||||
msg <- io $ receive c
|
||||
case msg of
|
||||
MsgJobAdd job rsvp -> do
|
||||
state <- get
|
||||
let JobId jobid = spcJobCounter state
|
||||
put $
|
||||
state
|
||||
{ spcJobsPending =
|
||||
(spcJobCounter state, job) : spcJobsPending state,
|
||||
spcJobCounter = JobId $ succ jobid
|
||||
}
|
||||
io $ reply rsvp $ JobId jobid
|
||||
MsgJobStatus jobid rsvp -> do
|
||||
state <- get
|
||||
io $ reply rsvp $ case ( lookup jobid $ spcJobsPending state,
|
||||
lookup jobid $ spcJobsRunning state,
|
||||
lookup jobid $ spcJobsDone state
|
||||
) of
|
||||
(Just _, _, _) -> JobPending
|
||||
(_, Just _, _) -> JobRunning
|
||||
(_, _, Just r) -> JobDone r
|
||||
_ -> JobUnknown
|
||||
|
||||
startSPC :: IO SPC
|
||||
startSPC = do
|
||||
let initial_state =
|
||||
SPCState
|
||||
{ spcJobCounter = JobId 0,
|
||||
spcJobsPending = [],
|
||||
spcJobsRunning = [],
|
||||
spcJobsDone = []
|
||||
}
|
||||
c <- spawn $ \c -> runSPCM initial_state $ forever $ handleMsg c
|
||||
void $ spawn $ timer c
|
||||
pure $ SPC c
|
||||
where
|
||||
timer c _ = forever $ do
|
||||
threadDelay 1000000 -- 1 second
|
||||
sendTo c MsgTick
|
||||
|
||||
-- | Add a job for scheduling.
|
||||
jobAdd :: SPC -> Job -> IO JobId
|
||||
jobAdd (SPC c) job =
|
||||
requestReply c $ MsgJobAdd job
|
||||
|
||||
-- | Asynchronously query the job status.
|
||||
jobStatus :: SPC -> JobId -> IO JobStatus
|
||||
jobStatus (SPC c) jobid =
|
||||
requestReply c $ MsgJobStatus jobid
|
||||
|
||||
-- | Synchronously block until job is done and return the reason.
|
||||
jobWait :: SPC -> JobId -> IO JobDoneReason
|
||||
jobWait (SPC c) jobid =
|
||||
requestReply c $ MsgJobWait jobid
|
||||
|
||||
-- | Asynchronously cancel a job.
|
||||
jobCancel :: SPC -> JobId -> IO ()
|
||||
jobCancel (SPC c) jobid =
|
||||
sendTo c $ MsgJobCancel jobid
|
||||
|
||||
-- | Add a new worker with this name. Fails with 'Left' if a worker
|
||||
-- with that name already exists.
|
||||
workerAdd :: SPC -> WorkerName -> IO (Either String Worker)
|
||||
workerAdd = undefined
|
||||
|
||||
-- | Shut down a running worker. No effect if the worker is already
|
||||
-- terminated.
|
||||
workerStop :: Worker -> IO ()
|
||||
workerStop = undefined
|
15
a6/a6-handout/src/SPC_Tests.hs
Normal file
15
a6/a6-handout/src/SPC_Tests.hs
Normal file
@ -0,0 +1,15 @@
|
||||
module SPC_Tests (tests) where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad (forM, forM_, replicateM)
|
||||
import Data.IORef
|
||||
import SPC
|
||||
import Test.Tasty (TestTree, localOption, mkTimeout, testGroup)
|
||||
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
|
||||
|
||||
tests :: TestTree
|
||||
tests =
|
||||
localOption (mkTimeout 3000000) $
|
||||
testGroup
|
||||
"SPC (core)"
|
||||
[]
|
Reference in New Issue
Block a user