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

Refactor metadata fetching into MetadataProvider trait #88

Merged
merged 6 commits into from
Jul 2, 2018

Conversation

csssuf
Copy link
Contributor

@csssuf csssuf commented Jun 18, 2018

Instead of fetch_metadata() fetching all potentially needed metadata, return a MetadataProvider trait object which has drop-in replacements for Metadata's functions. This allows only the requested metadata types to be fetched and avoids potentially expensive fetching work.

There's still a little bit of cleanup work that could take place in some of the providers (mostly rolling now-duplicate functions into the trait implementation), but those seemed like they should wait for another PR. This PR contains roughly the minimal amount of refactoring necessary to switch over to the MetadataProvider trait.

Fixes coreos/bugs#2362.

/// `fetch_metadata` is the generic, top-level function that is used by the main
/// function to fetch metadata. The configured provider is passed in and this
/// function dispatches the call to the correct provider-specific fetch function
pub fn fetch_metadata(provider: &str) -> Result<Metadata> {
pub fn fetch_metadata(provider: &str) -> Result<Box<providers::MetadataProvider>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the Box needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we're returning multiple different concrete types that all implement MetadataProvider, impl Trait can't be used and we need a Box to create an actual trait object.

Copy link
Contributor

Choose a reason for hiding this comment

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

I thought that was the whole point of impl Trait...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately not. From the RFC:

If a function returns impl Trait, its body can return values of any type that implements Trait, but all return values need to be of the same type.

Copy link
Contributor

Choose a reason for hiding this comment

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

@csssuf for my own reference: pushing the -> impl Trait one more level down too (i.e. returned by new()), would that also not work?

Copy link
Contributor Author

@csssuf csssuf Jun 19, 2018

Choose a reason for hiding this comment

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

Nope. The compiler needs to be able to figure out static dispatch to use impl Trait, so there's no way to trick it into doing the thing we want here.

Copy link
Member

Choose a reason for hiding this comment

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

https://www.reddit.com/r/rust/comments/8sqiin/newbie_question_why_is_the_impl_and_dyn_in_the/ is also useful on this topic (there's various others too)

With rustc beta today for example, one can write:

diff --git a/src/lib.rs b/src/lib.rs
index 0f80d1c..2ec3af3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -90,7 +90,7 @@ macro_rules! box_result {
 /// `fetch_metadata` is the generic, top-level function that is used by the main
 /// function to fetch metadata. The configured provider is passed in and this
 /// function dispatches the call to the correct provider-specific fetch function
-pub fn fetch_metadata(provider: &str) -> Result<Box<providers::MetadataProvider>> {
+pub fn fetch_metadata(provider: &str) -> Result<Box<dyn providers::MetadataProvider>> {
     match provider {
         "azure" => box_result!(azure::Azure::new()),
         "cloudstack-metadata" => box_result!(cloudstack::network::CloudstackNetwork::new()),

Then it's explicitly clear the goal is dynamic dispatch.

BTW I love how much discussion there's been over a single heap allocation 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

without having looked into it at all, I wonder if it would be possible to simultaneously use dyn and impl Trait to do what we want without a heap allocation? mostly curious, the current version is totally fine by me too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, they're mutually exclusive. We could avoid the Box by using an enum as the function's return type, and essentially provide our own dynamic dispatch by implementing the trait for the enum, but IMO that's even less clean than simply returning a Boxed trait object.

@csssuf csssuf force-pushed the partial-metadata branch 4 times, most recently from c52250e to 89485a9 Compare June 18, 2018 22:40
@cgwalters
Copy link
Member

This is the first time I've ever looked at this code, and my Rust is beginner-intermediate-ish...so take my comments with that in mind.

Overall, makes a lot of sense. A personal style thing, I would tend to use a single commit that adds and deletes code at once, but eh.

I was about to ask about tests, but I found they exist just for EC2. I assume Kola will cover other cases?

@arithx
Copy link
Contributor

arithx commented Jun 20, 2018

@cgwalters kola does indeed cover a number of cases. The tests are located here (also some fields are used in other individual tests).

@lucab
Copy link
Contributor

lucab commented Jun 20, 2018

Fully testing for this basically means reproducing all API and quirks of all supported cloud providers, so the mocking is mostly assuring that it doesn't break at a very coarse level. It only covers EC2 mostly due to time limitations, and because real/subtle regressions will hopefully break the integration CI on the relevant cloud provider.

impl MetadataProvider for Azure {
fn attributes(&self) -> Result<HashMap<String, String>> {
let attributes = self.get_attributes()?;
let mut out = HashMap::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

Here and in all the other providers too, what about with_capacity(2) instead?

impl ::std::ops::Drop for ConfigDrive {
fn drop(&mut self) {
if self.temp_dir.is_some() {
ConfigDrive::unmount(&self.path).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we should panic here. (This is carried-over code, feel free to defer this)

@lucab
Copy link
Contributor

lucab commented Jun 21, 2018

@csssuf I did a cursory review of this, and it seems overall fine. Most of this is just moving functions around, I didn't dig into those.

Copy link
Contributor

@sdemos sdemos left a comment

Choose a reason for hiding this comment

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

lgtm after we get positive tests on all the major cloud providers (it might need a rebase on master, shouldn't be any conflicts though)

fn attributes(&self) -> Result<HashMap<String, String>> {
let mut out = HashMap::with_capacity(3);

let add_value = |map: &mut HashMap<_, _>, key: &str, name| -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unfortunate that there is so much duplication with the add_value closure, but I'm not sure if there is a better way to do it after thinking through some of the alternatives.

Add a new trait `MetadataProvider` with functions for fetching each type
of metadata coreos-metadata knows about, and helper functions for
writing metadata to disk.
Add an implementation of `MetadataProvider` for each existing metadata
provider.
Now that we expose an Ec2Provider struct in providers/ec2, use it in
the ec2 mock tests.
Instead of fetching all possible metadata in fetch_metadata(), return a
trait object which provides the same functions for writing to disk as
the original `Metadata` struct, but only fetches metadata when
necessary.
Drop the unused fetch_metadata() function from each provider, as the
`MetadataProvider` trait is now used in their place.
`Metadata` is now unused after the switch to `MetadataProvider`, so drop
it.
@csssuf
Copy link
Contributor Author

csssuf commented Jun 22, 2018

Verified that the refactor's output matches the current release's (but without the unnecessary requests) on:

  • EC2
  • Azure
  • DigitalOcean
  • Packet
  • GCE

@csssuf csssuf merged commit 4c0ca16 into coreos:master Jul 2, 2018
@csssuf csssuf deleted the partial-metadata branch July 2, 2018 18:30
@csssuf csssuf mentioned this pull request Jul 2, 2018
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.

6 participants