Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use annotated-exception #131

Merged
merged 18 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.10.4.0...main)
## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.10.5.0...main)

## [v1.10.5.0](https://github.com/freckle/freckle-app/compare/v1.10.4.0...v1.10.5.0)

- Add `Freckle.App.Exception...` modules with exception utilities based on the
`annotated-exception` package.

- The Prelude module is expanded to reexport from `Freckle.App.Exception.MonadUnliftIO`
the following: `throwM`, `throwString`, `fromJustNoteM`, `catch`, `catchJust`,
`catches`, `try`, `tryJust`, `impossible`, `ExceptionHandler`, `Exception`,
`SomeException`. These should be used in place of their relevant counterparts from
packages `base`, `exceptions`, `safe-exceptions`, or `unliftio`.

- Add `Freckle.App.Random`

Expand Down
7 changes: 5 additions & 2 deletions freckle-app.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ cabal-version: 1.18
-- see: https://github.com/sol/hpack

name: freckle-app
version: 1.10.4.0
version: 1.10.5.0
synopsis: Haskell application toolkit used at Freckle
description: Please see README.md
category: Utils
Expand Down Expand Up @@ -39,6 +39,9 @@ library
Freckle.App.Dotenv
Freckle.App.Ecs
Freckle.App.Env
Freckle.App.Exception.MonadThrow
Freckle.App.Exception.MonadUnliftIO
Freckle.App.Exception.Types
Freckle.App.Ghci
Freckle.App.GlobalCache
Freckle.App.Http
Expand Down Expand Up @@ -108,6 +111,7 @@ library
, Glob
, MonadRandom
, aeson
, annotated-exception
, aws-xray-client-persistent
, aws-xray-client-wai
, base
Expand Down Expand Up @@ -294,7 +298,6 @@ test-suite spec
, monad-validate
, nonempty-containers
, postgresql-simple
, unliftio
, vector
, wai
, wai-extra
Expand Down
1 change: 0 additions & 1 deletion library/Freckle/App/Async.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import qualified Data.Aeson.Compat as KeyMap
import UnliftIO.Async (Async)
import qualified UnliftIO.Async as UnliftIO
import UnliftIO.Concurrent (threadDelay)
import UnliftIO.Exception (SomeException, displayException)

-- | 'UnliftIO.Async.async' but passing the thread context along
async :: (MonadMask m, MonadUnliftIO m) => m a -> m (Async a)
Expand Down
2 changes: 1 addition & 1 deletion library/Freckle/App/Bugsnag.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ import Database.PostgreSQL.Simple (SqlError (..))
import Database.PostgreSQL.Simple.Errors
import Freckle.App.Async (async)
import qualified Freckle.App.Env as Env
import qualified Freckle.App.Exception.MonadUnliftIO as Exception
import Network.Bugsnag hiding (notifyBugsnag, notifyBugsnagWith)
import qualified Network.Bugsnag as Bugsnag
import Network.HTTP.Client (HttpException (..), host, method)
import qualified UnliftIO.Exception as Exception
import Yesod.Core.Lens
import Yesod.Core.Types (HandlerData)

Expand Down
7 changes: 3 additions & 4 deletions library/Freckle/App/Csv.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import Data.Sequence.NonEmpty (NESeq)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import UnliftIO.Exception (handle)

-- | Treat CSV header line as 1
--
Expand Down Expand Up @@ -142,10 +141,10 @@ runCsvConduit
. MonadUnliftIO m
=> ConduitT () Void (ValidateT (Seq (CsvException err)) (ResourceT m)) r
-> m (Either (Seq (CsvException err)) r)
runCsvConduit = handle nonUtf8 . runResourceT . runValidateT . runConduit
runCsvConduit = flip catch nonUtf8 . runResourceT . runValidateT . runConduit
where
nonUtf8 :: Conduit.TextException -> m (Either (Seq (CsvException err)) r)
nonUtf8 = const $ pure $ Left $ pure CsvUnknownFileEncoding
nonUtf8 (_ :: Conduit.TextException) =
pure $ Left $ pure CsvUnknownFileEncoding

-- | Stream in 'ByteString's and parse records in constant space
decodeCsv
Expand Down
1 change: 0 additions & 1 deletion library/Freckle/App/Database.hs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ import qualified Freckle.App.Stats as Stats
import OpenTelemetry.Instrumentation.Persistent
import System.Process.Typed (proc, readProcessStdout_)
import UnliftIO.Concurrent (threadDelay)
import UnliftIO.Exception (displayException)
import UnliftIO.IORef
import Yesod.Core.Types (HandlerData (..), RunHandlerEnv (..))

Expand Down
1 change: 0 additions & 1 deletion library/Freckle/App/Ecs.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import Data.Aeson
import Data.List.Extra (dropPrefix)
import Freckle.App.Http
import System.Environment (lookupEnv)
import UnliftIO.Exception (Exception (..))

