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

Synchronize OperationInternalBase calls #27639

Closed

Conversation

AlexanderSher
Copy link
Contributor

@AlexanderSher AlexanderSher commented Mar 18, 2022

This is a draft of the change to resolve several issues:

  • When two WaitForCompletion calls are made concurrently, RawResponse property may contain response that is not from the final state. For details, see: [Core] Improve OperationHelpers used by our LROs #19105 (comment)
  • Because of the problem above, right now we have a workaround that every call for WaitForCompletion makes at least one call even if operation is completed, including cases when operation was created as completed
  • OperationInternal<T>.Value may contain value that can't be recreated from RawResponse
  • OperationInternal<T>.HasValue may be true while RawResponse contains "pending" response

/// Primitive that combines async lock and value cache
/// </summary>
/// <typeparam name="T"></typeparam>
internal sealed class AsyncLockWithValue<T>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This type is a copy of type from Azure.Identity with added HasValue property and TryGetValue method.

Copy link
Member

@christothes christothes Mar 21, 2022

Choose a reason for hiding this comment

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

Assuming we make this change, we should change Identity's reference to this shared source version and delete its copy?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can do that.

RawResponse = rawResponse;
_fallbackStrategy = fallbackStrategy;

if (finalState != null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To avoid overriding of one final state with another, final state is assigned in constructor instead of separate method.


private async ValueTask<bool> TrySetPendingResponseAsync(bool async, Response response, CancellationToken cancellationToken)
{
using var asyncLock = await _operationStateLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method allows update of the RawResponse up until operation is completed

Copy link
Member

Choose a reason for hiding this comment

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

Could you add that as a comment? Would help with future maintenance.

private async ValueTask<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken)
{
using var asyncLock = await _operationStateLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here is where synchronization of calls happens.

Copy link
Member

Choose a reason for hiding this comment

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

Same here. Would help with future maintenance.

if (!state.HasSucceeded && state.OperationFailedException == null)
{
var exception = async
? await _diagnostics.CreateRequestFailedExceptionAsync(state.RawResponse).ConfigureAwait(false)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@annelo-msft when custom error parsing introduced, we will have to update this place as well


namespace Azure.Core.Tests
{
public class AsyncLockWithValueTests
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tests also copied from Azure.Identity

@@ -61,15 +61,13 @@ async ValueTask<OperationState> IOperation.UpdateStateAsync(bool async, Cancella
.ConfigureAwait(false)
: _client.GetTransactionStatus(Id, new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow });

_operationInternal.RawResponse = statusResponse;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not needed, _operationInternal updates its RawResponse

@AlexanderSher
Copy link
Contributor Author

I'll add more tests for this if design is approved.

Copy link
Member

@heaths heaths left a comment

Choose a reason for hiding this comment

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

General and KV changes LGTM, though I had a few suggestions and question.

}
}

public void Dispose() => _owner?.Reset();
Copy link
Member

Choose a reason for hiding this comment

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

Might be worth a comment here clarifying what this is intended to do. I believe the goal here is just to release any queued lock - not necessarily this one being disposed - and cycle through until we get another lock that hasn't completed, effectively like a semaphore, right?


private async ValueTask<bool> TrySetPendingResponseAsync(bool async, Response response, CancellationToken cancellationToken)
{
using var asyncLock = await _operationStateLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

Could you add that as a comment? Would help with future maintenance.

private async ValueTask<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken)
{
using var asyncLock = await _operationStateLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

Same here. Would help with future maintenance.

@@ -24,18 +24,15 @@ internal DeleteCertificateOperation(KeyVaultPipeline pipeline, Response<DeletedC
{
_pipeline = pipeline;
_value = response.Value ?? throw new InvalidOperationException("The response does not contain a value.");
// The recoveryId is only returned if soft delete is enabled.
Copy link
Member

Choose a reason for hiding this comment

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

Nit: can you add a blank line between 26 and 27. IIRC, this may cause some code style errors.

DelayStrategy? fallbackStrategy = null)
: base(clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy)
DelayStrategy? fallbackStrategy = null,
OperationState<T>? finalState = null)
Copy link
Member

Choose a reason for hiding this comment

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

Given we are taking this in on the constructor now I think we should move this class into its own file along with the non T version

https://github.com/Azure/azure-sdk-for-net/pull/27639/files#diff-f4e6fe06a0be62f7078fe921b22173ed7ad094e129c90206626734d527eb31f8R286

}

protected abstract ValueTask<Response> UpdateStateAsync(bool async, CancellationToken cancellationToken);
protected abstract ValueTask<OperationState> UpdateStateAsync(bool async, CancellationToken cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

It feels weird that we have a State class representing the state of the current operation yet that state is never stored in the instance of the class it simply creates one, then assigns some member variables. Should we maybe store this as a member variable and have the HasCompleted / HasValue / Value all lambda into this object to get their answers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DelayStrategy? fallbackStrategy = null)
:base(clientDiagnostics, rawResponse, operationTypeName ?? operation.GetType().Name, scopeAttributes, fallbackStrategy)
DelayStrategy? fallbackStrategy = null,
OperationState? finalState = null)
Copy link
Member

Choose a reason for hiding this comment

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

Do we worry about the conflict between the response in this final state and the response as the parameter? Should we take one or the other?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@heaths , is it possible that these responses are different in keyvault?

Copy link
Member

Choose a reason for hiding this comment

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

For the operations you changed, they are all pseudo-LROs anyway. The service backed endpoints aren't actually LROs, but we poll the final resource URI until it's not 404. Given the response and the final state should be agreed in that case, I don't think it should be possible.

}

protected override async ValueTask<Response> UpdateStateAsync(bool async, CancellationToken cancellationToken)
protected override async ValueTask<OperationState> UpdateStateAsync(bool async, CancellationToken cancellationToken)
{
OperationState<T> state = await _operation.UpdateStateAsync(async, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

This method on IOperation.UpdateStateAsync feels like it should be named GetStateAsync since it shouldn't be updating anything only this instance method should?


HasCompleted = true;
Copy link
Member

Choose a reason for hiding this comment

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

I would like to understand more why this race condition has to be fixed this way. I don't like that we are forcing an extra call even when the operation is completed (unless there is some nuance in your statement there).

Copy link
Member

Choose a reason for hiding this comment

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

I read the OP comment of To mitigate this problem we are adding an extra call, but it was referring to the previous bullet and how it was previously fixed. My understanding is now that the extra call is being removed.

I still would like to understand if there is a simpler way to resolve the race condition and allow the occasional thread to need an extra call

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Depends on what do you mean by "simpler". If we make an extra call, then we need to throw away its response if operation is in a final state. Otherwise, Response.FromValue(Value, rawResponse) may use Value from one request and rawResponse from another.
There are two options how to achieve it: using locks/semaphores or using state object that can be read/replaced in atomic operation. Since we have OperationInternal and OperationInternal<T>, there will be at least two objects (one derived from another). I can implement this version in a separate branch so we can compare.

@AlexanderSher
Copy link
Contributor Author

Closing in favor of #28375

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants