-
Notifications
You must be signed in to change notification settings - Fork 36
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
Implemented custom errors types for Calloop #66
Conversation
Codecov Report
@@ Coverage Diff @@
## master #66 +/- ##
==========================================
- Coverage 81.84% 81.36% -0.49%
==========================================
Files 14 15 +1
Lines 1526 1524 -2
==========================================
- Hits 1249 1240 -9
- Misses 277 284 +7
Continue to review full report at Codecov.
|
Thanks! That looks nice at first glance, I'll take some time for a more in-depth review later. However glancing over your code this raises a question. It is a pre-existing issue, but your PR is related: currently, whenever an event source returns an error, the event loop dispatch is short-circuited and that error is early returned. I don't think that is a robust way to handle such errors, a single source erroring should not bring down the whole event loop. I'm thinking we should instead distinguish errors generated by calloop itself from errors generated by an event source, and errors generated by an event source should be accumulated (in a With such an architecture, I'm thinking there would not actually be any need to unify source-generated errors and calloop errors in a single error enum. 🤔 |
I think it greatly depends on how you're using Calloop. I tend to use it with only one or two top level sources that are ever actually added with In my code, once inside a A big part of this was also getting away from using |
Ok, so, took some time to look closer, and I'm not sure about the whole "error conversion extension traits". Wouldn't it be much simpler to just change |
If you were to do that, I'd suggest using anyhow (or maybe snafu). Anyhow has all the benefits of a boxed error, but also has some useful extra features and conversions, particularly the ability to statically downcast back to the original error. However the general consensus I've seen has been that anyhow is for binary crates, or libraries that consume a lot of different other APIs, whereas single-purpose libraries should have their own error type. The other wart here is that The conversion traits aren't necessary, but they're a common strategy I've seen used elsewhere - they're a workaround for the fact that you can't implement There is a third approach too: add an associated type alongside |
To be more specific, my call structure might look a lot like:
A lot of the time, the error handling for my specific use of event sources won't take the form of returned errors from An error returned from Anyway, that's just my usage, but it's what motivated this approach so you might find it informative. |
Ah yes, if you want to retrieve the error in a nested In that case, what I'd suggest instead is to add a new associated type With that, a process_events calling into a nested process_events will be able to process the error adequately, and if the toplevel process_events returns an error, the event loop can just box it and forward it as the return value of dispatch. How does that sound? |
I like that approach too, but will it cause problems when integrating event sources into the loop itself (ie. in |
I don't think so, the conversion to Lines 148 to 160 in 7198d71
|
Okay I'll code it up and see if I hit any snags. |
5869597
to
15d06c1
Compare
The associated type approach works very well! I kept the custom errors, but reduced their scope to internal errors that might need to bubble up to the public API. For actual user implemented event sources, they don't really need to be used unless you're doing extremely bespoke things in the I still need to update the examples and book, but I'll leave that until you've had a look at the latest approach. |
I quite like how this is getting at, nice 👍 On thing though, I was mostly expecting that each event source could have its own error type, to be able to express errors in a way that is insightful for the user, rather than just reuse the crate error type. Though this is mostly boring stuff, that I can take care of once your PR is merged if you don't feel like making all those detailed error types. |
They could, I only did that for the crate's own event sources because it... sort of... seemed more ergonomic. That's not a strong opinion though. If they didn't use a common error type, how would you break it down? Maybe:
Let me know, I don't mind coding it up, or at least a first pass at it. |
Take a look at this, I think it's closer to what you meant. Once I started writing it, it made a lot of sense. |
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.
Ok, I quite like the shape of this PR as it is now. 👍
Only missing things for are now a changelog entry and fixing of the FreeBSD CI.
Also, do you want to rebase your PR to make a clean commit history, or shall I squash the when merging it?
Hah, absolutely no problem. It took me almost a year to actually write it, so I understand.
Ah I overlooked FreeBSD, I'll fix that. Also the book and examples to some extent.
No, leave it to me to clean up, I've already done that a few times behind the scenes anyway. |
76261c5
to
a66528b
Compare
Updated changelog, fixed BSD and squashed the commits into code, book and changelog. Feel free to start reviewing, but wait before merging - I'm going to test these changes on some real projects and see how they fare. |
8981131
to
41a085d
Compare
Recent change: I realised as I was making my changes that I needed to add a couple of paras in the new book chapter on error handling. See if you agree with the following:
|
Hold up, I just realised a way I can simplify a few things. My last comment still applies, but wait a bit before reviewing the code. |
The paragraph you proposed seems like a good explanation to me! 👍 |
41a085d
to
cc9e512
Compare
Okay, I think this is a lot closer to what you'd hoped for. I realised that if the associated type is constrained by |
There is one thing this breaks that might not be obvious: fn main() -> anyhow::Result<()> {
let _ = calloop::EventLoop::<()>::try_new()?;
Ok(())
} previously compiled, but now gives:
This is tricky though. Making Calloop's error sync + send means that errors going into it need to be the same, which adds complexity to user code in other places. On the other hand, these are top-level functions and it's not hard to just convert the errors into strings or change the error type for main. Let me know what you think. |
Hmm, this has some other implications too. Let me do some more work on that aspect of things. |
Okay, I reached a compromise. Our new The I went with flattening them into strings, since it's unlikely that errors which propagate all the way out of the event loop will need to be downcast anyway - probably a user will need to either restart the event loop or the entire program. It's more likely to be useful to let users use a wider variety of errors in that associated type so they can compose them easily. |
9b9c57d
to
ecd8112
Compare
How restrictive would it be in practice to require that |
You have two in Calloop in fact 😉 — both the So to answer your question honestly: in general, not very restrictive. While I was playing around with the different approaches, the only general purpose error library I found that didn't have these baked in was Snafu, and only then its general purpose If we went the other way, the only major annoyance would be if eg. a user used a library that bubbled up a non Sync + Send error type and had to do some conversions at the boundary (probably in |
Right, fair point. 😅 However, I'd argue that this kind of error (which give back a value) is not really errors that are subject to being forwarding from an |
Exactly, typically they'd stay within the scope where the original value being "handed back" was created in the first place. I'll put those bounds back on and tidy up some things. |
7208c29
to
fb5cbf0
Compare
I've updated some projects to use this and I'm quite happy with how it's turned out. Go ahead and review it and let me know if you have any questions or feedback. |
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.
Okay, so there is one point about specifically the error type for Generic
, but other than that I really like this PR!
src/sources/generic.rs
Outdated
/// An error arising from processing events in a generic event source. Usually | ||
/// this will be an IO error. | ||
#[derive(thiserror::Error, Debug)] | ||
pub enum GenericError { | ||
/// This variant wraps a [`std::io::Error`], which might arise from | ||
/// operations on the underlying file descriptor. | ||
#[error("underlying IO error")] | ||
Io(#[from] std::io::Error), | ||
|
||
/// This variant wraps any other kind of error. | ||
#[error("error dispatching event")] | ||
Other(#[from] Box<dyn std::error::Error + Sync + Send>), | ||
} | ||
|
||
impl From<nix::errno::Errno> for GenericError { | ||
/// Converts a [`nix::Error`] into a wrapped version of the equivalent | ||
/// [`std::io::Error`]. | ||
fn from(err: nix::errno::Errno) -> Self { | ||
Into::<std::io::Error>::into(err).into() | ||
} | ||
} | ||
|
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.
Regarding the error type for Generic
, given this source is really meant as a building block for other event sources, I wonder if it would be more convenient to instead make it possible to specify the error type, and so have a Generic<F, E>
, with a default to E = std::io::Error
for convenience.
That would make something like this:
struct Generic<F: AsRawFd, E=std::io::Error> {
_error_type: std::marker::PhantomData<E>,
/* the rest of the struct */
}
impl<F, E> EventSource for Generic<F, E> where F: AsRawFd, E: Error + Send + Sync + 'static {
type Error = E;
type Ret = Result<Postaction, E>;
/* the rest of the impl */
}
This way, users that are fine with std::io::Error
can just use Generic<F>
with the default value for E
, and users that want to customize it for their internal use still can. How does that sound?
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.
You know, I was just hacking around this with both the inotify
and gpio
crates, I think this would work quite well.
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.
Ah wait, this might require "generic associated types" which are still being implemented. I'll look into it.
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.
Nope, I was wrong, it works fine. Let me check with some non-std crates.
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.
Annoyingly the default type parameter doesn't actually help a lot of the time, but it's not onerous
So
Generic::new(420, Interest::READ, Mode::Level)
might need to become
Generic::<_>::new(420, Interest::READ, Mode::Level)
see https://stackoverflow.com/questions/57824584/why-rust-dont-use-default-generic-parameter-type
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.
We can use the same trick the stdlib uses to handle the Alloc
parameter with collections, something like this:
struct Generic<F: AsRawFd, E=std::io::Error> {
_error_type: std::marker::PhantomData<E>,
fd: F
/* the rest of the struct */
}
impl<F: AsRawFd> Generic<F, std::io::Error> {
fn new(fd: F) -> Generic<F, std::io::Error> {
Generic {
fd,
_error_type: std::marker::PhantomData,
}
}
fn new_with_error<E>(fd: F) -> Generic<F, E> {
Generic {
fd,
_error_type: std::marker::PhantomData,
}
}
}
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.
Nice, that worked. One more thing, should the trait bound be Into<Box<dyn Error + etc>>
to match that on EventSource
?
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.
Sounds good yes. 👍
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.
Extremely happy with this change, it leads to some good gains in error handling for generic-based event sources eg. I can use anyhow to propagate informative errors up to a point where I can log them, or use my own structured errors to manage things. I deleted about 200 lines of extension trait and error mapping code in my own project.
fb5cbf0
to
8bf5abe
Compare
src/sources/generic.rs
Outdated
/// This exists only to allow us the make the associated types `Ret` and | ||
/// `Error` be generic. It should be revisited when *[generic associated | ||
/// types]* are complete and stable. | ||
/// | ||
/// [generic associated types]: https://github.com/rust-lang/rfcs/blob/master/text/1598-generic_associated_types.md |
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.
What kind of future revision are you thinking about?
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.
I think, but I'm not 100% sure, that when GATs are stable you should be able to get rid of the phantom data marker and just have the <E>
in the impl<F: AsRawFd, E: Into<Box<dyn Error etc>>> EventSource for Generic<F>
instead.
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.
I don't think that will work, using GAT here would require changing the EventSource
trait itself, and I'm not sure it'd be appropriate. I think that is going to stay as is, so I don't think this comment is necessary.
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.
Okay I re-read the RFC and blog post and you're right, I'll remove this.
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.
Alright! Sorry for the many rounds of review, I think this is now in a pretty good shape. There's just this comment about GAT that I don't think we should keep, but other than that, all is 👍
…vent sources. Calloop now uses thiserror-generated error types for certain API operations, and has an associated type on the EventSource to tell it what kind of error is returned from process_events(). Calloop-provided event sources all have their own error types.
A new section covers error handling with a couple of recommendations. All examples and existing sections are updated with the changes.
8bf5abe
to
ec6c5e8
Compare
No, it's good! I think it ended up in really good shape. |
Closes #57. Sorry this took so long, this year has been a bit garbage.
My plan was to have something that lets you use
?
seamlessly inprocess_events()
and other places, but I couldn't quite achieve that due to Rust's coherence rules. When specialisation becomes stable, that will help. Nonetheless I've added an extension trait makes things less fiddly for most of the uses cases I personally have encountered, which is hopefully a good start.On that note, I expect that this will change a bit in the near future as you and Calloop's users determine what works for them and what doesn't. I'm happy to be tagged in any issue reports or questions about it.
A super quick tour:
calloop::Error
.InsertError
, which needs to "return" the event source that couldn't be inserted. This is a bit of a weird case, but is very similar to MPSC channel sending errors - they have to return the item as well. This means you can't assume it will beSync + Send
, which breaks a few assumptions in libraries likethiserror
. Fortunately it's not a huge deal, and I don't think it happens much.calloop::Error::CallbackError
. Maybe the IO error too.Final note: I haven't run
cargo fmt
because I don't know what your policy is on that, and I didn't want to pollute the PR with formatting changes. Let me know if you'd rather I did.