packages feed

om-fork (empty) → 0.7.1.6

raw patch · 7 files changed

+535/−0 lines, 7 filesdep +aesondep +basedep +exceptionssetup-changed

Dependencies added: aeson, base, exceptions, ki, monad-logger, om-fork, om-show, text, unliftio

Files

+ LICENSE view
@@ -0,0 +1,19 @@+Copyright 2022 Rick Owens++Permission is hereby granted, free of charge, to any person obtaining a+copy of this software and associated documentation files (the "Software"),+to deal in the Software without restriction, including without limitation+the rights to use, copy, modify, merge, publish, distribute, sublicense,+and/or sell copies of the Software, and to permit persons to whom the+Software is furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR+OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,140 @@+# om-fork++This package provides some concurrency abstractions.++- [Structured concurrency](#structured-concurrency)+    - [Motivation](#motivation)+    - [Example](#example)+- [Actor model](#actor-model)+    - [Motivation](#motivation)+    - [Example](#example)++## Structured concurrency++This package provides some very limited tools for structured concurrency. These+tools are "limited" in the sense that they target a very specific use case:+making sure that if any one of a group of cooperating threads ends for any+reason, then they all end. For a more complete treatment of structured+concurrency, see the [ki](https://hackage.haskell.org/package/ki) package.++### Motivation++If you are using the actor model (see below) and one of your actors dies for+whatever reason, you probably want to crash completely instead of ending up in+some kind of "half-crashed" zombie state.++### Example++```haskell+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Main (main) where++import Control.Concurrent (threadDelay)+import Control.Exception (SomeException, try)+import Control.Monad (void)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad.Logger (runStdoutLoggingT)+import OM.Fork (race, runRace, wait)++main :: IO ()+main =+  void . try @SomeException $+    runStdoutLoggingT $ do+      runRace $ do+        race "loop1" $ loop1+        race "loop2" $ loop2+        race "loopThatCrashes" $ loopThatCrashes+        wait++loop1 :: (MonadIO m) => m void+loop1 = do+  liftIO $ do+    threadDelay 1_000_000+    putStrLn "loop1 cycle"+  loop1+  +loop2 :: (MonadIO m) => m void+loop2 = do+  liftIO $ do+    threadDelay 200_000+    putStrLn "loop2 cycle"+  loop2++loopThatCrashes :: (MonadIO m) => m void+loopThatCrashes = do+  liftIO $ do+    threadDelay 5_000_000+    putStrLn "loopThatCrashes about to crash"+  error "crash"+```++## Actor model++This package provides an abstraction over the "actor model". In+particular, the main thing of value is a way get heterogeneously typed+blocking responses from the actor, using `call`.++### Motivation++The actor model isn't suitable for every concurrency problem, but+maybe you've been programming in Erlang and you have a problem which+sits easily in your mind as an Actor Model problem. A full motivation+of the actor model is beyond the scope of this package.++Anyway, you probably want a way to interact with your "actor" in a type+safe way. This package provides convenient tools and patterns to do so.+++### Example+ ++```haskell+{-# LANGUAGE LambdaCase #-}++module Main (main) where++import Control.Concurrent (Chan, forkIO, newChan, readChan)+import Control.Monad (void)+import OM.Fork (Responder, call, cast, respond)+++{- | Messages that can be sent to the actor. -}+data MyMsg+  = ReverseEcho String (Responder String)+    {- ^ A blocking message. Responds with a String -}+  | Print String+    {- ^ A non-blocking message. -}+  | GetState (Responder Int)+    {- ^ Another blocking message. Responds with an Int -}+++{- | The "state" is just a count of how many messages we've seen so far. -}+actorLoop :: Int -> Chan MyMsg -> IO void+actorLoop state chan = do+  readChan chan >>= \case+      ReverseEcho str responder ->+        void $ respond responder (reverse str)+      Print str ->+        putStrLn ("Printeded in background by actor thread: " <> str)+      GetState responder ->+        void $ respond responder state+  actorLoop (succ state) chan+++main :: IO ()+main = do+  {- `Chan a` is an instance of `Actor` -}+  chan <- newChan+  void . forkIO $ actorLoop 0 chan++  {- | Notice that the result of this `call` is a String. -}+  putStrLn =<< call chan (ReverseEcho "hello world")+  cast chan (Print "foo")++  {- | Notice that the result of this `call` is an Int. -}+  actorState <- call chan GetState+  print (actorState * 2)+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/actor.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE LambdaCase #-}++module Main (main) where++import Control.Concurrent (Chan, forkIO, newChan, readChan)+import Control.Monad (void)+import OM.Fork (Responder, call, cast, respond)+++{- | Messages that can be sent to the actor. -}+data MyMsg+  = ReverseEcho String (Responder String)+    {- ^ A blocking message. Responds with a String -}+  | Print String+    {- ^ A non-blocking message. -}+  | GetState (Responder Int)+    {- ^ Another blocking message. Responds with an Int -}+++{- | The "state" is just a count of how many messages we've seen so far. -}+actorLoop :: Int -> Chan MyMsg -> IO void+actorLoop state chan = do+  readChan chan >>= \case+      ReverseEcho str responder ->+        void $ respond responder (reverse str)+      Print str ->+        putStrLn ("Printeded in background by actor thread: " <> str)+      GetState responder ->+        void $ respond responder state+  actorLoop (succ state) chan+++main :: IO ()+main = do+  {- `Chan a` is an instance of `Actor` -}+  chan <- newChan+  void . forkIO $ actorLoop 0 chan++  {- | Notice that the result of this `call` is a String. -}+  putStrLn =<< call chan (ReverseEcho "hello world")+  cast chan (Print "foo")++  {- | Notice that the result of this `call` is an Int. -}+  actorState <- call chan GetState+  print (actorState * 2)++
+ examples/structured.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Main (main) where++import Control.Concurrent (threadDelay)+import Control.Exception (SomeException, try)+import Control.Monad (void)+import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad.Logger (runStdoutLoggingT)+import OM.Fork (race, runRace, wait)++main :: IO ()+main =+  void . try @SomeException $+    runStdoutLoggingT $ do+      runRace $ do+        race "loop1" $ loop1+        race "loop2" $ loop2+        race "loopThatCrashes" $ loopThatCrashes+        wait++loop1 :: (MonadIO m) => m void+loop1 = do+  liftIO $ do+    threadDelay 1_000_000+    putStrLn "loop1 cycle"+  loop1+  +loop2 :: (MonadIO m) => m void+loop2 = do+  liftIO $ do+    threadDelay 200_000+    putStrLn "loop2 cycle"+  loop2++loopThatCrashes :: (MonadIO m) => m void+loopThatCrashes = do+  liftIO $ do+    threadDelay 5_000_000+    putStrLn "loopThatCrashes about to crash"+  error "crash"
+ om-fork.cabal view
@@ -0,0 +1,71 @@+cabal-version:       3.0+name:                om-fork+version:             0.7.1.6+synopsis:            Concurrency utilities.+description:         Actor pattern and some limited structured concurrency+                     tools+homepage:            https://github.com/owensmurray/om-fork+license:             MIT+license-file:        LICENSE+author:              Rick Owens+maintainer:          rick@owensmurray.com+copyright:           2018 Owens Murray, LLC.+-- category:            +build-type:          Simple+extra-source-files:+  README.md+  LICENSE++common dependencies+  build-depends:+    , aeson        >= 2.0.3.0  && < 2.1+    , base         >= 4.15.0.0 && < 4.16+    , exceptions   >= 0.10.4   && < 0.11+    , ki           >= 0.2.0.1  && < 0.3+    , monad-logger >= 0.3.36   && < 0.4+    , om-show      >= 0.1.2.0  && < 0.2+    , text         >= 1.2.5.0  && < 1.3+    , unliftio     >= 0.2.22.0 && < 0.3++common warnings+  ghc-options:+    -Wmissing-deriving-strategies+    -Wmissing-export-lists+    -Wmissing-import-lists+    -Wredundant-constraints+    -Wall++library+  import: warnings, dependencies+  exposed-modules:     +    OM.Fork+  -- other-modules:       +  -- other-extensions:    +  hs-source-dirs: src+  default-language: Haskell2010+++-- This isn't really a test, it is just used to make sure the example+-- compiles without clusttering up the package with a bunch of+-- executables.+test-suite actor-example+  import: warnings, dependencies+  main-is: actor.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: examples+  default-language: Haskell2010+  build-depends:+    , om-fork++-- This isn't really a test, it is just used to make sure the example+-- compiles without clusttering up the package with a bunch of+-- executables.+test-suite structured-example+  import: warnings, dependencies+  main-is: structured.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: examples+  default-language: Haskell2010+  build-depends:+    , om-fork+
+ src/OM/Fork.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ImplicitParams #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}++{- | Description: Thread utilities. -}+module OM.Fork (+  -- * Actor Communication.+  Actor(..),+  Responder,+  Responded,+  respond,+  call,+  cast,++  -- * Forking Background Processes.+  logUnexpectedTermination,+  ProcessName(..),+  Race,+  runRace,+  race,+  wait,+) where+++import Control.Concurrent (Chan, myThreadId, newEmptyMVar, putMVar,+  takeMVar, writeChan)+import Control.Monad (void)+import Control.Monad.Catch (MonadThrow(throwM), MonadCatch, SomeException,+  try)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Logger.CallStack (MonadLogger, logInfo, logWarn)+import Data.Aeson (ToJSON, toJSON)+import Data.String (IsString)+import Data.Text (Text)+import OM.Show (showt)+import UnliftIO (MonadUnliftIO, askRunInIO, throwString)+import qualified Ki+++{- | How to respond to a asynchronous message. -}+newtype Responder a = Responder {+    unResponder :: a -> IO ()+  }+instance ToJSON (Responder a) where+  toJSON _ = toJSON ("<Responder>" :: Text)+instance Show (Responder a) where+  show _ = "Responder"+++{- | The class of types that can act as the handle for an asynchronous actor. -}+class Actor a where+  {- | The type of messages associated with the actor. -}+  type Msg a+  {- | The channel through which messages can be sent to the actor. -}+  actorChan :: a -> Msg a -> IO ()+instance Actor (Chan m) where+  type Msg (Chan m) = m+  actorChan = writeChan+++{- | Respond to an asynchronous message. -}+respond :: (MonadIO m) => Responder a -> a -> m Responded+respond responder val = do+  liftIO (unResponder responder val)+  return Responded+++{- | Send a message to an actor, and wait for a response. -}+call+  :: ( Actor actor+     , MonadIO m+     )+  => actor {- ^ The actor to which we are sending a call request. -}+  -> (Responder a -> Msg actor)+     {- ^+       Given a way for the actor to respond to the message, construct+       a message that should be sent to the actor.++       Typically, your 'Msg' type will look something like this:++       > data MyMsg+       >   = MsgWithResponse SomeData (Responder ResponseType)+       >     -- In this example, this type of message requires a+       >     -- response. We package the responder up as part of the+       >     -- message itself. Idiomatically it is best to put the+       >     -- responder as the last argument so that it is easy to pass+       >     -- 'MsgWithResponse someData' to 'call'.+       >   | MsgWithoutResponse SomeData+       >     -- In this example, this type of message requires no response. It+       >     -- is a "fire and forget" message.++       you will call 'call' like this:++       > do+       >   response <- call actor (MsgWithResponse someData)+       >   -- response :: ResponseType++     -}+  -> m a+call actor mkMessage = liftIO $ do+  mVar <- newEmptyMVar+  actorChan actor (mkMessage (Responder (putMVar mVar)))+  takeMVar mVar+++{- | Send a message to an actor, but do not wait for a response. -}+cast :: (Actor actor, MonadIO m) => actor -> Msg actor -> m ()+cast actor = liftIO . actorChan actor+++{- |+  Proof that 'respond' was called. Clients can use this type in their+  type signatures when they require that 'respond' be called at least+  once, because calling 'respond' is the only way to generate values of+  this type.+-}+data Responded = Responded+++{- | Log (at WARN) when the action terminates for any reason. -}+logUnexpectedTermination :: (MonadLogger m, MonadCatch m)+  => ProcessName+  -> m a+  -> m a+logUnexpectedTermination (ProcessName name) action =+  try action >>= \case+    Left err -> do+      logWarn+        $ "Thread " <> name <> " finished with an error: " <> showt err+      throwM (err :: SomeException)+    Right v -> do+      logWarn $ "Thread " <> name <> " finished normally."+      return v+++{- |+  Run a thread race.++  Within the provided action, you can call 'race' to fork new background+  threads. When the action terminates, all background threads forked+  with 'race' are also terminated. Likewise, if any one of the racing+  threads terminates, then all other racing threads are terminated _and_+  'runRace' will throw an exception.++  In any event, when 'runRace' returns, all background threads forked+  by the @action@ using 'race' will have been terminated.+-}+runRace+  :: (MonadUnliftIO m)+  => (Race => m a) {- ^ - @action@: The provided "race" action. -}+  -> m a+runRace action = do+  runInIO <- askRunInIO+  liftIO . Ki.scoped $ \scope ->+    runInIO (let ?scope = scope in action)+++{- |+  This constraint indicates that we are in the context of a thread race. If any+  threads in the race terminate, then all threads in the race terminate.+  Threads are "in the race" if they were forked using 'race'.+-}+type Race = (?scope :: Ki.Scope)+++{- |+  Fork a new thread within the context of a race. This thread will be+  terminated when any other racing thread terminates, or else if this+  thread terminates first it will cause all other racing threads to+  be terminated.++  Generally, we normally expect that the thread is a "background thread"+  and will never terminate under "normal" conditions.+-}+race+  :: ( MonadCatch m+     , MonadLogger m+     , MonadUnliftIO m+     , Race+     )+  => ProcessName+  -> m a+  -> m ()+race name action = do+  runInIO <- askRunInIO+  liftIO+    . Ki.fork_ ?scope+    $ do+      tid <- myThreadId+      runInIO . logUnexpectedTermination name $ do+        logInfo $ "Starting thread (tid, name): " <> showt (tid, name)+        void action+      throwString $ "Thread Finished (tid, name): " <> show (tid, name)+++{- | The name of a process. -}+newtype ProcessName = ProcessName+  { unProcessName :: Text+  }+  deriving newtype (IsString, Semigroup, Monoid, Show)+++{- | Wait for all racing threads to terminate. -}+wait :: (MonadIO m, Race) => m ()+wait = liftIO $ Ki.wait ?scope++