-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Conversation
/// Primitive that combines async lock and value cache | ||
/// </summary> | ||
/// <typeparam name="T"></typeparam> | ||
internal sealed class AsyncLockWithValue<T> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
I'll add more tests for this if design is approved. |
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
} | ||
|
||
protected abstract ValueTask<Response> UpdateStateAsync(bool async, CancellationToken cancellationToken); | ||
protected abstract ValueTask<OperationState> UpdateStateAsync(bool async, CancellationToken cancellationToken); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Closing in favor of #28375 |
This is a draft of the change to resolve several issues:
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)WaitForCompletion
makes at least one call even if operation is completed, including cases when operation was created as completedOperationInternal<T>.Value
may contain value that can't be recreated fromRawResponse
OperationInternal<T>.HasValue
may betrue
whileRawResponse
contains "pending" response