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

Board Review: Azure.Core features for LLC/codegen project #3391

Closed
lmazuel opened this issue Sep 21, 2021 · 7 comments
Closed

Board Review: Azure.Core features for LLC/codegen project #3391

lmazuel opened this issue Sep 21, 2021 · 7 comments
Assignees
Labels
architecture board-review Request for an Architectural Board Review

Comments

@lmazuel
Copy link
Member

lmazuel commented Sep 21, 2021

Context

The LLC project required some improvement in Azure.Core that are today in Experimental package or preview, that we need to review to GA the associated Azure.Core. This is a necessary step in the story of enabling LLC-based SDK to go GA (we can't GA a LLC based service team that uses previews of Azure.Core).

.NET

Java

Python

TypeScript

@lmazuel lmazuel added architecture board-review Request for an Architectural Board Review labels Sep 21, 2021
@lilyjma
Copy link
Contributor

lilyjma commented Oct 14, 2021

scheduled for 10/25 2-4pm pst

@tg-msft
Copy link
Member

tg-msft commented Oct 25, 2021

An issue with RequestOptions.CancellationToken

Evolution (LLC => HLC)

I think there's a plan to evolve LLC methods into HLC methods by taking a signature like:

public class FooClient
{
    public Response DoFoo(string query, string header, RequestContent body, RequestOptions options = default);
}

and adding a strongly-typed overload right next to it like:

public class FooClient
{
    public Response DoFoo(string query, string header, RequestContent body, RequestOptions options = default);

    public Response<FooModel> DoFoo(FooOptions options, CancellationToken cancellationToken = default) =>
        DoFoo(
            options?.Query,
            options?.Header,
            RequestContent.Create(options?.Body),
            new RequestOptions { CancellationToken = cancellationToken});
}

[JsonConverter(...)]
public class FooModel { ... } 

public class FooOptions
{
    public FooModel Body { get; set; }   
    public string Query { get; set; }
    public string Header { get; set; }
}

The benefit of this approach is body-less methods can make RequestOptions required so the compiler is able to bind correctly between the different LLC/HLC overloads.

Devolution (HLC + RequestOptions)

RequestOptions adds a number of important features like customizing error behavior and per-invocation pipeline policies. We want that for our Wave 1 HLCs like Storage and KeyVault too. There's currently no plan to update all the handcrafted artisinal client libraries to use RequestOptions, but we'll support it on a case by case basis using customer feedback.

BlobClient.DeleteIfExists is a good example where we'd like to start using this today. We have a problem where 404s from DeleteIfExists pollute the tracing data collected in Azure Monitor. The WebJobs extensions for Azure Functions cause more of the same. We'd like to make use of RequestOptions to provide a per call classifier on select methods to work around this.

Option 1: Just add RequestOptions

The naïve approach would be simply adding an overload taking RequestOptions for all the power of an LLC method:

public class BlobClient
{
    // Current method:
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption = None,
        BlobRequestConditions conditions = default,
        CancellationToken cancellationToken = default);

    // New overload:
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption,
        BlobRequestConditions conditions,
        RequestOptions options,
        CancellationToken cancellationToken);
}

First I have to make all the parameters required so the compiler will correctly bind the empty parameter list. But then what does it mean when I pass both requestOptions.CancellationToken and the cancellationToken parameter? I think the only clean but potentially not very cheap solution is to create another CancellationTokenSource to link the two of them together. This just feels messy and like we designed RequestOptions without caring up about the HLC devolution story.

Option 2: Add an LLC method

We could add the LLC method as if we'd started there two years ago:

public class BlobClient
{
    // Current method:
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption = None,
        BlobRequestConditions conditions = default,
        CancellationToken cancellationToken = default);

    // New Delete overload instead because that's all we have at the protocol layer
    public virtual Response<bool> Delete(
        RequestOptions options,
        string deleteSnapshots = default,
        string leaseId = default,
        DateTimeOffset? ifModifiedSince = default,
        DateTimeOffset? ifUnmodifiedSince = default,
        ETag ifMatch = default,
        ETag ifNoneMatch = default,
        string ifTags = default,
        BlobDeleteType? blobDeleteType = default);
}