data EcsMetadata = EcsMetadata
{ emContainerMetadata :: EcsContainerMetadata
Expand Down
115 changes: 115 additions & 0 deletions library/Freckle/App/Exception/MonadThrow.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
module Freckle.App.Exception.MonadThrow
( throwM
, throwString
, fromJustNoteM
, impossible
, catch
, catchJust
, catches
, try
, tryJust
, checkpointCallStack

-- * Miscellany
, MonadThrow
, MonadCatch
, MonadMask
, module Freckle.App.Exception.Types
) where

import Freckle.App.Exception.Types

import Control.Applicative (pure)
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
import Data.Either (Either (..))
import Data.Function (($), (.))
import Data.Functor (fmap, (<$>))
import Data.Maybe (Maybe, maybe)
import Data.String (String)
import GHC.IO.Exception (userError)
import GHC.Stack (withFrozenCallStack)

import qualified Control.Exception.Annotated as Annotated
import qualified Control.Monad.Catch

-- Throws an exception, wrapped in 'AnnotatedException' which includes a call stack
throwM :: forall e m a. (Exception e, MonadThrow m, HasCallStack) => e -> m a
throwM = Annotated.throw

throwString :: forall m a. (MonadThrow m, HasCallStack) => String -> m a
throwString = throwM . userError

fromJustNoteM
:: forall m a. (MonadThrow m, HasCallStack) => String -> Maybe a -> m a
fromJustNoteM err = maybe (throwString err) pure

impossible :: forall m a. (MonadThrow m, HasCallStack) => m a
impossible = throwString "Impossible"

catch
:: forall e m a
. (Exception e, MonadCatch m, HasCallStack)
=> m a
-> (e -> m a)
-> m a
catch = withFrozenCallStack Annotated.catch

catchJust
:: forall e b m a
. (Exception e, MonadCatch m, HasCallStack)
=> (e -> Maybe b)
-> m a
-> (b -> m a)
-> m a
catchJust test action handler =
withFrozenCallStack $ Annotated.catch action $ \e ->
maybe (Control.Monad.Catch.throwM e) handler (test e)

catches
:: forall m a
. (MonadCatch m, HasCallStack)
=> m a
-- ^ Action to run
-> [ExceptionHandler m a]
-- ^ Recovery actions to run if the first action throws an exception
-- with a type of either @e@ or @'AnnotatedException' e@
-> m a
catches action handlers =
withFrozenCallStack $
Annotated.catches
action
(fmap (\case (ExceptionHandler f) -> Annotated.Handler f) handlers)

try
:: forall e m a
. (Exception e, MonadCatch m, HasCallStack)
=> m a
-- ^ Action to run
-> m (Either e a)
-- ^ Returns 'Left' if the action throws an exception with a type
-- of either @e@ or @'AnnotatedException' e@
try = withFrozenCallStack Annotated.try

tryJust
:: forall e b m a
. (Exception e, MonadCatch m, HasCallStack)
=> (e -> Maybe b)
-> m a
-- ^ Action to run
-> m (Either b a)
tryJust test action =
withFrozenCallStack $ Annotated.catch (Right <$> action) $ \e ->
maybe (Control.Monad.Catch.throwM e) (pure . Left) (test e)

-- | When dealing with a library that does not use 'AnnotatedException',
-- apply this function to augment its exceptions with call stacks.
checkpointCallStack
:: forall m a
. (MonadCatch m, HasCallStack)
=> m a
-- ^ Action that might throw whatever types of exceptions
-> m a
-- ^ Action that only throws 'AnnotatedException',
-- where the annotations include a call stack
checkpointCallStack =
withFrozenCallStack Annotated.checkpointCallStack
116 changes: 116 additions & 0 deletions library/Freckle/App/Exception/MonadUnliftIO.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
module Freckle.App.Exception.MonadUnliftIO
( throwM
, throwString
, fromJustNoteM
, impossible
, catch
, catchJust
, catches
, try
, tryJust
, checkpointCallStack

-- * Miscellany
, IO
, MonadIO
, MonadUnliftIO
, module Freckle.App.Exception.Types
) where

import Freckle.App.Exception.Types

import Control.Applicative (pure)
import Data.Either (Either (..))
import Data.Function (($), (.))
import Data.Functor (fmap, (<$>))
import Data.Maybe (Maybe, maybe)
import Data.String (String)
import GHC.IO.Exception (userError)
import GHC.Stack (withFrozenCallStack)
import System.IO (IO)
import UnliftIO (MonadIO, MonadUnliftIO)
import qualified UnliftIO.Exception

import qualified Control.Exception.Annotated.UnliftIO as Annotated

-- Throws an exception, wrapped in 'AnnotatedException' which includes a call stack
throwM :: forall e m a. (Exception e, MonadIO m, HasCallStack) => e -> m a
throwM = Annotated.throw

throwString :: forall m a. (MonadIO m, HasCallStack) => String -> m a
throwString = throwM . userError

fromJustNoteM
:: forall m a. (MonadIO m, HasCallStack) => String -> Maybe a -> m a
fromJustNoteM err = maybe (throwString err) pure

impossible :: forall m a. (MonadIO m, HasCallStack) => m a
impossible = throwString "Impossible"

catch
:: forall e m a
. (Exception e, MonadUnliftIO m, HasCallStack)
=> m a
-> (e -> m a)
-> m a
catch = withFrozenCallStack Annotated.catch

catchJust
:: forall e b m a
. (Exception e, MonadUnliftIO m, HasCallStack)
=> (e -> Maybe b)
-> m a
-> (b -> m a)
-> m a
catchJust test action handler =
withFrozenCallStack $ Annotated.catch action $ \e ->
maybe (UnliftIO.Exception.throwIO e) handler (test e)

catches
:: forall m a
. (MonadUnliftIO m, HasCallStack)
=> m a
-- ^ Action to run
-> [ExceptionHandler m a]
-- ^ Recovery actions to run if the first action throws an exception
-- with a type of either @e@ or @'AnnotatedException' e@
-> m a
catches action handlers =
withFrozenCallStack $
Annotated.catches
action
(fmap (\case (ExceptionHandler f) -> Annotated.Handler f) handlers)

try
:: forall e m a
. (Exception e, MonadUnliftIO m, HasCallStack)
=> m a
-- ^ Action to run
-> m (Either e a)
-- ^ Returns 'Left' if the action throws an exception with a type
-- of either @e@ or @'AnnotatedException' e@
try = withFrozenCallStack Annotated.try

tryJust
:: forall e b m a
. (Exception e, MonadUnliftIO m, HasCallStack)
=> (e -> Maybe b)
-> m a
-- ^ Action to run
-> m (Either b a)
tryJust test action =
withFrozenCallStack $ Annotated.catch (Right <$> action) $ \e ->
maybe (UnliftIO.Exception.throwIO e) (pure . Left) (test e)

-- | When dealing with a library that does not use 'AnnotatedException',
-- apply this function to augment its exceptions with call stacks.
checkpointCallStack
:: forall m a
. (MonadUnliftIO m, HasCallStack)
=> m a
-- ^ Action that might throw whatever types of exceptions
-> m a
-- ^ Action that only throws 'AnnotatedException',
-- where the annotations include a call stack
checkpointCallStack =
withFrozenCallStack Annotated.checkpointCallStack
15 changes: 15 additions & 0 deletions library/Freckle/App/Exception/Types.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Freckle.App.Exception.Types
( ExceptionHandler (..)
, AnnotatedException (..)
, Exception (..)
, SomeException (..)
, HasCallStack
) where

import Control.Exception (Exception (..), SomeException (..))
import Control.Exception.Annotated (AnnotatedException (..))
import GHC.Stack (HasCallStack)

-- Renamed just so that it can go into Freckle.App.Prelude and have a less generic name than 'Handler'
data ExceptionHandler m a
= forall e. Exception e => ExceptionHandler (e -> m a)
Comment on lines +14 to +15

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did something very similar! But we used a type and pattern combination to retain backwards compatibility.

9 changes: 5 additions & 4 deletions library/Freckle/App/Http.hs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ module Freckle.App.Http
-- error-handling specific to exceptions caused by 4XX responses:
--
-- @
-- 'handleJust' (guarded 'httpExceptionIsClientError') handle4XXError $ do
-- flip 'catchJust' (guard 'httpExceptionIsClientError' *> handle4XXError) $ do
-- resp <- 'httpJson' $ 'setRequestCheckStatus' $ parseRequest_ "http://..."
-- body <- 'getResponseBodyUnsafe' resp
--
Expand Down Expand Up @@ -137,7 +137,6 @@ import Network.HTTP.Types.Status
, statusIsServerError
, statusIsSuccessful
)
import UnliftIO.Exception (Exception (..), throwIO)

data HttpDecodeError = HttpDecodeError
{ hdeBody :: ByteString
Expand Down Expand Up @@ -213,8 +212,10 @@ addBearerAuthorizationHeader = addRequestHeader hAuthorization . ("Bearer " <>)
-- error response bodies too, you'll want to use 'setRequestCheckStatus' so that
-- you see status-code exceptions before 'HttpDecodeError's.
getResponseBodyUnsafe
:: (MonadIO m, Exception e) => Response (Either e a) -> m a
getResponseBodyUnsafe = either throwIO pure . getResponseBody
:: (MonadIO m, Exception e, HasCallStack)
=> Response (Either e a)
-> m a
getResponseBodyUnsafe = either throwM pure . getResponseBody

httpExceptionIsInformational :: HttpException -> Bool
httpExceptionIsInformational = filterStatusException statusIsInformational
Expand Down
Loading