Skip to content

Conversation

rylev
Copy link
Contributor

@rylev rylev commented Mar 30, 2021

This pull request introduces a new way to architect SDKs, known from here on as the "policy" architecture. This is the architecture that other Azure SDKs use.

Why this architecture?

There are several ways to architect an SDK, but when in doubt it's best to align ourselves closely with the SDKs from other languages. Luckily this architecture lends itself fairly well to Rust. It is our eventual hope that this crate becomes officially supported by the the Azure SDK team, and this is a good faith step in that direction since the SDK team strongly wishes that their SDKs have a similar architecture and only differ from each other when there is a good language specific reason to (and not "just because").

Highlights of the architecture

The architecture is fairly straightforward. At a high-level operations against the Azure API are performed by creating sending requests and their subsequent responses through a pipeline of policies. Each policy in the pipeline can transform the outgoing request and react to incoming responses. It is up to each policy to continue the policy pipeline unless it explicitly chooses not to.

At the end of the pipeline is the TransportPolicy which "performs" the HTTP request (either by actually making a network request or in the case of testing, returning some stubbed response object).

As an example of a policy, the Retry policy will send a request further down the pipeline keeping track of a specified number of retries. if the response is successful, it returns the response to the caller, and it if is not, it will retry the request until the number of retries has been exhausted.

Pipelines are a per-client configuration so basically anything that would make sense to configure for a given client (for all their operations), will be a policy. Operation specific options are based to each operations method.

The implementation so far

The PR (as first submitted up to 22fafa2 implements one operation using this new architecture - the CreateDatabase operation. It should therefore hopefully be easy to see how the architecture will work at a high-level. We'll continue to tweak the specific as we go, but we already have sign off from the SDK team that this is the right direction to head in.

Notes

  • This also creates Request and Response types specific to the Azure SDK. Currently these are just wrappers around http crate types but this might eventually change. Having our own types allows for future flexibility.
  • This heavily uses dynamic dispatch. While performance is definitely something we care about, we believe that the flexibility that the policy architecture provides is worth the performance penalty of using dynamic dispatch. I/O is the bottle neck in this library and so this shouldn't be an issue.
  • I've started rearranging the Cosmos SDK to have an operations module which will replace both the requests and responses modules grouping everything together by operation.

@heaths
Copy link
Member

heaths commented Mar 30, 2021

Pipelines are a per-client configuration

Perhaps not in this PR, but we do want to allow for devs to pass in their own policies as well. They've been used for proxying data for some request methods, redirecting to test environments, and likely more.

@rylev rylev marked this pull request as ready for review April 6, 2021 17:59
@ctaggart ctaggart added this to the v0.1 Cosmos milestone Apr 6, 2021
}
}

pub struct Response {
Copy link
Member

Choose a reason for hiding this comment

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

In our various SDKs we also have Response<T> or something like that, which clients would return to wrap the actual response model, but with response information, e.g.:

Response<KeyVaultKey> response = client.GetKey("name");
KeyVaultKey key = response.Value;
Response rawResponse = response.GetRawResponse();

Is this something we should do now, or can we later?

Also, is inner just the body, or the entire response? Might be good to call it body if the former.

Copy link
Contributor

Choose a reason for hiding this comment

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

How other SDKs handle returning the stream? Without reading the stream first it is impossible to convert it into a struct (KeyVaultKey in your example). Is there a specific exception for blob storage?


#[derive(Copy, Clone, Debug)]
pub struct RetryOptions {
num_retries: usize,
Copy link
Member

Choose a reason for hiding this comment

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

At some point we should respect headers sent by the server, for e.g. in .NET (probably same across languages):

        private const string RetryAfterHeaderName = "Retry-After";
        private const string RetryAfterMsHeaderName = "retry-after-ms";
        private const string XRetryAfterMsHeaderName = "x-ms-retry-after-ms";

See https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/src/Pipeline/Internal/RetryPolicy.cs.

Copy link
Member

Choose a reason for hiding this comment

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

BTW: There's a lot of good policies in that folder we should eventually port. Most SDK languages have these or something like them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely, the policies as they are, are really just proofs of concept. We'll need to improve them considerably.

/// TODO
pub fn with_pipeline(
account: String, // TODO: this will eventually be a URL
auth_token: AuthorizationToken,
Copy link
Member

Choose a reason for hiding this comment

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

At some point, this should be credential of type TokenCredential. We have a whole pluggable system on this. Some TokenCredential providers just wrap a field and supply that to a policy, like shared key credentials (api-key, etc.). See https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core/src/AzureKeyCredential.cs for an example.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes good point. Right now the authorization is tightly coupled with the client. We should extract the code and put it in a specific policy.

Comment on lines +1 to +3
pub struct Request {
inner: http::Request<bytes::Bytes>,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed yesterday in our meeting, this will be an enum accepting either a stream (AsyncRead+AsyncSeek) or a bytes::Bytes. We should merge this as it is now and build upon it.

}
}

pub struct Response {
Copy link
Contributor

Choose a reason for hiding this comment

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

How other SDKs handle returning the stream? Without reading the stream first it is impossible to convert it into a struct (KeyVaultKey in your example). Is there a specific exception for blob storage?

/// TODO
pub fn with_pipeline(
account: String, // TODO: this will eventually be a URL
auth_token: AuthorizationToken,
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes good point. Right now the authorization is tightly coupled with the client. We should extract the code and put it in a specific policy.

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