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

make it a compile error to accept more than one body extractor #556

Merged
merged 34 commits into from
Jan 18, 2023

Conversation

davepacheco
Copy link
Collaborator

@davepacheco davepacheco commented Jan 10, 2023

See #555 for motivation. We currently verify at runtime that you don't have two extractors that both try to read the body. We could make this a compile-time error instead.

This currently depends on #554 so I'm going to leave this as "draft". Once #554 lands, I will update the base branch of this PR. We may also want to wait for #557 (which implements #555), and then also wait for a prototype of that in Omicron, before landing this. Until we have all that, this could all be a bust.

Status:

  • get consensus that this seems promising
  • confirm it's not a disaster in consumers like Omicron. (It was very simple. There's just one custom extractor and it's easily converted to a SharedExtractor.) I'm happy to check any other consumers people would like.
  • get consensus on the rest of this path (remove hyper::Request from RequestContext #557 at least, and maybe remove Arc around RequestContext #558)
  • polish up this PR
    • changelog update
    • fix error messages so they're as useful as they were before when your endpoint function arg isn't an extractor
    • make sure we have tests
    • any other XXXs
  • fix up websocket argument order (and changelog for that)

@davepacheco davepacheco changed the base branch from main to extractor-move January 10, 2023 00:34
Copy link
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

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

looks good

@@ -0,0 +1,198 @@
// Copyright 2022 Oxide Computer Company
Copy link
Collaborator

Choose a reason for hiding this comment

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

2023 here and elsewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in 8ee3aa9.

pub trait ExclusiveExtractor: Send + Sync + Sized {
/// Construct an instance of this type from a `RequestContext`.
async fn from_request<Context: ServerContext>(
rqctx: &RequestContext<Context>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I assumed this would be either the actual RequestContext<Context> or a &mut to it; perhaps that's in a subsequent PR...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Counter-intuitively, this works with just &RequestContext<Context>. That's because what exclusive extractors need mutable access to is the hyper::Request, which is in the RequestContext but wrapped with Arc<Mutex<...>>. So you can take the lock to access a &mut hyper::Request.

After #558, the hyper::Request won't even be in the RequestContext any more.

So given that it's not necessary to have a &mut RequestContext and that the caller is going to pass the same RequestContext to the request handler function after calling this one, I prefer giving the extractor just a &RequestContext. Hopefully that all makes sense.

dropshot/src/extractor/common.rs Outdated Show resolved Hide resolved
--> tests/fail/bad_endpoint3.rs:17:12
|
17 | param: String,
| ^^^^^^ the trait `Extractor` is not implemented for `String`
Copy link
Collaborator

Choose a reason for hiding this comment

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

this seems helpful and I'm not sure the parts that remain are as helpful

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Totally. Since you're on board with this general idea, the next step is to figure out how to ensure good error messages when (1) an argument is not an extractor at all, or (2) you attempt to use two exclusive extractors. I think (1) is pretty easy because I made SharedExtractor impl ExclusiveExtractor, so we can change the existing check to check ExclusiveExtractor. I have not yet figured out a good way to do (2) but Rain suggested an approach I'm going to look at.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. The basic approach is to verify that:

  • the first argument is a RequestContext (like we did before)
  • the last argument is an ExclusiveExtractor (slight variation on what we did before)
  • all other arguments are SharedExtractor

This has the side effect of verifying that you don't have two exclusive extractors because they don't impl SharedExtractor.

The naming of the tests is a little weird. I kept the existing dense integer naming but it's a little confusing that test 19 is a slight variant of test 3. (That is, the behavior tested by the old bad_endpoint3.rs is now tested by a combination of bad_endpoint3.rs and bad_endpoint19.rs.) The alternatives seemed like separating into 3a and 3b or renaming everything to be more semantic. 🤷

Comment on lines 230 to 231
sig.output =
syn::parse2(quote!(-> dropshot::WebsocketEndpointResult))?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

huh.. had not seen this construction.

const _: fn() = || {
fn need_extractor<T>()
where
T: ?Sized + dropshot::Extractor,
Copy link
Collaborator

Choose a reason for hiding this comment

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

could this change to RequestExtractor?

Copy link
Collaborator

Choose a reason for hiding this comment

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

... or maybe SharedExtractor?

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 think it could change to ExclusiveExtractor (shared extractors can be treated as exclusive, but not the other way around). (see my other comment -- I do intend to restore a helpful message for this case, one way or another)

@@ -173,7 +173,8 @@ fn do_channel(
ChannelProtocol::WEBSOCKETS => {
// here we construct a wrapper function and mutate the arguments a bit
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// here we construct a wrapper function and mutate the arguments a bit
// Here we construct a wrapper function and mutate the arguments a bit

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done in fb31831.

Base automatically changed from extractor-move to main January 11, 2023 18:35
Comment on lines 223 to 235
// Historically, we required that the `WebsocketConnection` argument
// be first after the `RequestContext`. However, we also require
// that any exclusive extractor (which includes the
// `WebsocketUpgrade` argument that we put in its place) appears
// last. We replaced the type above, but now we need to put it in
// the right spot.
sig.inputs = {
let mut input_pairs =
sig.inputs.clone().into_pairs().collect::<Vec<_>>();
let second_pair = input_pairs.remove(1);
input_pairs.push(second_pair);
input_pairs.into_iter().collect()
};
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'm curious what people think about this. The comment basically summarizes it: we previously said that a WebsocketConnection has to be the first extractor. As part of this PR, I decided that exclusive extractors (which would include this one) have to be last. I did that because I think it's natural for the extractors to appear in the order they look at parts of the HTTP request, and exclusive extractors generally look at the request body (the last thing).

There's not actually a conflict here because this macro already translates the user's argument list into what the endpoint macro expects, so we can shuffle the arguments around like this. But it's kind of annoying that we have to do this, and a user might notice the weirdness that their WebsocketConnection has to appear first, but other exclusive extractors have to appear last.

We could:

  • do nothing (what we're doing here). Downside: this ugliness, and maybe slightly confusing for people?
  • make WebsocketConnection appear last. Downside: breaking change, though trivial to fix.
  • make exclusive extractors appear first instead. Downside: less intuitive for the much more common case

Thoughts @ahl @lifning ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I like making WebsocketConnection appear last -- it's a breaking change but I think it's justified by the overall direction of the PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

likewise, i would by far prefer consistency over just leaving things as-is. and being that the only code using it now is this one thing, now would be the time to make such a change

@davepacheco
Copy link
Collaborator Author

I've sync'd this change up with "main", cleaned up the tests and other XXXs, and written migration instructions in the changelog. I think it's ready to go except maybe for this one question.

@ahl Unfortunately I think the way I sync'd this up will make it annoying to do an incremental review. Sorry. Let me know if I can help.

@davepacheco davepacheco marked this pull request as ready for review January 18, 2023 16:57
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.

4 participants