Skip to content

Draft: Introduce hierarchical error types and error handling for plugins #3659

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

Closed

Conversation

fendor
Copy link
Collaborator

@fendor fendor commented Jun 14, 2023

Essentially, infuse the existing pluginResponse infrastructure with steroids:

  • Provide a simple interface for performing the work you usually want to do
  • Allow fine-grained control how errors are finally reported to the client
  • Hierarchical error types, somewhat analogous to the logger

@joyfulmantis
Copy link
Collaborator

joyfulmantis commented Jun 14, 2023 via email

@fendor fendor marked this pull request as draft June 14, 2023 15:01
@fendor fendor force-pushed the enhance/plugin-logger-structure branch 2 times, most recently from 7aad4b6 to 1e077da Compare June 15, 2023 19:26
@fendor fendor changed the title Draft: Improve logs of plugins Draft: Introduce hierarchical error types and error handling for plugins Jun 15, 2023
@fendor fendor force-pushed the enhance/plugin-logger-structure branch 2 times, most recently from 526fc9a to 565e016 Compare June 15, 2023 19:46
@fendor fendor force-pushed the enhance/plugin-logger-structure branch from 565e016 to 8adb03e Compare June 19, 2023 09:30
Copy link
Collaborator

@michaelpj michaelpj left a comment

Choose a reason for hiding this comment

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

I like the direction. At a high level my suggestions are:

  • Stuff inside Action already throws a bunch. Let's just use exceptions there, and have "strict" versions of the use functions that can throw if the thing isn't there.
  • Using ExceptT for the "main line" of plugin code seems nice. It would be nice if we could make the error type there fully generic so plugins can provide their own. I'm not sure how to do that because of the normalized file path stuff. The only suggestion I have is that we could also use exceptions there, and provide a throwing variant that runs in MonadIO. It's not like anyone wants to handle the failures there anyway!

@@ -964,11 +964,15 @@ useWithStale key file = runIdentity <$> usesWithStale key (Identity file)

-- | Request a Rule result, it not available return the last computed result which may be stale.
-- Errors out if none available.
--
-- The thrown error is a 'BadDependency' error which is caught by the rule system.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe link to the definition of Action, which also explains that it's okay to throw inside Actions.

)
nfp <- PluginUtils.withPluginError $ getNormalizedFilePath uri
env <- hscEnv . fst <$>
PluginUtils.runAction "codeLens.GhcSession" ideState
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason not to merge these all into one Action?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I did not want to impose Plugin changes for no reason, to keep it easier to review.
Also, I think some people prefer this style, because otherwise you have to return a big tuple and pattern match on it. I don't think there is any performance impact.


type PluginHandler e m a = ExceptT e m a

pluginResponse :: Monad m => ExceptT PluginError m a -> m (Either ResponseError a)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about

handlePluginError :: Pretty e => e -> ResponseError
handlePluginError msg = ResponseError Internalerror (renderPrettyWhatever msg) Nothing

pluginResponse :: Monad m => ExceptT e m a -> m (Either ResponseError a)

pluginResponse' is more generic still but I'm not sure if people really need that power?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, we can't make it completely generic as it is now because of the attempt to fold the getNormalizedFilePath functions in...

fmap (first handlePluginError)
. runExceptT

pluginResponse' :: Monad m => (e -> ResponseError) -> ExceptT e m a -> m (Either ResponseError a)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is nearly pluginResponse . withExceptT handleError, right? Maybe we should just let people do this inline, withExceptT is not so exotic...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Generally, I think if we can nudge people towards just using standard Control.Monad.Except stuff that seems good

| PluginUriToNormalizedFilePath J.Uri
| PluginErrorMessage T.Text

prettyPluginError :: PluginError -> T.Text
Copy link
Collaborator

Choose a reason for hiding this comment

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

instance Pretty PluginError?

-- | useE is useful to implement functions that aren’t rules but need shortcircuiting
-- e.g. getDefinition.
useWithStaleFast :: IdeRule k v => k -> NormalizedFilePath -> ExceptT GhcidePluginError IdeAction (v, PositionMapping)
useWithStaleFast k = maybeToExceptT (RuleFailed k) . MaybeT . Shake.useWithStaleFast k
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not totally sure why this gets a different error than useWithStale 🤔 More generally I think it would be good for the errors to match the function names. We can rename the functions... but let's keep the two consistent.

