-
Notifications
You must be signed in to change notification settings - Fork 721
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
Better error message for query utxo on oops #4777
Better error message for query utxo on oops #4777
Conversation
cardano-api/src/Cardano/Api/Query.hs
Outdated
@@ -263,6 +277,23 @@ data QueryInShelleyBasedEra era result where | |||
:: PoolId | |||
-> QueryInShelleyBasedEra era (SerialisedStakeSnapshots era) | |||
|
|||
instance NtcVersionOf (QueryInShelleyBasedEra era result) where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot happening in this PR. Can you break it up, starting with the introduction of the NodeToClientVersion
class? And also how this can be used in the non-monadic query interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The NodeToClientVersion
is never used by the users of the API
. It is used internally to map from the query to which version of the protocol supports it.
Users will call queryExpr
, passing in a QueryInEra
. If that query is supported, queryExpr
will return normally. Otherwise UnsupportedNtcVersionError
is "thrown".
Users can catch UnsupportedNtcVersionError
if they wanted handle it, for example fall back to another query, return no results or throw a different error.
c879560
to
a89d79f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's hard to comment on precise changes without having a deep knowledge of the code, which I don't have, but here are my 2 cts:
- Being unfamiliar with the
BlockArguments
extension makes the code harder to read to me. Maybe it's only me, maybe it's not uncommon in which case we've made potential external contributors' work harder - I fail to see how the pattern
OO.catchM @AcquireFailure \e -> OO.throwM $ TxGenError $ show e
is better than usingcatches
with a list ofHandle a
- This code adds quite a few layers (Oops monad, exceptT monad) which adds more burden to the potential future person wanting to refactor this or add a new feature
- This requires wrapping things in
ExceptT
and at the same time use exceptions which seems somewhat contradictory to me: I would expect a strategy for error handling either embraces exceptions or explicit return types but not both for the same layer - This PR introduces heavy changes to code used quite often without a single test being changed, which would make me nervous if I were a maintainer of this codebase
- As already discussed on slack, this code adds yet another dependency on an external library which implies new contributors would need to know and understand this library
As an additional comment: I unfortunately don't have the time to provide some counterproposal that would solve the problem while IMO being simpler and easier to maintain, which I regret and implies you should take my comments with a grain of salt. |
49f4915
to
b186e93
Compare
As it is currently written, the benefits are non-obvious, but over time benefits accumulate. The following are the things we can expect.
In new code, one of the things we can do is to not handle the error and allow it to propagate which is a thing we cannot easily do with the old code. This means we would have a lot less marshalling.
With
When errors are expressed as a sum type for example This ends up being too hard an at the moment we tend to propagate everything. This is extremely risky. Maybe I add a new Therefore to the extent that maintenance of the current the code base isn't as bad as it could be it's because we are being lax with our error handling in a way where we could be introducing bugs.
The PR as it currently stands uses a lot of The use of the wrapping shows how that interoperating with existing code is possible and there is no the need to rewrite the entire code base in one go. Note that our codebase already does a lot of As more of the code transitions to the proposed style the
I think this is good. The code being modified is being tested indirectly through our integration tests. If a lot of tests had to be modified, I think this is bad because then I would have to worry if changes to the tests changes what the tests are testing.
I want the code to be intuitive. Contributors adding a new function to our code ought to be able to imitate existing code. Good error messages and type inference and good documentation will be what gets new contributors over the line. Over time the code will also become more regular. The library promotes the writing of code that reads straight down as a sequence of statements. The goal is to be able to cobble a bunch of statements together in a new function and the compiler or HLS can mostly just infer the type (which is a thing that fails enough with the current codebase that it bothers me). |
6949851
to
197a213
Compare
7c2307c
to
98e2811
Compare
…nt runQueryKesPeriodInfo with oops
…tate and delete getSbe_
a0467f3
to
a59f0f8
Compare
a59f0f8
to
ae0980b
Compare
This PR introduces the use of the
oops
library for simplifying error handling. Functions that useoops
for error handling end in an underscore. For example:determineEraExpr_
getNtcVersion_
getSbeInQuery_
handleQueryConvenienceErrors_
maybeQueryExpr_
queryEraHistory_
queryExpr_
queryNodeLocalState_
queryProtocolParams_
queryStakePools_
queryStateForBalancedTx_
querySystemStart_
queryUtxo_
setupLocalStateQueryExpr_
writeStakePools_
In most cases, equivalent functions that don't use
oops
types in their type signature are retained, but have been re-implemented to call theoops
version of the function. This is to allow code to gradually migrate to theoops
error handling style and give time to learn the new style.As a result of these changes
executeQueryCardanoMode
andexecuteQueryAnyMode
become unused and are removed.Reading notes
This PR adds code that uses a different error handling style supported by the
oops
library. Functions that have a typeoops
enabled signature end in an underscore so that they are easily distinguishable from others.Use of
BlockArguments
has been removed from the PR because it was optional in the first place and there was too much happening in the one PR.Stylistic choices
As part of implementing this feature, select parts of the code base has been converted to use
oops
error handling library to solve a particularly nasty problem.The shape of the problem can be described like this:
The problem is a long lived one I've been trying to solve since July 2021 in this PR #2957
Using the
oops
library allows us to neatly solve the above problem. For example theexecuteLocalStateQueryExpr_
is now:In the above,
e
is a type list of all possible types that could be thrown.There is a constraint
CouldBe
constraint that says thatAcquireFailure
must at least in the type list.This means that
executeLocalStateQueryExpr_
is able to throwAcquireFailure
while the query expression in functionf
is free to throw whatever it wants.To show how functions using
oops
are implemented, butexecuteQuery
function is reproduced below:The first thing to note that whilst this function uses
oops
, its type signature is justExceptT ShelleyQueryCmdError
.The interface between code that uses
oops
and regularExceptT
code is demarcated byrunOops1 @ShelleyQueryCommandError
.This function embeds an
ExceptT (Variant '[x])
expression into aExceptT x
expression.This means the
oops
expression in the followingdo
block may only throwShelleyQueryCmdError
.If functions inside this
do
block throw something else, they must be caught to return a value or re-throw asShelleyQueryCmdError
.The catch and re-throw is done by the lines:
Failing to catch an except that may not be thrown results in an error that looks like this:
This is a sign that the developer must do something with
UnsupportedNtcVersionError
. This can be one of the following options:ShelleyQueryCmdError
.UnsupportedNtcVersionError
by addingCouldBe e UnsupportedNtcVersionError
. This is not an option in this case becauserunOops1
imposes the restriction that only one errorShelleyQueryCmdAcquireFailure
may propagate, but in other contexts, this may be possible.An interesting feature of the
executeQuery
is that it makes heavy use of theBlockArguments
Haskell extension:Firstly the extension allows us to write
do
instead of$ do
.Secondly the expression that follows a
do
does not need to be aMonad
orApplicative
. It can be a value of any type.This happens here:
do \_ -> ...
.In this way the
do
is kind of acting like a parentheses around the expression.The expression is equivalent to
(\_ -> ...)
.The contents of these virtual parentheses is determined by indent.
The second
do
indo \_ -> do
is a regulardo
block.Finally there is
& do OO.catch ...
.Again, this is like
& (OO.catch ...)
.BlockArguments
allows us to not use parentheses which means the developer doesn't have to worry about closing parentheses lining up. They can look at indent instead which is easier to see and typical of other Haskell code.An added benefit is that fewer lines are touched when expressions need to be regrouped.
For example the opening parenthesis for the expression
Left e -> OO.throw (ShelleyQueryCmdLocalStateQueryError $ EraMismatchError e)))
are distributed across three different lines so regrouping at any of the three levels involves touching this line.Additionally, note that the type signature of
catch
is not typical of catch functions.Catch functions typically take the handler in the second argument.
catch
however takes the handler in the first argument.This is important because it means adding and removing handlers in the above code touches the fewest lines. We don't have to worry about nesting expressions further (which can touch a lot of lines and cause large
git
diffs) or breaking out handlers into a where clause.The use of
BlockArguments
is entirely optional. However when evaluating its merits it is worth considering that Haskell provides two mechanisms for grouping expressions: parentheses anddo
blocks.When refactoring code that has a mixture of both, the developer will have to maintain both the indent and the parentheses and ensure they play well together.
BlockArguments
allows the developer to no longer have to think about parentheses and just think about grouping code with indents. This halves the cognitive burden that grouping of expressions imposes on the developer.Finally,
oops
has decent type inference.For example GHC and HLS can infer that the type of this expression:
is this:
Which I can easily rewrite to my preferred type signature:
Why not use another library
I have evaluated a number of other options.
plucky
,fused-effects
,control-monad-exceptions
,polysemy
andhaskus-utils-variant
.They all suffer from a common problem which is the catch functions take the handler in the second argument making stacking handlers more fiddly that it needs to be.
Additionally they can have bad error messages. Take for example
plucky
:Some libraries like
fused-effects
andpolysemy
make it difficult to interoperate with the rest of the Haskell ecosystem. For example it is difficult to getResourceT
working if at all and a lot of libraries in the ecosystem usesResourceT
.Other common interoperability pain points include callbacks (for example with FFI callbacks), lazy
IO
andUnliftIO
.oops
, likelyplucky
is based onExceptT
which is common in the Haskell ecosystem and most developers know how to useExceptT
.oops
imposes very little friction when interoperating with libraries that don't useoops
.In some cases, a library's type inference is not up to scratch. For example in
oops
, if I try to infer the type of the expression:I get this:
Which isn't quite right, but I can guess what the type signature should be:
In
plucky
I'm not actually sure what the type of this is:The type doesn't compile to start with and the inferred type is wrong:
I would expect the type to be something like this to indicate no exceptions are thrown:
but that doesn't work.
The following doesn't work either:
The example might seem academic, but it is not. If
oops
fails to infer a valid type (is there even one) when the code catches all errors, then a developer could easily get into this situation by catching all errors. Code with all errors caught is valid code so it ought to have a type and that type should be inferrable.Finally
oops
does one more thing to help the developer past type inference issues. Every function that is used for error handling has an explicitforall
and the the generic type argument for the errorx
is always the first argument. This is because when writing error handling code where there may be multiple possible error types at place it is very beneficial for the developer to be able to useTypeApplications
to fix the error type to a particular type. Doing this will often resolve type inference problems.