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

{.async: (raises).} for MultistreamSelect #1066

Merged
merged 3 commits into from
Mar 12, 2024

Conversation

etan-status
Copy link
Contributor

Annotate MultistreamSelect with {.async: (raises).} and ensure that handle returns a MultiStreamError instead of CatchableError in the "invalid first message" case.

Annotate `MultistreamSelect` with `{.async: (raises).}` and ensure that
`handle` returns a `MultiStreamError` instead of `CatchableError` in the
"invalid first message" case.
Copy link

codecov bot commented Mar 12, 2024

Codecov Report

Attention: Patch coverage is 80.73394% with 21 lines in your changes are missing coverage. Please review.

Project coverage is 82.50%. Comparing base (08a48fa) to head (a24ead9).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##           unstable    #1066      +/-   ##
============================================
- Coverage     82.56%   82.50%   -0.06%     
============================================
  Files            91       91              
  Lines         15814    15849      +35     
============================================
+ Hits          13057    13077      +20     
- Misses         2757     2772      +15     
Files Coverage Δ
libp2p/protocols/protocol.nim 100.00% <100.00%> (ø)
libp2p/protocols/connectivity/relay/relay.nim 81.76% <81.81%> (+0.41%) ⬆️
libp2p/protocols/pubsub/gossipsub.nim 87.01% <81.81%> (-0.03%) ⬇️
libp2p/protocols/rendezvous.nim 76.73% <81.81%> (ø)
libp2p/multistream.nim 87.30% <76.92%> (-6.41%) ⬇️

... and 4 files with indirect coverage changes

conn: Connection,
proto: string
): Future[bool] {.async: (raises: [
CancelledError, LPStreamError, MultiStreamError]).} =
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure this is the right PR for it, but reraising underlying stream errors represents an abstraction leak - ie they should be caught and rebranded as multistreamerror really

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, but the pattern of re-raising LPStreamError applies to quite large parts of the codebase, and the goal for now is to just document the reality with the {.async: (raises).}, while fixing the obvious offenders of raise (ref CatchableError)(). Semantic changes should be separate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another precedent we have in muxer.nim where the procs currently raise {.async: (raises: [CancelledError, LPStreamError, MuxerError].}, it's just how it's done today.

proc start*(m: MultistreamSelect) {.async.} =
await allFutures(m.handlers.mapIt(it.protocol.start()))
proc start*(m: MultistreamSelect) {.async: (raises: []).} =
await noCancel allFutures(m.handlers.mapIt(it.protocol.start()))
Copy link
Contributor

Choose a reason for hiding this comment

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

hm, this is somewhat unexpected, that protocol start functions shouldn't support cancellation - I'm thinking about the case where for example a protocol might need to perform a dns lookup or the like and we want to exit the process - timeouts in such protocols can be longer than the "cancellation tolerance".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once more it is simply reflecting current behaviour. But agree that having cancellation on start feels more intuitive. Have updated the logic to support that, and also to properly process cancellation to avoid inconsistent state.

Copy link
Contributor

Choose a reason for hiding this comment

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

When deciding how to constrain raises, it's important to take into account an important aspect of exception-based API: when they raise CatchableError it means the calling code must be prepared to handle any error even if de facto there's no raise today - this is an important "feature" of designing exception-based api: you can "future-proof" them by signalling in the signature that you're raising more than you actually are - this is why the strategy of simply "describing current behavior" is .. incomplete: it doesn't take into account the non-code intent that went into the design.

Of course, in this particular case, there was probably no intent or thinking about cancellation at all since the code didn't actually do cancellation correctly until your fix, but when "narrowing" an API to raise less, it's a good question to ask.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, widening it to match the intended design space makes sense.

method start*(g: GossipSub) {.async.} =
method start*(
g: GossipSub
): Future[void] {.async: (raises: [CancelledError], raw: true).} =
Copy link
Contributor

Choose a reason for hiding this comment

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

why the raw here? can't see how it matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not familiar enough to know if it matters, but it makes the flow more explicit. there is no async involved in this method, so if the transformation introduces more complex logic or increases compile time, rather opting out of it

Copy link
Contributor

Choose a reason for hiding this comment

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

makes it hard to read though - complexity is negligible in simple cases like this, specially since it's not part of any hot path

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, that's true, the non-raw version is a bit simpler to read, but is a bit less clear about what's going on / as in, there is no async involved at all in the implementation, it's just a regular synchronous function.

side note: one of the other PRs actually was triggered by switching some of the redirectors to raw, it changed test timing just slightly to make it fail, revealing a small logic issue.

anyway, will keep it in mind going forward and transform to raw a bit less agressively. I think one could argue that anytime newFuture can be avoided, the readability benefit generally outweighs the optimization aspect; while for redirections / wrappers where newFuture is not necessary, the raw version looks similar enough to the transformed version that there is no downside to using it?

can do another cleanup pass for readability after the raises annotations are done, depending on the outcome of the discussion. personally I'm also fine with newFuture.

Copy link
Contributor

Choose a reason for hiding this comment

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

the thing with newFuture is that it opens up two avenues for "classic" bugs: returning nil on some code path and returning an accidentally unfinished future on some code path - it's risky shit, for a dumb start function. ie when reviewing, I had to spend time ascertaining this point (it would have been more clear btw had you put the newfuture call at the end of the function where it gets returned, instead of at the beginning because as is, there's too much unnecessary distance between construction and use).

The most simple things are the most pleasurable to debate ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, haven't been bitten by those personally, but can see them happening relatively easily. It's a good point, generally, especially as it seems that the async transformation is not too costly for purely synchronous functions.

@etan-status etan-status merged commit 48a3ac0 into unstable Mar 12, 2024
10 of 11 checks passed
@etan-status etan-status deleted the dev/etan/ex-multistream branch March 12, 2024 20:05
@etan-status
Copy link
Contributor Author

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

Successfully merging this pull request may close these issues.

2 participants