-
Notifications
You must be signed in to change notification settings - Fork 170
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
RUST-679 Simplify error API to ease pattern matching #301
Conversation
7a29c60
to
7361e48
Compare
@@ -74,7 +73,7 @@ pub(crate) struct ServerDescription { | |||
// allows us to ensure that only valid states are possible (e.g. preventing that both an error | |||
// and a reply are present) while still making it easy to define helper methods on | |||
// ServerDescription for information we need from the isMaster reply by propagating with `?`. | |||
pub(crate) reply: Result<Option<IsMasterReply>>, | |||
pub(crate) reply: Result<Option<IsMasterReply>, String>, |
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.
Because the only place we use the errors on server descriptions is in the error messages to users when server selection fails, the simplest way to avoid needing to copy errors around here is to just store the description of the error instead of the error itself, since that's what will be shown to users anyhow. The rest of the changes in this specific commit are due to this one change.
@@ -661,8 +666,13 @@ impl TopologyDescription { | |||
} | |||
|
|||
/// Create a new ServerDescription for each address and add it to the topology. | |||
fn add_new_servers<'a>(&mut self, servers: impl Iterator<Item = &'a String>) -> Result<()> { | |||
let servers: Result<Vec<_>> = servers.map(|server| StreamAddress::parse(server)).collect(); | |||
fn add_new_servers<'a>( |
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.
Rustfmt changed the spacing a bit here, so it might not be super clear what the change is: the return value of the function was changed from Result<()>
to Result<(), String>
, and then a call to to_string()
on the error was added to the iterator expression.
@@ -731,15 +741,17 @@ pub(crate) struct TopologyDescriptionDiff { | |||
pub(crate) new_addresses: HashSet<StreamAddress>, | |||
} | |||
|
|||
fn verify_max_staleness(max_staleness: Option<Duration>) -> Result<()> { | |||
fn verify_max_staleness(max_staleness: Option<Duration>) -> crate::error::Result<()> { |
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 were two existing uses of this function before this commit, one of which does return the error to users, so we still want to be able to return an actual Error rather than a string in this case. I split this function into two so that we don't have to pattern match the error and assert that it's an ArgumentError (when we know it always will be) when it's called in a place where we don't return the error to users.
@@ -74,7 +74,10 @@ impl Server { | |||
/// TODO: add success cases from application handshakes. | |||
#[derive(Debug)] | |||
pub(crate) enum ServerUpdate { | |||
Error { error: Error, error_generation: u32 }, | |||
Error { | |||
error: String, |
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.
Passed an owned error over the channel is no longer possible since the error also needs to be returned to the user in some places (e.g. when a new connection is created but fails to establish during a checkout). Luckily, the other side of the channel only uses the errors to mark the server as unknown in the topology, and an earlier commit in this PR already updated ServerDescription to use String instead of Error, so we can just pass the string across the channel eagerly instead of lazily getting the message on the other end.
#[error(display = "{}", kind)] | ||
#[non_exhaustive] | ||
pub struct Error { | ||
/// The type of error that occurred. | ||
pub kind: Arc<ErrorKind>, | ||
pub kind: ErrorKind, |
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.
This is the only substantive change in this commit; the rest are just fixes for the Arc being removed
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.
Overall looks good! I have one question about possibly preserving Clone
and a few other minor questions. Also, it looks like this broke the async-std tests + lint.
@@ -108,7 +108,7 @@ impl Client { | |||
|
|||
// Retryable writes are only supported by storage engines with document-level | |||
// locking, so users need to disable retryable writes if using mmapv1. | |||
if let ErrorKind::CommandError(ref err) = err.kind.as_ref() { | |||
if let ErrorKind::CommandError(ref err) = err.kind { |
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.
is there any reason we would want kind
to be a method instead of accessing the field directly? This is what std::io::Error
does, but I don't really have any further justification than that.
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 main benefit would be that we would be able to change the way that we store the field in the struct without breaking API. As it is currently, removing or changing the type of the field would break user code that accesses the field directly.
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 really have any strong feelings on this either way, if you think that protection is worthwhile then I'd say go ahead and make the change and if not, seems fine to me to leave it as is.
src/error.rs
Outdated
@@ -23,12 +23,12 @@ pub type Result<T> = std::result::Result<T, Error>; | |||
/// An error that can occur in the `mongodb` crate. The inner | |||
/// [`ErrorKind`](enum.ErrorKind.html) is wrapped in an `Arc` to allow the errors to be | |||
/// cloned. | |||
#[derive(Clone, Debug, Error)] |
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.
Before we go through with removing the Clone
implementation altogether, have we revisited just wrapping the std::io::Error
branch in an Arc
to preserve Clone
on our type? I went back to see why we didn't opt for this originally, and it seems to be to avoid having users handle the indirection of Arc
non-uniformally. For std::io::Error
specifically, I'm not sure if Arc
actually introduces any additional work for users though, since they have to invoke a method on the error (e.g. kind()
) to get any useful result anyways.
I've looked over the other branches of our ErrorKind
, and the only ones that also don't implement Clone
are our BSON errors (which we own and can make Clone
) and TokioTimeoutElapsed
error, which we don't actually even use since RUNTIME.timeout
converts everything to std::io::Error
anyways.
I think if we manage to keep Clone
alongside the changes made here to ease in matching, we'll have the best of both worlds.
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 considered that a bit too when working on this PR. I didn't end up making the change (although I hadn't looked back into why we didn't choose that originally), although a bit of that did make it into one of the proposals I made in the google doc in the context of making CommandFailedEvent
use an alternate error struct.
I guess my main question is what benefit we think users will get from having our error type implement Clone
. The only thing I can think of is the reason that we wanted it in the first place (to make their own wrapping error type cloneable), but I don't think this would be quite as common an issue for our users due to the fact that most of them are using the driver in the context of an application rather than a library, and the current consensus in the Rust community is that applications should just use Box<dyn Error>
(or something like anyhow
to abstract that). While there are some libraries out there that wrap the driver (and obviously more could be written in the future), my general instinct is to make any performance costs (e.g. heap allocations and reference counts) opt-in for users who need them rather than on by default with no way to opt out of, especially since the number of users who will need it in this case are far fewer than the ones who won't. My opposition to doing this isn't particularly strong, though.
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 can't really think of a specific example of what a user might need the cloning for, but I imagine it could be something similar to what we needed clone for as you mentioned. Even if they don't require the cloning (as we didn't), having to update their existing code could be non-trivial, as is seen in this PR. Also, if they really do need cloning, they'll need to wrap our entire error in an Arc
, which is pretty annoying to deal with (hence this PR 🙂), so even if it is somewhat uncommon, the frustration felt could be high and worth avoiding. Lastly, if we don't go for Clone
now, I don't think we'll be able to do so until 3.0, so if it does turn out to be a burden for users, we can't really give them any good fix.
Regarding performance costs, I definitely agree, but given that this Arc would only be allocated in single specific error case (i.e. not the "hot" path), I don't think it's much of a concern.
So overall, for reasons of reducing user friction + safeguarding us against any future need, I think we should try to preserve Clone
here.
This also has made me wonder, if we preserve Clone
, could we avoid having to make the String
changes in this PR? I realize this is an frustrating question to ask after all the work has been done and I'm really sorry about that; it's just that up to this point I had assumed that preserving Clone
wasn't going to be possible or easy but now have realized there could be another option.
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.
If we kept Error being Clone, then yes, I think we would want to undo the changes to CommandFailedEvent. All of the other changes are probably fine to keep though, since they don't put any burden on the user and are marginal perf improvements.
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.
LGTM! I think this will make our error API a lot nicer to use.
Looks like clippy needs to be satisfied but otherwise LGTM (I don't need to re-review the clippy fix) |
Looks like the clippy issue is just a slight memory inefficiency in the CMAP tests, which isn't really anything to worry about, so I suppressed it. |
7bdc777
to
c8f89a8
Compare
There were several different places in the driver that we assumed we could clone errors, so I handled each of those places (along with cascading changes due to other places related to that code) in a separate commit to make it easier to follow along with each one. The final commit removes the Arc and the Clone derive from the Error type. Because of this, I think the easier way to review this PR is to review each commit separately so that all of the different unrelated changes don't make things confusing. I'll push a separate commit for each change that's suggested during review, and then once everything looks okay, I'll rebase to move each change into the commit associated with the part that changed and request one final review to make sure that all of the changes look okay after that rebase.