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

Add scope of waitForCompletion in LRO #29033

Merged
merged 15 commits into from
Jun 22, 2022
40 changes: 29 additions & 11 deletions sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ namespace Azure.Core
internal abstract class OperationInternalBase
{
private readonly ClientDiagnostics _diagnostics;
private readonly string _updateStatusScopeName;
private readonly IReadOnlyDictionary<string, string>? _scopeAttributes;
private readonly DelayStrategy? _fallbackStrategy;
private readonly AsyncLockWithValue<Response> _responseLock;

private readonly string? _waitForCompletionResponseScopeName;
protected readonly string? _updateStatusScopeName;
protected readonly string? _waitForCompletionScopeName;

protected OperationInternalBase(Response rawResponse)
{
_diagnostics = new ClientDiagnostics(ClientOptions.Default);
_updateStatusScopeName = string.Empty;
_scopeAttributes = default;
_fallbackStrategy = default;
_responseLock = new AsyncLockWithValue<Response>(rawResponse);
Expand All @@ -33,6 +35,8 @@ protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string oper
{
_diagnostics = clientDiagnostics;
_updateStatusScopeName = $"{operationTypeName}.UpdateStatus";
_waitForCompletionResponseScopeName = $"{operationTypeName}.WaitForCompletionResponse";
_waitForCompletionScopeName = $"{operationTypeName}.WaitForCompletion";
_scopeAttributes = scopeAttributes?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_fallbackStrategy = fallbackStrategy;
_responseLock = new AsyncLockWithValue<Response>();
Expand Down Expand Up @@ -178,7 +182,7 @@ public Response WaitForCompletionResponse(CancellationToken cancellationToken)
public Response WaitForCompletionResponse(TimeSpan pollingInterval, CancellationToken cancellationToken)
=> WaitForCompletionResponseAsync(async: false, pollingInterval, cancellationToken).EnsureCompleted();

private async ValueTask<Response> WaitForCompletionResponseAsync(bool async, TimeSpan? pollingInterval, CancellationToken cancellationToken)
protected async ValueTask<Response> WaitForCompletionResponseAsync(bool async, TimeSpan? pollingInterval, CancellationToken cancellationToken)
{
// If _responseLock has the value, lockOrValue will contain that value, and no lock is acquired.
// If _responseLock doesn't have the value, GetLockOrValueAsync will acquire the lock that will be released when lockOrValue is disposed
Expand All @@ -188,20 +192,29 @@ private async ValueTask<Response> WaitForCompletionResponseAsync(bool async, Tim
return lockOrValue.Value;
}

var poller = new OperationPoller(_fallbackStrategy);
var response = async
? await poller.WaitForCompletionResponseAsync(this, pollingInterval, cancellationToken).ConfigureAwait(false)
: poller.WaitForCompletionResponse(this, pollingInterval, cancellationToken);
using var scope = CreateScope(_waitForCompletionResponseScopeName!);
try
{
var poller = new OperationPoller(_fallbackStrategy);
var response = async
? await poller.WaitForCompletionResponseAsync(this, pollingInterval, cancellationToken).ConfigureAwait(false)
: poller.WaitForCompletionResponse(this, pollingInterval, cancellationToken);

lockOrValue.SetValue(response);
return response;
lockOrValue.SetValue(response);
return response;
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}

protected abstract ValueTask<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken);

protected DiagnosticScope CreateScope()
protected DiagnosticScope CreateScope(string scopeName)
{
DiagnosticScope scope = _diagnostics.CreateScope(_updateStatusScopeName);
DiagnosticScope scope = _diagnostics.CreateScope(scopeName);

if (_scopeAttributes != null)
{
Expand All @@ -218,5 +231,10 @@ protected DiagnosticScope CreateScope()
protected async ValueTask<RequestFailedException> CreateException(bool async, Response response) => async
? await _diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false)
: _diagnostics.CreateRequestFailedException(response);

protected bool TryGetResponseValue(out Response? value)
{
return _responseLock.TryGetValue(out value);
}
}
}
40 changes: 25 additions & 15 deletions sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,7 @@ public T Value
/// <returns>The last HTTP response received from the server, including the final result of the long-running operation.</returns>
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
public async ValueTask<Response<T>> WaitForCompletionAsync(CancellationToken cancellationToken)
{
var rawResponse = await WaitForCompletionResponseAsync(cancellationToken).ConfigureAwait(false);
return Response.FromValue(Value, rawResponse);
}
=> await WaitForCompletionAsync(async: true, null, cancellationToken).ConfigureAwait(false);

/// <summary>
/// Periodically calls <see cref="OperationInternalBase.UpdateStatusAsync(CancellationToken)"/> until the long-running operation completes. The interval
Expand All @@ -198,10 +195,7 @@ public async ValueTask<Response<T>> WaitForCompletionAsync(CancellationToken can
/// <returns>The last HTTP response received from the server, including the final result of the long-running operation.</returns>
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
public async ValueTask<Response<T>> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken)
{
var rawResponse = await WaitForCompletionResponseAsync(pollingInterval, cancellationToken).ConfigureAwait(false);
return Response.FromValue(Value, rawResponse);
}
=> await WaitForCompletionAsync(async: true, pollingInterval, cancellationToken).ConfigureAwait(false);

/// <summary>
/// Periodically calls <see cref="OperationInternalBase.UpdateStatus(CancellationToken)"/> until the long-running operation completes.
Expand All @@ -220,10 +214,7 @@ public async ValueTask<Response<T>> WaitForCompletionAsync(TimeSpan pollingInter
/// <returns>The last HTTP response received from the server, including the final result of the long-running operation.</returns>
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
public Response<T> WaitForCompletion(CancellationToken cancellationToken)
{
var rawResponse = WaitForCompletionResponse(cancellationToken);
return Response.FromValue(Value, rawResponse);
}
=> WaitForCompletionAsync(async: false, null, cancellationToken).EnsureCompleted();

/// <summary>
/// Periodically calls <see cref="OperationInternalBase.UpdateStatus(CancellationToken)"/> until the long-running operation completes. The interval
Expand All @@ -244,9 +235,28 @@ public Response<T> WaitForCompletion(CancellationToken cancellationToken)
/// <returns>The last HTTP response received from the server, including the final result of the long-running operation.</returns>
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
public Response<T> WaitForCompletion(TimeSpan pollingInterval, CancellationToken cancellationToken)
=> WaitForCompletionAsync(async: false, pollingInterval, cancellationToken).EnsureCompleted();

private async ValueTask<Response<T>> WaitForCompletionAsync(bool async, TimeSpan? pollingInterval, CancellationToken cancellationToken)
{
var rawResponse = WaitForCompletionResponse(pollingInterval, cancellationToken);
return Response.FromValue(Value, rawResponse);
if (TryGetResponseValue(out var rawResponse))
{
return Response.FromValue(Value, rawResponse!);
}

using var scope = CreateScope(_waitForCompletionScopeName!);
try
{
rawResponse = async
? await WaitForCompletionResponseAsync(async: true, pollingInterval, cancellationToken).ConfigureAwait(false)
: WaitForCompletionResponseAsync(async: false, pollingInterval, cancellationToken).EnsureCompleted();
return Response.FromValue(Value, rawResponse);
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}

protected override async ValueTask<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken)
Expand All @@ -260,7 +270,7 @@ protected override async ValueTask<Response> UpdateStatusAsync(bool async, Cance
return GetResponseFromState(asyncLock.Value);
}

using var scope = CreateScope();
using var scope = CreateScope(_updateStatusScopeName!);
try
{
var state = await _operation.UpdateStateAsync(async, cancellationToken).ConfigureAwait(false);
Expand Down
81 changes: 80 additions & 1 deletion sdk/core/Azure.Core/tests/OperationInternalOfTTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Azure.Core.Tests
public class OperationInternalOfTTests
{
private static readonly string DiagnosticNamespace = "Azure.Core.Tests";
private static readonly ClientDiagnostics ClientDiagnostics = new(new TestClientOptions());
private static readonly ClientDiagnostics ClientDiagnostics = new(new TestClientOptions(), true);
private static readonly RequestFailedException OriginalException = new("");
private static readonly StackOverflowException CustomException = new();
private static readonly MockResponse InitialResponse = new(200);
Expand Down Expand Up @@ -148,6 +148,19 @@ public async Task UpdateStatusCreatesDiagnosticScope([Values(true, false)] bool
testListener.AssertScope($"{expectedTypeName}.UpdateStatus", expectedAttributes);
}

[Test]
public async Task UpdateStatusNotCreateDiagnosticScope([Values(true, false)] bool async)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

var operationInternal = OperationInternal<int>.Succeeded(InitialResponse, 1);
_ = async
? await operationInternal.UpdateStatusAsync(CancellationToken.None)
: operationInternal.UpdateStatus(CancellationToken.None);

CollectionAssert.IsEmpty(testListener.Scopes);
}

[Test]
public async Task UpdateStatusSetsFailedScopeWhenOperationFails([Values(true, false)] bool async)
{
Expand Down Expand Up @@ -205,6 +218,72 @@ public async Task UpdateStatusPassesTheCancellationTokenToUpdateState([Values(tr
Assert.AreEqual(originalToken, passedToken);
}

[Test]
public async Task WaitForCompletionResponseCreatesDiagnosticScope([Values(true, false)] bool async, [Values(null, "CustomTypeName")] string operationTypeName)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

string expectedTypeName = operationTypeName ?? nameof(TestOperation);
KeyValuePair<string, string>[] expectedAttributes = { new("key1", "value1"), new("key2", "value2") };
var operationInternal = new OperationInternal<int>(ClientDiagnostics, TestOperation.Succeeded(1), InitialResponse, operationTypeName, expectedAttributes);

_ = async
? await operationInternal.WaitForCompletionResponseAsync(CancellationToken.None)
: operationInternal.WaitForCompletionResponse(CancellationToken.None);

testListener.AssertScope($"{expectedTypeName}.WaitForCompletionResponse", expectedAttributes);
#if NET5_0_OR_GREATER
testListener.AssertAndRemoveScope($"{expectedTypeName}.WaitForCompletionResponse", expectedAttributes);
CollectionAssert.IsEmpty(testListener.Scopes);
#endif
}

[Test]
public async Task WaitForCompletionCreatesDiagnosticScope([Values(true, false)] bool async, [Values(null, "CustomTypeName")] string operationTypeName)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

string expectedTypeName = operationTypeName ?? nameof(TestOperation);
KeyValuePair<string, string>[] expectedAttributes = { new("key1", "value1"), new("key2", "value2") };
var operationInternal = new OperationInternal<int>(ClientDiagnostics, TestOperation.Succeeded(1), InitialResponse, operationTypeName, expectedAttributes);

_ = async
? await operationInternal.WaitForCompletionAsync(CancellationToken.None)
: operationInternal.WaitForCompletion(CancellationToken.None);

testListener.AssertScope($"{expectedTypeName}.WaitForCompletion", expectedAttributes);
#if NET5_0_OR_GREATER
testListener.AssertAndRemoveScope($"{expectedTypeName}.WaitForCompletion", expectedAttributes);
CollectionAssert.IsEmpty(testListener.Scopes);
#endif
}

[Test]
public async Task WaitForCompletionResponseNotCreateDiagnosticScope([Values(true, false)] bool async)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

var operationInternal = OperationInternal<int>.Succeeded(InitialResponse, 1);
_ = async
? await operationInternal.WaitForCompletionResponseAsync(CancellationToken.None)
: operationInternal.WaitForCompletionResponse(CancellationToken.None);

CollectionAssert.IsEmpty(testListener.Scopes);
}

[Test]
public async Task WaitForCompletionNotCreateDiagnosticScope([Values(true, false)] bool async)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

var operationInternal = OperationInternal<int>.Succeeded(InitialResponse, 1);
_ = async
? await operationInternal.WaitForCompletionAsync(CancellationToken.None)
: operationInternal.WaitForCompletion(CancellationToken.None);

CollectionAssert.IsEmpty(testListener.Scopes);
}

[Test]
public async Task WaitForCompletionCallsUntilOperationCompletes([Values(true, false)] bool useDefaultPollingInterval)
{
Expand Down
48 changes: 47 additions & 1 deletion sdk/core/Azure.Core/tests/OperationInternalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Azure.Core.Tests
public class OperationInternalTests
{
private static readonly string DiagnosticNamespace = "Azure.Core.Tests";
private static readonly ClientDiagnostics ClientDiagnostics = new(new TestClientOptions());
private static readonly ClientDiagnostics ClientDiagnostics = new(new TestClientOptions(), true);
private static readonly RequestFailedException OriginalException = new("");
private static readonly StackOverflowException CustomException = new();
private static readonly MockResponse InitialResponse = new(200);
Expand Down Expand Up @@ -130,6 +130,19 @@ public async Task UpdateStatusCreatesDiagnosticScope([Values(true, false)] bool
testListener.AssertScope($"{expectedTypeName}.UpdateStatus", expectedAttributes);
}

[Test]
public async Task UpdateStatusNotCreateDiagnosticScope([Values(true, false)] bool async)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

var operationInternal = OperationInternal.Succeeded(InitialResponse);
_ = async
? await operationInternal.UpdateStatusAsync(CancellationToken.None)
: operationInternal.UpdateStatus(CancellationToken.None);

CollectionAssert.IsEmpty(testListener.Scopes);
}

[Test]
public async Task UpdateStatusSetsFailedScopeWhenOperationFails([Values(true, false)] bool async)
{
Expand Down Expand Up @@ -187,6 +200,39 @@ public async Task UpdateStatusPassesTheCancellationTokenToUpdateState([Values(tr
Assert.AreEqual(originalToken, passedToken);
}

[Test]
public async Task WaitForCompletionResponseCreatesDiagnosticScope([Values(true, false)] bool async, [Values(null, "CustomTypeName")] string operationTypeName)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

string expectedTypeName = operationTypeName ?? nameof(TestOperation);
KeyValuePair<string, string>[] expectedAttributes = { new("key1", "value1"), new("key2", "value2") };
var operationInternal = new OperationInternal(ClientDiagnostics, TestOperation.Succeeded(), InitialResponse, operationTypeName, expectedAttributes);

_ = async
? await operationInternal.WaitForCompletionResponseAsync(CancellationToken.None)
: operationInternal.WaitForCompletionResponse(CancellationToken.None);

testListener.AssertScope($"{expectedTypeName}.WaitForCompletionResponse", expectedAttributes);
#if NET5_0_OR_GREATER
testListener.AssertAndRemoveScope($"{expectedTypeName}.WaitForCompletionResponse", expectedAttributes);
CollectionAssert.IsEmpty(testListener.Scopes);
#endif
}

[Test]
public async Task WaitForCompletionResponseNotCreateDiagnosticScope([Values(true, false)] bool async)
{
using ClientDiagnosticListener testListener = new(DiagnosticNamespace);

var operationInternal = OperationInternal.Succeeded(InitialResponse);
_ = async
? await operationInternal.WaitForCompletionResponseAsync(CancellationToken.None)
: operationInternal.WaitForCompletionResponse(CancellationToken.None);

CollectionAssert.IsEmpty(testListener.Scopes);
}

[Test]
public async Task WaitForCompletionCallsUntilOperationCompletes([Values(true, false)] bool useDefaultPollingInterval)
{
Expand Down