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

inSpan without UnliftIO #76

Open
JonathanLorimer opened this issue May 14, 2023 · 4 comments
Open

inSpan without UnliftIO #76

JonathanLorimer opened this issue May 14, 2023 · 4 comments

Comments

@JonathanLorimer
Copy link
Collaborator

I am working on an app where the monad doesn't support unliftio. I would like to understand why inSpan requires UnliftIO? It seems like it is just in the bracketError function. It would be great to provide an inSpan function that doesn't require this constraint, but I am not sure where to start.

@JonathanLorimer
Copy link
Collaborator Author

for example, I think this works:

bracketErrorIO :: IO a -> (Maybe SomeException -> a -> IO b) -> (a -> IO c) -> IO c
bracketErrorIO before after thing = EUnsafe.mask $ \restore -> do
  x <- before
  res1 <- EUnsafe.try $ restore $ thing x
  case res1 of
    Left (e1 :: SomeException) -> do
      -- explicitly ignore exceptions from after. We know that
      -- no async exceptions were thrown there, so therefore
      -- the stronger exception must come from thing
      --
      -- https://github.com/fpco/safe-exceptions/issues/2
      _ :: Either SomeException b <-
        EUnsafe.try $ EUnsafe.uninterruptibleMask_ $ after (Just e1) x
      EUnsafe.throwIO e1
    Right y -> do
      _ <- EUnsafe.uninterruptibleMask_ $ after Nothing x
      return y

@iand675
Copy link
Owner

iand675 commented May 14, 2023

This is probably a good overview on the tradeoffs around catching errors for various monads:

https://stackoverflow.com/questions/46425062/should-i-prefer-monadunliftio-or-monadmask-for-bracketting-like-functions

if you know [...] (that you) want compile time guarantees about the correctness of your code vis-a-vis monadic state, use MonadUnliftIO.

This is a particularly important principal for observability tooling– the presence of of tracing should not affect the semantics of the code that's being instrumented. Therefore, we go with the most conservative option for what the library supports by default. If you don't particularly care about maintaining those semantics in your situation, you could use the exceptions-package based implementation provided here: https://github.com/iand675/hs-opentelemetry/tree/main/utils/exceptions

However, there are often ways to make it work for your situation, even with MonadUnliftIO:

Say you're working with ExceptT, for example:

getUser :: MonadIO m => ExceptT HttpResponse m User
getUser = do
  thing1 <- foo
  thing2 <- bar
  thing3 <- baz
  pure $ constructUser thing1 thing2 thing3
  • Option 1: use inSpan within liftIO if you are able
  ...
  thing2 <- liftIO $ inSpan "bar" bar
  ...
  • Option 2: use a natural transformation to convert your code that doesn't support MonadUnliftIO into one that handles exceptions better. This is a bit of code I use when I use servant to avoid the overhead of ExceptT and support a MonadUnliftIO instance, as an example:
-- natural transformation
nt :: AppState -> AppM a -> Handler a
nt s x = Handler $ ExceptT $ catch (Right <$> runAppM s x) $ \(e :: ServerError) -> pure $ Left e
  • Option 3: Use the exceptions based OTel instrumentation I mentioned above. Likely the easiest drop in solution, but with the caveat that it can be subtly wrong in some cases if you don't pay close attention to the semantics of your monad transformer stack.

@JonathanLorimer
Copy link
Collaborator Author

Oh this is great. I actually ended up implementing exactly the exceptions solution, but I think the natural transformation is the one I want (and I happen to be using servant).

@parsonsmatt
Copy link
Collaborator

I do think there's value in providing a class (Monad m) => MonadSpan m where inSpan :: (span arguments) -> m a -> m a, even if that ends up being implemented as something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants