theatre-dev 0.0.1 → 0.1
raw patch · 13 files changed
+278/−131 lines, 13 filesdep +QuickCheckdep +uuiddep ~asyncdep ~stm
Dependencies added: QuickCheck, uuid
Dependency ranges changed: async, stm
Files
- hspec/Main.hs +1/−1
- hspec/TheatreDev/StmBasedSpec.hs +118/−7
- hspec/TheatreDev/StmBasedSpec/IO.hs +34/−0
- hspec/TheatreDev/StmBasedSpec/Preferences.hs +9/−0
- library/TheatreDev/ExtrasFor/TBQueue.hs +2/−2
- library/TheatreDev/Perpetual.hs +1/−1
- library/TheatreDev/Prelude.hs +2/−0
- library/TheatreDev/StmBased.hs +56/−62
- library/TheatreDev/StmBased/StmStructures/Runner.hs +39/−36
- library/TheatreDev/StmBased/Tell.hs +1/−1
- library/TheatreDev/StmBased/Wait.hs +2/−16
- library/TheatreDev/Terminal/Actor.hs +2/−2
- theatre-dev.cabal +11/−3
hspec/Main.hs view
@@ -1,7 +1,7 @@ module Main (main) where import Test.Hspec-import qualified TheatreDev.StmBasedSpec+import TheatreDev.StmBasedSpec qualified import Prelude main :: IO ()
hspec/TheatreDev/StmBasedSpec.hs view
@@ -1,17 +1,25 @@-{-# OPTIONS_GHC -Wno-unused-local-binds #-}+{-# OPTIONS_GHC -Wno-unused-local-binds -Wno-unused-binds #-} module TheatreDev.StmBasedSpec (spec) where +import Control.Concurrent.Async+import Data.IntMap.Strict qualified as IntMap+import Data.IntSet qualified as IntSet import Test.Hspec-import qualified TheatreDev.StmBased as Actor+import Test.Hspec.QuickCheck+import Test.QuickCheck+import TheatreDev.StmBased (Actor)+import TheatreDev.StmBased qualified as Actor+import TheatreDev.StmBasedSpec.IO qualified as IO+import TheatreDev.StmBasedSpec.Preferences qualified as Preferences import Prelude spec :: Spec spec = do- describe "spawnStatelessBatched" do- let spawnInt step = Actor.spawnStatefulBatched @Int 0 step (const (return ()))- let spawnUnit step = Actor.spawnStatefulBatched () step (const (return ()))+ describe "spawnStatefulBatched" do+ let spawnIntUpdater step = Actor.spawnStatefulBatched @Int 0 (const (return ())) step+ let spawnUnit step = Actor.spawnStatefulBatched () (const (return ())) step it "Works in batches" do acc <- newIORef []@@ -35,12 +43,115 @@ putMVar actorLock () takeMVar emitterLock+ Actor.kill actor collectedBatches <- reverse . fmap toList <$> readIORef acc shouldBe collectedBatches [[1], [2, 3], [4]] it "Threads the state" do- pending+ let input = [0 .. 9]+ inputLength = length input + resultsVar <- newTVarIO []+ actor <- Actor.spawnStatefulBatched [] (const (return ())) $ \state msgs -> do+ let !newState = foldl' (flip (:)) state msgs+ atomically $ writeTVar resultsVar newState+ return newState+ traverse_ (Actor.tell actor) input++ results <- atomically $ do+ results <- readTVar resultsVar+ if length results < inputLength+ then retry+ else return results++ shouldBe (reverse results) input+ it "Kill and wait" do- pending+ let input = [0 .. 9]+ inputLength = length input++ resultVar <- newEmptyMVar+ actor <-+ Actor.spawnStatefulBatched+ []+ ( \state -> do+ threadDelay 1000+ putMVar resultVar state+ )+ ( \state msgs -> return $ foldl' (flip (:)) state msgs+ )+ traverse_ (Actor.tell actor) input++ Actor.kill actor+ Actor.wait actor++ result <- takeMVar resultVar+ shouldBe result $ reverse input++ describe "allOf" . modifyMaxSuccess (max 1000) $ do+ it "Passes 1" do+ let emittersNum = 2+ messagesNum = 10+ actorsNum = 3+ messages = [0 .. messagesNum - 1]+ results <- fmap (fmap sort) (IO.simulateReduction Actor.allOf actorsNum emittersNum messages)+ shouldBe results (replicate actorsNum (sort (concat (replicate emittersNum messages))))+ shouldBe (getSum (foldMap (Sum . length) results)) (actorsNum * emittersNum * messagesNum)+ prop "" $ forAll (chooseInt (0, 99)) $ \size -> forAll arbitrary $ \(messages :: [Int]) ->+ idempotentIOProperty do+ results <- sort . concat <$> IO.simulateReduction Actor.allOf Preferences.concurrency size messages+ return+ $ conjoin+ [ length results === length messages * size * Preferences.concurrency,+ results === sort (concat (replicate (size * Preferences.concurrency) messages))+ ]++ describe "oneOf" . modifyMaxSuccess (max 1000) $ do+ prop "Dispatches correctly" $ forAll (chooseInt (0, 99)) $ \size -> forAll arbitrary $ \(messages :: [Int]) ->+ idempotentIOProperty do+ results <- sort . concat <$> IO.simulateReduction Actor.oneOf Preferences.concurrency size messages+ return+ $ conjoin+ [ length results === length messages * size,+ results+ === sort (concat (replicate (size) messages))+ ]++ describe "byKeyHash" . modifyMaxSuccess (max Preferences.largePropertyMaxSuccess) $ do+ prop "Dispatches individually" $ forAll (chooseInt (0, 99)) $ \size -> forAll arbitrary $ \(messages :: [Int]) -> idempotentIOProperty $ do+ resultsVar <- newTVarIO []+ actor <-+ fmap (Actor.byKeyHash id)+ $ replicateM size+ $ Actor.spawnStatefulIndividual+ IntMap.empty+ ( \state ->+ atomically+ $ modifyTVar' resultsVar+ $ (:) state+ )+ ( \state msg ->+ return $ IntMap.alter (Just . maybe 1 succ) msg state+ )++ mapConcurrently id+ $ replicate Preferences.concurrency+ $ for_ messages+ $ Actor.tell actor++ Actor.kill actor+ Actor.wait actor++ results <- readTVarIO resultsVar++ let allKeys = results >>= IntMap.keys+ nubbedKeys = nub allKeys++ return+ $ conjoin+ [ allKeys === nubbedKeys,+ if size == 0+ then nubbedKeys === []+ else IntSet.fromList nubbedKeys === IntSet.fromList messages+ ]
+ hspec/TheatreDev/StmBasedSpec/IO.hs view
@@ -0,0 +1,34 @@+module TheatreDev.StmBasedSpec.IO where++import Control.Concurrent.Async+import TheatreDev.StmBased (Actor)+import TheatreDev.StmBased qualified as Actor+import Prelude++simulateReduction :: (Show a) => ([Actor a] -> Actor a) -> Int -> Int -> [a] -> IO [[a]]+simulateReduction reducer actorsNum generatorsNum messages =+ do+ resultsVar <- newTVarIO []++ actor <-+ fmap reducer+ $ replicateM actorsNum+ $ Actor.spawnStatefulIndividual+ []+ ( \state ->+ atomically+ $ modifyTVar' resultsVar (reverse state :)+ )+ ( \state msg ->+ return $ msg : state+ )++ mapConcurrently id+ $ replicate generatorsNum+ $ for_ messages+ $ Actor.tell actor++ Actor.kill actor+ Actor.wait actor++ readTVarIO resultsVar
+ hspec/TheatreDev/StmBasedSpec/Preferences.hs view
@@ -0,0 +1,9 @@+module TheatreDev.StmBasedSpec.Preferences where++import Prelude++concurrency :: Int+concurrency = 7++largePropertyMaxSuccess :: Int+largePropertyMaxSuccess = 10000
library/TheatreDev/ExtrasFor/TBQueue.hs view
@@ -6,13 +6,13 @@ flushNonEmptyTBQueue :: TBQueue a -> STM (NonEmpty a) flushNonEmptyTBQueue x = do head <- readTBQueue x- tail <- flushTBQueue x+ tail <- simplerFlushTBQueue x return (head :| tail) -- | Get a list of all entries in the queue without removing them. inspectTBQueue :: TBQueue a -> STM [a] inspectTBQueue queue = do- list <- flushTBQueue queue+ list <- simplerFlushTBQueue queue forM_ list $ writeTBQueue queue return list
library/TheatreDev/Perpetual.hs view
@@ -12,7 +12,7 @@ ) where -import qualified Control.Concurrent.Chan.Unagi as Unagi+import Control.Concurrent.Chan.Unagi qualified as Unagi import TheatreDev.Prelude -- |
library/TheatreDev/Prelude.hs view
@@ -8,6 +8,7 @@ import Control.Category as Exports import Control.Concurrent as Exports import Control.Concurrent.Async as Exports (Concurrently (..))+import Control.Concurrent.STM as Exports import Control.Exception as Exports import Control.Monad as Exports hiding (fail, forM, forM_, mapM, mapM_, msum, sequence, sequence_) import Control.Monad.Fail as Exports@@ -44,6 +45,7 @@ import Data.String as Exports import Data.Traversable as Exports import Data.Tuple as Exports+import Data.UUID as Exports (UUID) import Data.Unique as Exports import Data.Version as Exports import Data.Void as Exports
library/TheatreDev/StmBased.hs view
@@ -22,12 +22,13 @@ ) where +import Data.UUID.V4 qualified as UuidV4 import TheatreDev.Prelude import TheatreDev.StmBased.StmStructures.Runner (Runner)-import qualified TheatreDev.StmBased.StmStructures.Runner as Runner+import TheatreDev.StmBased.StmStructures.Runner qualified as Runner import TheatreDev.StmBased.Tell (Tell)-import qualified TheatreDev.StmBased.Tell as Tell-import qualified TheatreDev.StmBased.Wait as Wait+import TheatreDev.StmBased.Tell qualified as Tell+import TheatreDev.StmBased.Wait qualified as Wait -- | -- Controls of an actor, which processes the messages of type @message@.@@ -42,32 +43,37 @@ -- | Kill the actor. kill :: STM (), -- | Wait for the actor to die due to error or being killed.- wait :: STM (Maybe SomeException)+ wait :: STM (Maybe SomeException),+ -- | IDs of the constituent actors.+ -- Useful for debugging.+ ids :: [UUID] } instance Contravariant Actor where- contramap fn (Actor tell kill wait) =- Actor (tell . fn) kill wait+ contramap fn (Actor tell kill wait ids) =+ Actor (tell . fn) kill wait ids instance Divisible Actor where conquer =- Actor (const (return ())) (return ()) (return Nothing)- divide divisor (Actor lTell lKill lWait) (Actor rTell rKill rWait) =- Actor tell kill wait- where- tell msg = case divisor msg of (lMsg, rMsg) -> lTell lMsg >> rTell rMsg- kill = lKill >> rKill- wait = Wait.both lWait rWait+ Actor (const (return ())) (return ()) (return Nothing) []+ divide divisor (Actor lTell lKill lWait lIds) (Actor rTell rKill rWait rIds) =+ Actor+ { tell = \msg -> case divisor msg of (lMsg, rMsg) -> lTell lMsg >> rTell rMsg,+ kill = lKill >> rKill,+ wait = Wait.both lWait rWait,+ ids = lIds <> rIds+ } instance Decidable Actor where lose fn =- Actor (const (return ()) . absurd . fn) (return ()) (return Nothing)- choose choice (Actor lTell lKill lWait) (Actor rTell rKill rWait) =- Actor tell kill wait- where- tell = either lTell rTell . choice- kill = lKill >> rKill- wait = Wait.both lWait rWait+ Actor (const (return ()) . absurd . fn) (return ()) (return Nothing) []+ choose choice (Actor lTell lKill lWait lIds) (Actor rTell rKill rWait rIds) =+ Actor+ { tell = either lTell rTell . choice,+ kill = lKill >> rKill,+ wait = Wait.both lWait rWait,+ ids = lIds <> rIds+ } -- * Composition @@ -76,7 +82,8 @@ Actor { tell = Runner.tell runner, kill = Runner.kill runner,- wait = Runner.wait runner+ wait = Runner.wait runner,+ ids = [Runner.getId runner] } -- | Distribute the message stream across actors.@@ -109,7 +116,6 @@ -- The implementation applies a modulo equal to the amount -- of actors to the hash and thus determines the index -- of the actor to dispatch the message to.--- -- This is inspired by how partitioning is done in Kafka. byKeyHash :: -- | Function extracting the key from the message and hashing it.@@ -124,7 +130,8 @@ Actor { tell = tellReducer (fmap (.tell) actors), kill = traverse_ (.kill) actors,- wait = Wait.all (fmap (.wait) actors)+ wait = Wait.all (fmap (.wait) actors),+ ids = foldMap (.ids) actors } -- * Acquisition@@ -137,65 +144,49 @@ -- Killing that actor will make it process all the messages in the queue first. -- All the messages sent to it after killing won't be processed. spawnStatelessIndividual ::- -- | Interpreter of a message.- (message -> IO ()) ->+ (Show message) => -- | Clean up when killed. IO () ->+ -- | Interpreter of a message.+ (message -> IO ()) -> -- | Fork a thread to run the handler daemon on and -- produce a handle to control it. IO (Actor message)-spawnStatelessIndividual interpreter cleaner =+spawnStatelessIndividual cleaner interpreter = -- TODO: Optimize by reimplementing directly.- spawnStatefulIndividual () (const interpreter) (const cleaner)+ spawnStatefulIndividual () (const cleaner) (const interpreter) spawnStatelessBatched ::- -- | Interpreter of a batch of messages.- (NonEmpty message -> IO ()) ->+ (Show message) => -- | Clean up when killed. IO () ->+ -- | Interpreter of a batch of messages.+ (NonEmpty message -> IO ()) -> -- | Fork a thread to run the handler daemon on and -- produce a handle to control it. IO (Actor message)-spawnStatelessBatched interpreter cleaner =+spawnStatelessBatched cleaner interpreter = -- TODO: Optimize by reimplementing directly.- spawnStatefulBatched () (const interpreter) (const cleaner)+ spawnStatefulBatched () (const cleaner) (const interpreter) spawnStatefulIndividual ::+ (Show message) => state ->- (state -> message -> IO state) -> (state -> IO ()) ->+ (state -> message -> IO state) -> IO (Actor message)-spawnStatefulIndividual zero step finalizer =- do- runner <- atomically Runner.start- forkIOWithUnmask $ \unmask ->- let loop !state =- do- message <- atomically $ Runner.receiveSingle runner- case message of- Just message ->- do- result <- try @SomeException $ unmask $ step state message- case result of- Right newState ->- loop newState- Left exception ->- do- atomically $ Runner.releaseWithException runner exception- finalizer state- Nothing ->- finalizer state- in loop zero- return $ fromRunner runner+spawnStatefulIndividual zero finalizer step =+ spawnStatefulBatched zero finalizer $ foldM step spawnStatefulBatched ::+ (Show message) => state ->- (state -> NonEmpty message -> IO state) -> (state -> IO ()) ->+ (state -> NonEmpty message -> IO state) -> IO (Actor message)-spawnStatefulBatched zero step finalizer =+spawnStatefulBatched zero finalizer step = do- runner <- atomically Runner.start+ runner <- Runner.start forkIOWithUnmask $ \unmask -> let loop !state = do@@ -203,16 +194,19 @@ case messages of Just nonEmptyMessages -> do- result <- try @SomeException $ unmask $ step state nonEmptyMessages+ result <- try $ unmask $ step state nonEmptyMessages case result of Right newState -> loop newState Left exception ->- do- atomically $ Runner.releaseWithException runner exception- finalizer state+ finally (finalizer state)+ $ atomically+ $ Runner.releaseWithException runner exception -- Empty batch means that the runner is finished.- Nothing -> finalizer state+ Nothing ->+ finally (finalizer state)+ $ atomically+ $ Runner.releaseNormally runner in loop zero return $ fromRunner runner
library/TheatreDev/StmBased/StmStructures/Runner.hs view
@@ -10,27 +10,37 @@ receiveSingle, receiveMultiple, releaseWithException,+ releaseNormally,++ -- * Inspection+ getId, ) where import Control.Concurrent.STM.TBQueue import Control.Concurrent.STM.TMVar-import qualified TheatreDev.ExtrasFor.List as List+import Data.UUID.V4 qualified as UuidV4+import TheatreDev.ExtrasFor.List qualified as List import TheatreDev.ExtrasFor.TBQueue import TheatreDev.Prelude data Runner a = Runner- { queue :: TBQueue (Maybe a),+ { queue :: TBQueue a, aliveVar :: TVar Bool,- resVar :: TMVar (Maybe SomeException)+ resVar :: TMVar (Maybe SomeException),+ id :: UUID } -start :: STM (Runner a)+getId :: Runner a -> UUID+getId = (.id)++start :: IO (Runner a) start = do- queue <- newTBQueue 1000- aliveVar <- newTVar True- resVar <- newEmptyTMVar @(Maybe SomeException)+ queue <- newTBQueueIO 1000+ aliveVar <- newTVarIO True+ resVar <- newEmptyTMVarIO+ id <- UuidV4.nextRandom return Runner {..} tell :: Runner a -> a -> STM ()@@ -38,17 +48,18 @@ do alive <- readTVar aliveVar when alive do- writeTBQueue queue $ Just message+ writeTBQueue queue message kill :: Runner a -> STM () kill Runner {..} =- do- alive <- readTVar aliveVar- when alive do- writeTBQueue queue Nothing+ writeTVar aliveVar False wait :: Runner a -> STM (Maybe SomeException)-wait Runner {..} =+wait Runner {..} = do+ isAlive <- readTVar aliveVar+ when isAlive retry+ queueIsEmpty <- isEmptyTBQueue queue+ unless queueIsEmpty retry readTMVar resVar receiveSingle ::@@ -57,41 +68,33 @@ STM (Maybe a) receiveSingle Runner {..} = do- message <- readTBQueue queue- case message of- Just message -> return (Just message)- Nothing -> do- writeTVar aliveVar False- putTMVar resVar Nothing- return Nothing+ alive <- readTVar aliveVar+ if alive+ then Just <$> readTBQueue queue+ else return Nothing receiveMultiple ::+ (Show a) => Runner a -> STM (Maybe (NonEmpty a)) receiveMultiple Runner {..} = do- (messages, remainingCommands) <- do- queueLength <- lengthTBQueue queue- head <- readTBQueue queue- tail <- simplerFlushTBQueue queue- return $ List.splitWhileJust $ head : tail+ messages <- simplerFlushTBQueue queue case messages of- -- Implies that the tail is not empty,- -- because we have at least one element.- -- And that it starts with a Nothing. [] -> do- forM_ remainingCommands $ unGetTBQueue queue- writeTVar aliveVar False- putTMVar resVar Nothing- return Nothing- messagesHead : messagesTail -> do- unless (null remainingCommands) do- unGetTBQueue queue Nothing+ alive <- readTVar aliveVar+ if alive+ then retry+ else return Nothing+ messagesHead : messagesTail -> return $ Just $ messagesHead :| messagesTail releaseWithException :: Runner a -> SomeException -> STM () releaseWithException Runner {..} exception = do simplerFlushTBQueue queue- writeTVar aliveVar False putTMVar resVar (Just exception)++releaseNormally :: Runner a -> STM ()+releaseNormally Runner {..} =+ putTMVar resVar Nothing <|> pure ()
library/TheatreDev/StmBased/Tell.hs view
@@ -1,6 +1,6 @@ module TheatreDev.StmBased.Tell where -import qualified Data.Vector as Vector+import Data.Vector qualified as Vector import TheatreDev.Prelude type Tell a = a -> STM ()
library/TheatreDev/StmBased/Wait.hs view
@@ -6,22 +6,8 @@ both :: Wait -> Wait -> Wait both left right =- do- firstResult <- Left <$> left <|> Right <$> right- case firstResult of- Left Nothing -> right- Left (Just exception) -> return (Just exception)- Right Nothing -> left- Right (Just exception) -> return (Just exception)+ liftA2 (<|>) left right all :: [Wait] -> Wait all waits =- getException <|> getNothing- where- getException =- waits- & fmap (>>= maybe empty pure)- & asum- & fmap Just- getNothing =- Nothing <$ sequence_ waits+ asum <$> sequence waits
library/TheatreDev/Terminal/Actor.hs view
@@ -16,10 +16,10 @@ ) where -import qualified Control.Concurrent.Chan.Unagi as E+import Control.Concurrent.Chan.Unagi qualified as E import Control.Concurrent.STM.TBQueue import Control.Concurrent.STM.TMVar-import qualified TheatreDev.ExtrasFor.List as List+import TheatreDev.ExtrasFor.List qualified as List import TheatreDev.ExtrasFor.TBQueue import TheatreDev.Prelude
theatre-dev.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: theatre-dev-version: 0.0.1+version: 0.1 category: Concurrency, Actors synopsis: Minimalistic actor library experiments description:@@ -46,6 +46,7 @@ FunctionalDependencies GADTs GeneralizedNewtypeDeriving+ ImportQualifiedPost LambdaCase LiberalTypeSynonyms MagicHash@@ -98,8 +99,9 @@ , async >=2.2 && <3 , base >=4.16 && <5 , contravariant >=1.3 && <2- , stm >=2.5.2 && <3+ , stm >=2.5 && <2.5.2 || >=2.5.2.2 && <3 , unagi-chan >=0.4.1.4 && <0.5+ , uuid >=1.3.15 && <2 , vector >=0.13 && <0.14 test-suite hspec@@ -107,8 +109,14 @@ type: exitcode-stdio-1.0 hs-source-dirs: hspec main-is: Main.hs- other-modules: TheatreDev.StmBasedSpec+ other-modules:+ TheatreDev.StmBasedSpec+ TheatreDev.StmBasedSpec.IO+ TheatreDev.StmBasedSpec.Preferences+ build-depends:+ , async , hspec >=2.11.6 && <3+ , QuickCheck >=2.14 && <3 , rerebase <2 , theatre-dev