join $ shakeEnqueue (shakeExtras ide) (mkDelayedAction herald Logger.Debug $ runExceptT act)

-- | Request a Rule result, it not available return the last computed result which may be stale.
-- Errors out if none available.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can I request that we not use "errors out", it's very ambiguous when we have both ExceptT and exceptions in the picture. Maybe just "throws an exception"?

fmap (first (\msg -> ResponseError InternalError (fromString msg) Nothing))
. runExceptT
withError :: Functor m => (e' -> e) -> ExceptT e' m a -> ExceptT e m a
withError = withExceptT
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, I think I'm broadly in favour of getting people to use "normal" ExceptT functions as much as possible.

=> k -> NormalizedFilePath -> ExceptT e Action (v, PositionMapping)
useWithStale_ key file = ExceptT $ fmap Right $ Shake.useWithStale_ key file

useWithStale :: IdeRule k v
Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, so serious question: since we're in Action here, and it's safe to throw exceptions, is there any reason not to throw these errors as exceptions too?

That would give us an (I think) sensible heuristic:

  • Functions that run in Action throw exceptions
  • Functions that run outside Action use ExceptT

Copy link
Collaborator

Choose a reason for hiding this comment

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

That also suggests that the functions here could just become "stricter" versions of useWithStale and friends, which throw RuleNotReady instead of RuleFailed or whatever. Then this module mostly goes away and we instead add a few functions to Shake.hs

getFirstPragma (PluginId pId) state nfp = handleMaybeM "Could not get NextPragmaInfo" $ do
ghcSession <- liftIO $ runAction (T.unpack pId <> ".GhcSession") state $ useWithStale GhcSession nfp
(_, fileContents) <- liftIO $ runAction (T.unpack pId <> ".GetFileContents") state $ getFileContents nfp
getFirstPragma :: MonadIO m => PluginId -> IdeState -> NormalizedFilePath -> ExceptT GhcidePluginError m NextPragmaInfo
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we did what I suggested, this would "just" be in MonadIO. I think that's okay: if it's in MonadIO it can throw!

@@ -263,27 +277,64 @@ allLspCmdIds pid commands = concatMap go commands

-- ---------------------------------------------------------------------

getNormalizedFilePath :: Monad m => Uri -> ExceptT String m NormalizedFilePath
getNormalizedFilePath uri = handleMaybe errMsg
getNormalizedFilePath :: Monad m => Uri -> ExceptT PluginError m NormalizedFilePath
Copy link
Collaborator

Choose a reason for hiding this comment

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

So if we did what I'm suggesting, we'd also provide

getNormalizedFilePathIO :: MonadIO m => Uri -> m NormalizedFilePath

@michaelpj
Copy link
Collaborator

I realise that what I'm proposing is actually more radical than I maybe thought. It pretty much amounts to:

  • All plugin helper functions provide a variant that runs in MonadIO and throws on error. Plugins all run in MonadIO and have exception catching at the top level, so why not?
  • Plugins should just be expected to return ExceptT ResponseError m (Result...). If they want to use ExceptT with their own error type and use standard ExceptT to translate it to a ResponseError, that's pretty much completely up to them! At most we maybe could provide toResponseError :: Pretty e => e -> ResponseError

Perhaps this is a bit heretical, I'm basically proposing that we embrace exceptions in plugins. But maybe it would be fine?

@fendor
Copy link
Collaborator Author

fendor commented Jun 26, 2023

I like the idea of embracing exceptions, catching them and providing sensible log messages.
I am going to move the API towards what you are suggesting.

@joyfulmantis
Copy link
Collaborator

It would be nice if there was a PluginError type that wrapped ResponseErrors. The reason is that resolve methods will need to throw a ContentModified ResponseError if they are unable to resolve the provided request, and it would be nice to be able to use the available ExceptT pluginResponse infrastructure while at the same time being able to throw specific response errors.

@joyfulmantis
Copy link
Collaborator

If you end up going ahead with this, I think it might be a good idea to first merge the general infrastructure as a parallel system to what we have now. Then anyone interested in migrating the plugins, can do that piece by piece. Finally once everything is migrated, the old stuff can be removed.

@joyfulmantis
Copy link
Collaborator

Closing this as #3717 was merged

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

Successfully merging this pull request may close these issues.

3 participants