There are too many parameters to make them all required so I moved RequestOptions first in this case. I think this looks pretty awful. It'll be a lot of work for customers to unroll our handwritten models that may have drifted from the protocol as we added convenience.

Option 3: Don't take CancellationToken

We could also add an overload taking RequestOptions but not CancellationToken:

public class BlobClient
{
    // Current method:
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption = None,
        BlobRequestConditions conditions = default,
        CancellationToken cancellationToken = default);

    // New overload:
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption,
        BlobRequestConditions conditions,
        RequestOptions options);
}

The first problem is making it non-optional so the compiler can bind DeleteIfExists() also makes all our other parameters required. It's not a deal breaker, but it's annoying.

Second, I also don't think it's ideal for methods with Options bags. Why wouldn't these RequestOptions also live there instead of a top-level parameter? Here's what this would look like with Download:

public class BlobClient
{
    // Current method:
    public virtual Response<BlobDownloadResult> DownloadContent(
        BlobDownloadOptions options,
        CancellationToken cancellationToken = default);

    // New overload:
    public virtual Response<BlobDownloadResult> DownloadContent(
        BlobDownloadOptions options,
        RequestOptions otherMoreDifferentOptions);
}

public class BlobDownloadOptions
{
    public BlobDownloadOptions();
    public BlobRequestConditions Conditions { get; set; }
    public HttpRange Range { get; set; }
    public DownloadTransactionalHashingOptions TransactionalHashingOptions { get; set; }
}

Option 4: Remove CancellationToken from RequestOptions

This is my preferred approach. It just adds another parameter to Delete:

public class BlobClient
{
    // Hide the current method:
    [EditorBrowsable(EditorBrowsableState.Never)]
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption,
        BlobRequestConditions conditions,
        CancellationToken cancellationToken);

    // New overload that everyone starts seeing:
    public virtual Response<bool> DeleteIfExists(
        DeleteSnapshotsOption snapshotsOption = default,
        BlobRequestConditions conditions = default,
        RequestOptions requestOptions = default,
        CancellationToken cancellationToken = default);
}

And is easily hidden inside Options bags:

public class BlobClient
{
    // No change!
    public virtual Response<BlobDownloadResult> DownloadContent(
        BlobDownloadOptions options,
        CancellationToken cancellationToken = default);
}

public class BlobDownloadOptions
{
    public BlobDownloadOptions();
    public BlobRequestConditions Conditions { get; set; }
    public HttpRange Range { get; set; }
    public DownloadTransactionalHashingOptions TransactionalHashingOptions { get; set; }
    public RequestOptions Request { get; set; }
}

We'd need to think through how splitting off CancellationToken would affect the LLC evolution story, but I don't think it should be very different. You'd still make RequestOptions required which would resolve bindings and then tack an extra CancellationToken on the end.

@tg-msft
Copy link
Member

tg-msft commented Oct 26, 2021

Recording[MS INTERNAL ONLY]

@KrzysztofCwalina
Copy link
Member

The issues you describe exist in methods that don't take a model representing the request content. If the do take it, the protocol overload would take RequestContent and so there would not be an ambiguity of overload resolution.

For these relatively uncommon cases, I would prefer to use the workarounds you outlines, i.e. make existing formal parameters required in protocol overloads and live with the fact that there will be operation specific option bags and request-general option bag (RequestOption).

The reason for the preference is that we will have lots of protocol methods all over the SKD and I would like them to be as simple as possible, and I think it will be relatively uncommon for a method to not take models and have optional formal parameters.

@tg-msft
Copy link
Member

tg-msft commented Nov 1, 2021

Recording[MS INTERNAL ONLY]

@lilyjma
Copy link
Contributor

lilyjma commented Nov 11, 2021

scheduled for 11/15 2-3pm pst

@tg-msft
Copy link
Member

tg-msft commented Nov 15, 2021

Recording[MS INTERNAL ONLY]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
architecture board-review Request for an Architectural Board Review
Projects
None yet
Development

No branches or pull requests

5 participants