-
Couldn't load subscription status.
- Fork 5.1k
Update Azure.Core shared for LRO rehydration #41088
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
Changes from 53 commits
7613a82
108ada7
9518c51
bfd7490
6a5e480
6f484c5
f4cdc1e
2b9e1aa
74631b9
a505ca0
125ee0e
b4bb5e0
9bcc44e
f69fe9a
09eca6b
82448ef
cc68617
b425271
74183b1
1530b50
ddc3f57
5581f11
d04a39b
f15eb00
d715ab9
4e516e0
9aff37d
c40f52a
fcd3930
087a939
f7a5a3d
fc61827
db4f492
2e2be26
fa305a2
4c5f363
09e4e8e
2826256
4826528
9611c70
d9d5e60
be63fdc
5992a97
29c0169
2ec80f6
b16386d
0128166
0550657
074053a
5cc74bc
5e1a429
a9f216e
0d9b36f
98e3f91
bb06aec
107ecfb
6159674
c22da87
842fc6b
051a7de
819583a
b85e8ac
b76e9ea
6ebbc5f
07b5c18
ce8f1e3
53ebcf5
21664c7
79383b8
04b8769
3801741
eab4f25
8448de0
3e71f0a
a56b957
02b735c
90d3485
e4379c5
dc90663
4e88474
a8b7c0a
cdf4310
6bc5760
bd09a8b
0e1f069
dd2ee89
2195045
ac6d6ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,10 @@ | |
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.ClientModel.Primitives; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.IO; | ||
| using System.Reflection; | ||
| using System.Text.Json; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
@@ -38,7 +40,7 @@ public static IOperation Create( | |
| bool skipApiVersionOverride = false, | ||
| string? apiVersionOverrideValue = null) | ||
| { | ||
| string? apiVersionStr = null; | ||
| string? apiVersionStr; | ||
| if (apiVersionOverrideValue is not null) | ||
| { | ||
| apiVersionStr = apiVersionOverrideValue; | ||
|
|
@@ -70,6 +72,41 @@ public static IOperation<T> Create<T>( | |
| return new OperationToOperationOfT<T>(operationSource, operation); | ||
| } | ||
|
|
||
| public static IOperation Create( | ||
jsquire marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| HttpPipeline pipeline, | ||
| RehydrationToken? rehydrationToken, | ||
| string? apiVersionOverride = null) | ||
| { | ||
| Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); | ||
jsquire marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| var lroDetails = ModelReaderWriter.Write(rehydrationToken!, new ModelReaderWriterOptions("J")).ToObjectFromJson<Dictionary<string, string>>(); | ||
live1206 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (!Uri.TryCreate(lroDetails["initialUri"], UriKind.Absolute, out var startRequestUri)) | ||
| throw new InvalidOperationException("Invalid initial URI"); | ||
|
||
| if (!lroDetails.TryGetValue("nextRequestUri", out var nextRequestUri)) | ||
| throw new InvalidOperationException("Invalid next request URI"); | ||
jsquire marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| RequestMethod requestMethod = new RequestMethod(lroDetails["requestMethod"]); | ||
| string lastKnownLocation = lroDetails["lastKnownLocation"]; | ||
| if (!Enum.TryParse(lroDetails["finalStateVia"], out OperationFinalStateVia finalStateVia)) | ||
| finalStateVia = OperationFinalStateVia.Location; | ||
| string? apiVersionStr = apiVersionOverride ?? (TryGetApiVersion(startRequestUri, out ReadOnlySpan<char> apiVersion) ? apiVersion.ToString() : null); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we throw if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following with the existing logic here. Not sure if there are cases api-version missing from the initial Uri. |
||
| if (!Enum.TryParse(lroDetails["headerSource"], out HeaderSource headerSource)) | ||
live1206 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| headerSource = HeaderSource.None; | ||
|
|
||
| return new NextLinkOperationImplementation(pipeline, requestMethod, startRequestUri, nextRequestUri, headerSource, lastKnownLocation, finalStateVia, apiVersionStr); | ||
| } | ||
|
|
||
| public static IOperation<T> Create<T>( | ||
| IOperationSource<T> operationSource, | ||
| HttpPipeline pipeline, | ||
| RehydrationToken? rehydrationToken, | ||
| string? apiVersionOverride = null) | ||
| { | ||
| Argument.AssertNotNull(rehydrationToken, nameof(rehydrationToken)); | ||
jsquire marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| var operation = Create(pipeline, rehydrationToken, apiVersionOverride); | ||
| return new OperationToOperationOfT<T>(operationSource, operation); | ||
| } | ||
|
|
||
| private NextLinkOperationImplementation( | ||
| HttpPipeline pipeline, | ||
| RequestMethod requestMethod, | ||
|
|
@@ -90,6 +127,41 @@ private NextLinkOperationImplementation( | |
| _apiVersion = apiVersion; | ||
| } | ||
|
|
||
| public static RehydrationToken GetRehydrationToken( | ||
| RequestMethod requestMethod, | ||
| Uri startRequestUri, | ||
| Response response, | ||
| OperationFinalStateVia finalStateVia, | ||
| bool skipApiVersionOverride = false, | ||
| string? apiVersionOverrideValue = null) | ||
| { | ||
| string? apiVersionStr; | ||
| if (apiVersionOverrideValue is not null) | ||
| { | ||
| apiVersionStr = apiVersionOverrideValue; | ||
| } | ||
| else | ||
| { | ||
| apiVersionStr = !skipApiVersionOverride && TryGetApiVersion(startRequestUri, out ReadOnlySpan<char> apiVersion) ? apiVersion.ToString() : null; | ||
| } | ||
| var headerSource = GetHeaderSource(requestMethod, startRequestUri, response, apiVersionStr, out var nextRequestUri); | ||
| response.Headers.TryGetValue("Location", out var lastKnownLocation); | ||
| return GetRehydrationToken(requestMethod, startRequestUri, nextRequestUri, headerSource.ToString(), lastKnownLocation, finalStateVia.ToString()); | ||
| } | ||
|
|
||
| public static RehydrationToken GetRehydrationToken( | ||
| RequestMethod requestMethod, | ||
| Uri startRequestUri, | ||
| string nextRequestUri, | ||
| string headerSource, | ||
| string? lastKnownLocation, | ||
| string finalStateVia) | ||
| { | ||
| var parameters = new object?[] { null, null, headerSource, nextRequestUri, startRequestUri.AbsoluteUri, requestMethod, lastKnownLocation, finalStateVia }; | ||
| var rehydrationToken = Activator.CreateInstance(typeof(RehydrationToken), BindingFlags.NonPublic | BindingFlags.Instance, null, parameters, null); | ||
jsquire marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return (RehydrationToken)rehydrationToken!; | ||
| } | ||
|
|
||
| public async ValueTask<OperationState> UpdateStateAsync(bool async, CancellationToken cancellationToken) | ||
| { | ||
| Response response = await GetResponseAsync(async, _nextRequestUri, cancellationToken).ConfigureAwait(false); | ||
|
|
@@ -307,7 +379,7 @@ private static bool IsFinalState(Response response, HeaderSource headerSource, o | |
|
|
||
| if (response.Status is >= 200 and <= 204) | ||
| { | ||
| if (response.ContentStream is {Length: > 0}) | ||
| if (response.ContentStream is { Length: > 0 }) | ||
live1206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| try | ||
| { | ||
|
|
@@ -399,7 +471,7 @@ private static HeaderSource GetHeaderSource(RequestMethod requestMethod, Uri req | |
| return HeaderSource.None; | ||
| } | ||
|
|
||
| private enum HeaderSource | ||
| internal enum HeaderSource | ||
jsquire marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| None, | ||
| OperationLocation, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,14 +51,16 @@ internal class OperationInternal : OperationInternalBase | |
| /// Initializes a new instance of the <see cref="OperationInternal"/> class in a final successful state. | ||
| /// </summary> | ||
| /// <param name="rawResponse">The final value of <see cref="OperationInternalBase.RawResponse"/>.</param> | ||
| public static OperationInternal Succeeded(Response rawResponse) => new(OperationState.Success(rawResponse)); | ||
| /// <param name="rehydrationToken">The rehydration token.</param> | ||
| public static OperationInternal Succeeded(Response rawResponse, RehydrationToken? rehydrationToken = null) => new(OperationState.Success(rawResponse), rehydrationToken); | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="OperationInternal"/> class in a final failed state. | ||
| /// </summary> | ||
| /// <param name="rawResponse">The final value of <see cref="OperationInternalBase.RawResponse"/>.</param> | ||
| /// <param name="operationFailedException">The exception that will be thrown by <c>UpdateStatusAsync</c>.</param> | ||
| public static OperationInternal Failed(Response rawResponse, RequestFailedException operationFailedException) => new(OperationState.Failure(rawResponse, operationFailedException)); | ||
| /// <param name="rehydrationToken">rehydration token</param> | ||
| public static OperationInternal Failed(Response rawResponse, RequestFailedException operationFailedException, RehydrationToken? rehydrationToken = null) => new(OperationState.Failure(rawResponse, operationFailedException), rehydrationToken); | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="OperationInternal"/> class. | ||
|
|
@@ -83,23 +85,25 @@ internal class OperationInternal : OperationInternalBase | |
| /// </param> | ||
| /// <param name="scopeAttributes">The attributes to use during diagnostic scope creation.</param> | ||
| /// <param name="fallbackStrategy"> The delay strategy to use. Default is <see cref="FixedDelayWithNoJitterStrategy"/>.</param> | ||
| /// <param name="rehydrationToken">The rehydration token.</param> | ||
| public OperationInternal(IOperation operation, | ||
| ClientDiagnostics clientDiagnostics, | ||
| Response rawResponse, | ||
| Response? rawResponse, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What scenario motivates passing a null raw response? It looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the case of LRO rehydration used here, the user doesn't have response in place since the operation is not complete. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created #42666 for this change. |
||
| string? operationTypeName = null, | ||
| IEnumerable<KeyValuePair<string, string>>? scopeAttributes = null, | ||
| DelayStrategy? fallbackStrategy = null) | ||
| :base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy) | ||
| DelayStrategy? fallbackStrategy = null, | ||
| RehydrationToken? rehydrationToken = null) | ||
| : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, rehydrationToken) | ||
| { | ||
| _internalOperation = new OperationInternal<VoidValue>(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy); | ||
| _internalOperation = new OperationInternal<VoidValue>(new OperationToOperationOfTProxy(operation), clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, rehydrationToken); | ||
| } | ||
|
|
||
| private OperationInternal(OperationState finalState) | ||
| :base(finalState.RawResponse) | ||
| private OperationInternal(OperationState finalState, RehydrationToken? rehydrationToken) | ||
| : base(finalState.RawResponse, rehydrationToken) | ||
| { | ||
| _internalOperation = finalState.HasSucceeded | ||
| ? OperationInternal<VoidValue>.Succeeded(finalState.RawResponse, default) | ||
| : OperationInternal<VoidValue>.Failed(finalState.RawResponse, finalState.OperationFailedException!); | ||
| ? OperationInternal<VoidValue>.Succeeded(finalState.RawResponse, default, rehydrationToken) | ||
| : OperationInternal<VoidValue>.Failed(finalState.RawResponse, finalState.OperationFailedException!, rehydrationToken); | ||
| } | ||
|
|
||
| public override Response RawResponse => _internalOperation.RawResponse; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,10 @@ | |
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.ClientModel.Primitives; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Net; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Azure.Core.Pipeline; | ||
|
|
@@ -53,21 +56,23 @@ internal class OperationInternal<T> : OperationInternalBase | |
| { | ||
| private readonly IOperation<T> _operation; | ||
| private readonly AsyncLockWithValue<OperationState<T>> _stateLock; | ||
| private Response _rawResponse; | ||
| private Response? _rawResponse; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="OperationInternal"/> class in a final successful state. | ||
| /// </summary> | ||
| /// <param name="rawResponse">The final value of <see cref="OperationInternalBase.RawResponse"/>.</param> | ||
| /// <param name="value">The final result of the long-running operation.</param> | ||
| public static OperationInternal<T> Succeeded(Response rawResponse, T value) => new(OperationState<T>.Success(rawResponse, value)); | ||
| /// <param name="rehydrationToken">rehydration token</param> | ||
| public static OperationInternal<T> Succeeded(Response rawResponse, T value, RehydrationToken? rehydrationToken = null) => new(OperationState<T>.Success(rawResponse, value), rehydrationToken); | ||
jsquire marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="OperationInternal"/> class in a final failed state. | ||
| /// </summary> | ||
| /// <param name="rawResponse">The final value of <see cref="OperationInternalBase.RawResponse"/>.</param> | ||
| /// <param name="operationFailedException">The exception that will be thrown by <c>UpdateStatusAsync</c>.</param> | ||
| public static OperationInternal<T> Failed(Response rawResponse, RequestFailedException operationFailedException) => new(OperationState<T>.Failure(rawResponse, operationFailedException)); | ||
| /// <param name="rehydrationToken">rehydration token</param> | ||
| public static OperationInternal<T> Failed(Response rawResponse, RequestFailedException operationFailedException, RehydrationToken? rehydrationToken = null) => new(OperationState<T>.Failure(rawResponse, operationFailedException), rehydrationToken); | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="OperationInternal{T}"/> class. | ||
|
|
@@ -93,21 +98,23 @@ internal class OperationInternal<T> : OperationInternalBase | |
| /// <param name="scopeAttributes">The attributes to use during diagnostic scope creation.</param> | ||
| /// <param name="fallbackStrategy">The delay strategy when Retry-After header is not present. When it is present, the longer of the two delays will be used. | ||
| /// Default is <see cref="FixedDelayWithNoJitterStrategy"/>.</param> | ||
| /// <param name="rehydrationToken">The rehydration token.</param> | ||
| public OperationInternal(IOperation<T> operation, | ||
| ClientDiagnostics clientDiagnostics, | ||
| Response rawResponse, | ||
| Response? rawResponse, | ||
| string? operationTypeName = null, | ||
| IEnumerable<KeyValuePair<string, string>>? scopeAttributes = null, | ||
| DelayStrategy? fallbackStrategy = null) | ||
| : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy) | ||
| DelayStrategy? fallbackStrategy = null, | ||
| RehydrationToken? rehydrationToken = null) | ||
| : base(clientDiagnostics, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy, rehydrationToken) | ||
| { | ||
| _operation = operation; | ||
| _rawResponse = rawResponse; | ||
| _stateLock = new AsyncLockWithValue<OperationState<T>>(); | ||
| } | ||
|
|
||
| private OperationInternal(OperationState<T> finalState) | ||
| : base(finalState.RawResponse) | ||
| private OperationInternal(OperationState<T> finalState, RehydrationToken? rehydrationToken) | ||
| : base(finalState.RawResponse, rehydrationToken) | ||
| { | ||
| // FinalOperation represents operation that is in final state and can't be updated. | ||
| // It implements IOperation<T> and throws exception when UpdateStateAsync is called. | ||
|
|
@@ -116,7 +123,7 @@ private OperationInternal(OperationState<T> finalState) | |
| _stateLock = new AsyncLockWithValue<OperationState<T>>(finalState); | ||
| } | ||
|
|
||
| public override Response RawResponse => _stateLock.TryGetValue(out var state) ? state.RawResponse : _rawResponse; | ||
| public override Response RawResponse => (_stateLock.TryGetValue(out var state) ? state.RawResponse : _rawResponse) ?? throw new InvalidOperationException("The operation does not have a response yet. Please call UpdateStatus or WaitForCompletion first."); | ||
|
||
|
|
||
| public override bool HasCompleted => _stateLock.HasValue; | ||
|
|
||
|
|
@@ -265,7 +272,7 @@ protected override async ValueTask<Response> UpdateStatusAsync(bool async, Cance | |
| } | ||
|
|
||
| asyncLock.SetValue(state); | ||
| return GetResponseFromState(state); | ||
| return GetResponseFromState(state, GetRequestMethod(_rehydrationToken)); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
|
|
@@ -274,16 +281,89 @@ protected override async ValueTask<Response> UpdateStatusAsync(bool async, Cance | |
| } | ||
| } | ||
|
|
||
| private static Response GetResponseFromState(OperationState<T> state) | ||
| private RequestMethod? GetRequestMethod(RehydrationToken? rehydrationToken) | ||
| { | ||
| if (rehydrationToken is null) | ||
| { | ||
| return null; | ||
| } | ||
| var lroDetails = ModelReaderWriter.Write(rehydrationToken, new ModelReaderWriterOptions("J")).ToObjectFromJson<Dictionary<string, string>>(); | ||
| return new RequestMethod(lroDetails["requestMethod"]); | ||
| } | ||
|
|
||
| private static Response GetResponseFromState(OperationState<T> state, RequestMethod? requestmethod = null) | ||
| { | ||
| if (state.HasSucceeded) | ||
| { | ||
| return state.RawResponse; | ||
| } | ||
|
|
||
| // if this is a fake delete lro with 404, just return empty response with 200 | ||
live1206 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (RequestMethod.Delete == requestmethod && state.RawResponse.Status == 404) | ||
| { | ||
| return new EmptyResponse(HttpStatusCode.OK); | ||
live1206 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| throw state.OperationFailedException!; | ||
| } | ||
|
|
||
| private class EmptyResponse : Response | ||
|
||
| { | ||
| public EmptyResponse(HttpStatusCode status) | ||
| { | ||
| Status = (int)status; | ||
| ReasonPhrase = status.ToString(); | ||
| } | ||
|
|
||
| public override int Status { get; } | ||
|
|
||
| public override string ReasonPhrase { get; } | ||
|
|
||
| public override Stream? ContentStream { get => null; set => throw new System.NotImplementedException(); } | ||
| public override string ClientRequestId { get => string.Empty; set => throw new System.NotImplementedException(); } | ||
|
|
||
| public override void Dispose() | ||
| { | ||
| throw new System.NotImplementedException(); | ||
live1206 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| #if HAS_INTERNALS_VISIBLE_CORE | ||
| internal | ||
| #endif | ||
| protected override bool ContainsHeader(string name) | ||
| { | ||
| throw new System.NotImplementedException(); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| #if HAS_INTERNALS_VISIBLE_CORE | ||
| internal | ||
| #endif | ||
| protected override IEnumerable<HttpHeader> EnumerateHeaders() | ||
| { | ||
| throw new System.NotImplementedException(); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| #if HAS_INTERNALS_VISIBLE_CORE | ||
| internal | ||
| #endif | ||
| protected override bool TryGetHeader(string name, out string value) | ||
| { | ||
| throw new System.NotImplementedException(); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| #if HAS_INTERNALS_VISIBLE_CORE | ||
| internal | ||
| #endif | ||
| protected override bool TryGetHeaderValues(string name, out IEnumerable<string> values) | ||
| { | ||
| throw new System.NotImplementedException(); | ||
| } | ||
| } | ||
|
|
||
| private class FinalOperation : IOperation<T> | ||
| { | ||
| public ValueTask<OperationState<T>> UpdateStateAsync(bool async, CancellationToken cancellationToken) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.