Skip to content
This repository has been archived by the owner on Apr 16, 2020. It is now read-only.

Introduce tower_grpc::metadata::MetadataMap type #92

Merged
merged 11 commits into from Dec 5, 2018

Conversation

per-gron
Copy link
Collaborator

@per-gron per-gron commented Nov 8, 2018

As per the discussion in #90, it wraps http::HeaderMap to abstract away the fact that tower-grpc is HTTP based. At this time this class is a pure wrapper that does not actually implement any of the gRPC metadata specific stuff (such as binary fields), that will come later.

My goal with this PR is to make MetadataMap (largely) equivalent to HeaderMap while not exposing http types.

As per the discussion in tower-rs#90, it wraps http::HeaderMap to abstract
away the fact that tower-grpc is HTTP based. At this time this class
is a pure wrapper that does not actually implement any of the gRPC
metadata specific stuff (such as binary fields), that will come later.
@seanmonstar
Copy link
Contributor

Wow! I haven't reviewed yet, I'm about to look through, but first thoughts: you really took this seriously, this is impressive!

Copy link
Contributor

@seanmonstar seanmonstar left a comment

Choose a reason for hiding this comment

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

I see now that much of the diff is wrapping the http::header internals. Well done!

What was the decision regarding -bin headers?

/// Convert an HTTP HeaderMap to a MetadataMap
pub fn from_headers(mut headers: http::HeaderMap) -> Self {
let mut map = Self::with_capacity(headers.len());
for (name, values) in headers.drain() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to move these from the headers into a brand new map? Could this just be MetadataMap { inner: headers }?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

http::HeaderMap is equal to http::HeaderMap<http::HeaderValue>. MetadataMap however has a http::HeaderMap<MetadataValue>, so when constructing a MetadataMap the values need to be wrapped individually.

MetadataMap can't just wrap a http::HeaderMap<http::HeaderValue> because there are some important methods that return references to those values, which would be unimplementable then (or would require a separate MetadataValueRef type that wrapped HeaderName&).

(There is a similar issue for methods that return references to map keys but I worked around that by returning &strs instead, which is basically what HeaderName is anyways after construction and you don't need to quickly compare them with standard HTTP header names.)

At least this code just moves all the headers, so runtime is proportional to header count but not size.

Copy link
Member

Choose a reason for hiding this comment

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

We could still do it, though it would require an unsafe transmute. stdlib relies on this being valid in a number of places. For example, Path::new where Path(OsStr):

    pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &Path {
        unsafe { &*(s.as_ref() as *const OsStr as *const Path) }
    }

I would use this strategy personally. I think a zero-cost MetadataMap wrapper is critical here, otherwise switching is not be worth it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cool idea! Done.

}

/// Convert an HTTP HeaderMap to a MetadataMap
pub fn from_headers(mut headers: http::HeaderMap) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wasn't the point to hide any http types? Why did you feel this constructor was needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These two conversion methods are used in src/request.rs. Perhaps I should make them pub(crate) instead.

Copy link
Member

Choose a reason for hiding this comment

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

I mean... it doesn't seem terrible to have conversions. Being able to easily convert between the two does not violate "hiding".

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, I'll keep them public then.

@per-gron
Copy link
Collaborator Author

per-gron commented Nov 9, 2018

What was the decision regarding -bin headers?

This PR does not address that issue. This just hides the HeaderMap type from tower-grpc users, while exposing an API that is almost identical. I intend to address -bin headers in a later PR. I felt this PR was already large enough 😃

Differences include:

  • Wherever HeaderMap methods return a HeaderName&, the corresponding MetadataMap method returns str& because there is no MetadataKey value stored anywhere to take a reference of.
  • There is no Index operator, because I plan to make separate getters for binary and non-binary fields and then it's not clear if indexing should get binary or non-binary fields. Also I'm not a big fan of having such a convenient shorthand for a panic-ing operator.

Copy link
Member

@carllerche carllerche left a comment

Choose a reason for hiding this comment

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

Thanks for doing this 👍 Looking great.

Some thoughts?

  • Could you add some some top level docs (even just a few lines) for the metadata module.
  • To avoid cluttering the metadata module, maybe we can create metadata::errors and export all error types there. Those are less commonly needed and it would make exploring mod metadata easier.

Additional inline comments.

src/metadata_key.rs Outdated Show resolved Hide resolved
}

/// Convert an HTTP HeaderMap to a MetadataMap
pub fn from_headers(mut headers: http::HeaderMap) -> Self {
Copy link
Member

Choose a reason for hiding this comment

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

I mean... it doesn't seem terrible to have conversions. Being able to easily convert between the two does not violate "hiding".

/// Convert an HTTP HeaderMap to a MetadataMap
pub fn from_headers(mut headers: http::HeaderMap) -> Self {
let mut map = Self::with_capacity(headers.len());
for (name, values) in headers.drain() {
Copy link
Member

Choose a reason for hiding this comment

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

We could still do it, though it would require an unsafe transmute. stdlib relies on this being valid in a number of places. For example, Path::new where Path(OsStr):

    pub fn new<S: AsRef<OsStr> + ?Sized>(s: &S) -> &Path {
        unsafe { &*(s.as_ref() as *const OsStr as *const Path) }
    }

I would use this strategy personally. I think a zero-cost MetadataMap wrapper is critical here, otherwise switching is not be worth it.

src/metadata_map.rs Outdated Show resolved Hide resolved
src/metadata_map.rs Outdated Show resolved Hide resolved
src/metadata_map.rs Show resolved Hide resolved
src/metadata_map.rs Show resolved Hide resolved
src/metadata_map.rs Show resolved Hide resolved
src/metadata_map.rs Outdated Show resolved Hide resolved
src/metadata_map.rs Show resolved Hide resolved
This makes the API more self-symmetrical and more similar to HeaderMap.
Copy link
Member

@carllerche carllerche left a comment

Choose a reason for hiding this comment

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

LGTM modulo that commented out bit that depends on hyperium/http#279.

@seanmonstar you good with this?

@carllerche
Copy link
Member

@per-gron I pushed an http release: https://crates.io/crates/http/0.1.14


#[inline]
pub(crate) fn from_header_value(header_value: &HeaderValue) -> &Self {
unsafe { &*(header_value as *const HeaderValue as *const Self) }
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the only way this isn't UB is if MetadataValue has the #[repr(transparent)] attribute on it. Otherwise, struct layout is undefined.

Copy link
Member

Choose a reason for hiding this comment

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

As long as the struct has a single field and has no other repr on it, it is pretty much guaranteed to have the same layout. I don’t think it has been expressly stated, but std assumes this and so do many other crates.

This technique is basically the only way to define your own DST and has been shown to me by rust team members.

I think we can rely on it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@seanmonstar thanks for the link to #[repr(transparent)]! I was not aware of it. It seems like the perfect thing for this use case, as that is exactly the guarantee that is necessary in this case. I have updated the PR to use it.

Copy link
Member

Choose a reason for hiding this comment

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

I did it realize transparent was stable... that is the best option.

@per-gron
Copy link
Collaborator Author

I fixed the last issues. I think this is ready to merge now.

@per-gron
Copy link
Collaborator Author

per-gron commented Dec 5, 2018

I'll go ahead and merge this now.

@per-gron per-gron merged commit ca2ee54 into tower-rs:master Dec 5, 2018
@per-gron per-gron deleted the request-metadata branch December 5, 2018 10:08
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants