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
46 changes: 31 additions & 15 deletions sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@ 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;
_waitForCompletionResponseScopeName = string.Empty;
_waitForCompletionScopeName = string.Empty;
_scopeAttributes = default;
_fallbackStrategy = default;
_responseLock = new AsyncLockWithValue<Response>(rawResponse);
Expand All @@ -32,7 +37,9 @@ protected OperationInternalBase(Response rawResponse)
protected OperationInternalBase(ClientDiagnostics clientDiagnostics, string operationTypeName, IEnumerable<KeyValuePair<string, string>>? scopeAttributes = null, DelayStrategy? fallbackStrategy = null)
{
_diagnostics = clientDiagnostics;
_updateStatusScopeName = $"{operationTypeName}.UpdateStatus";
_updateStatusScopeName = $"{operationTypeName}.{nameof(UpdateStatus)}";
_waitForCompletionResponseScopeName = $"{operationTypeName}.{nameof(WaitForCompletionResponse)}";
_waitForCompletionScopeName = $"{operationTypeName}.WaitForCompletion";
_scopeAttributes = scopeAttributes?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
_fallbackStrategy = fallbackStrategy;
_responseLock = new AsyncLockWithValue<Response>();
Expand Down Expand Up @@ -114,7 +121,7 @@ public Response UpdateStatus(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 async ValueTask<Response> WaitForCompletionResponseAsync(CancellationToken cancellationToken)
=> await WaitForCompletionResponseAsync(async: true, null, cancellationToken).ConfigureAwait(false);
=> await WaitForCompletionResponseAsync(async: true, null, _waitForCompletionResponseScopeName, cancellationToken).ConfigureAwait(false);

/// <summary>
/// Periodically calls <see cref="UpdateStatusAsync(CancellationToken)"/> until the long-running operation completes. The interval
Expand All @@ -135,7 +142,7 @@ public async ValueTask<Response> WaitForCompletionResponseAsync(CancellationToke
/// <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> WaitForCompletionResponseAsync(TimeSpan pollingInterval, CancellationToken cancellationToken)
=> await WaitForCompletionResponseAsync(async: true, pollingInterval, cancellationToken).ConfigureAwait(false);
=> await WaitForCompletionResponseAsync(async: true, pollingInterval, _waitForCompletionResponseScopeName, cancellationToken).ConfigureAwait(false);

/// <summary>
/// Periodically calls <see cref="UpdateStatus(CancellationToken)"/> until the long-running operation completes.
Expand All @@ -155,7 +162,7 @@ public async ValueTask<Response> WaitForCompletionResponseAsync(TimeSpan polling
/// <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 WaitForCompletionResponse(CancellationToken cancellationToken)
=> WaitForCompletionResponseAsync(async: false, null, cancellationToken).EnsureCompleted();
=> WaitForCompletionResponseAsync(async: false, null, _waitForCompletionResponseScopeName, cancellationToken).EnsureCompleted();

/// <summary>
/// Periodically calls <see cref="UpdateStatus(CancellationToken)"/> until the long-running operation completes. The interval
Expand All @@ -176,9 +183,9 @@ public Response WaitForCompletionResponse(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 WaitForCompletionResponse(TimeSpan pollingInterval, CancellationToken cancellationToken)
=> WaitForCompletionResponseAsync(async: false, pollingInterval, cancellationToken).EnsureCompleted();
=> WaitForCompletionResponseAsync(async: false, pollingInterval, _waitForCompletionResponseScopeName, cancellationToken).EnsureCompleted();

private async ValueTask<Response> WaitForCompletionResponseAsync(bool async, TimeSpan? pollingInterval, CancellationToken cancellationToken)
protected async ValueTask<Response> WaitForCompletionResponseAsync(bool async, TimeSpan? pollingInterval, string scopeName, 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 +195,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(scopeName);
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 Down
24 changes: 10 additions & 14 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,8 +235,13 @@ 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);
var rawResponse = async
? await WaitForCompletionResponseAsync(async: true, pollingInterval, _waitForCompletionScopeName, cancellationToken).ConfigureAwait(false)
: WaitForCompletionResponseAsync(async: false, pollingInterval, _waitForCompletionScopeName, cancellationToken).EnsureCompleted();
return Response.FromValue(Value, rawResponse);
}

Expand All @@ -260,7 +256,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
85 changes: 85 additions & 0 deletions sdk/core/Azure.Core/tests/OperationInternalOfTTests.cs
Original file line number Diff line number Diff line change
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,78 @@ 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, [Values(true, false)] bool suppressNestedClientActivities)
{
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>(new(new TestClientOptions(), suppressNestedClientActivities), 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
if (suppressNestedClientActivities)
{
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, [Values(true, false)] bool suppressNestedClientActivities)
{
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>(new(new TestClientOptions(), suppressNestedClientActivities), 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
if (suppressNestedClientActivities)
{
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
Loading