Skip to content

Commit 71054d5

Browse files
authored
Resolve several Key Vault backlog issues (Azure#25593)
* Use operation helpers Partially fixes Azure#22912 * Resolve PR feedback * Add OperationInternal tests * Use OperationInternal for all pseudo-LROs Resolves Azure#22912 * Use KeyVaultPipeline for SecretClient Resolves #8115 * Deduplicate identifier parsing Resolves Azure#24262 * Resolve PR feedback
1 parent 8ba684a commit 71054d5

19 files changed

+303
-412
lines changed

sdk/core/Azure.Core/src/Shared/OperationInternal.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ public OperationInternal(
7878
_operation = operation;
7979
}
8080

81+
/// <summary>
82+
/// Sets the <see cref="OperationInternal"/> state immediately.
83+
/// </summary>
84+
/// <param name="state">The <see cref="OperationState"/> used to set <see cref="OperationInternalBase.HasCompleted"/> and other members.</param>
85+
public void SetState(OperationState state)
86+
{
87+
ApplyStateAsync(false, state.RawResponse, state.HasCompleted, state.HasSucceeded, state.OperationFailedException, throwIfFailed: false).EnsureCompleted();
88+
}
89+
8190
protected override async ValueTask<Response> UpdateStateAsync(bool async, CancellationToken cancellationToken)
8291
{
8392
OperationState state = await _operation.UpdateStateAsync(async, cancellationToken).ConfigureAwait(false);

sdk/core/Azure.Core/src/Shared/OperationInternalBase.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private async ValueTask<Response> UpdateStatusAsync(bool async, CancellationToke
179179
}
180180
}
181181

182-
protected async ValueTask<Response> ApplyStateAsync(bool async, Response response, bool hasCompleted, bool hasSucceeded, RequestFailedException? requestFailedException)
182+
protected async ValueTask<Response> ApplyStateAsync(bool async, Response response, bool hasCompleted, bool hasSucceeded, RequestFailedException? requestFailedException, bool throwIfFailed = true)
183183
{
184184
RawResponse = response;
185185

@@ -198,7 +198,13 @@ protected async ValueTask<Response> ApplyStateAsync(bool async, Response respons
198198
(async
199199
? await _diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false)
200200
: _diagnostics.CreateRequestFailedException(response));
201-
throw OperationFailedException;
201+
202+
if (throwIfFailed)
203+
{
204+
throw OperationFailedException;
205+
}
206+
207+
return response;
202208
}
203209

204210
protected static TimeSpan GetServerDelay(Response response, TimeSpan pollingInterval)

sdk/core/Azure.Core/src/Shared/OperationInternalOfT.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ public async ValueTask<Response<T>> WaitForCompletionAsync(TimeSpan pollingInter
168168
return Response.FromValue(Value, rawResponse);
169169
}
170170

171+
/// <summary>
172+
/// Sets the <see cref="OperationInternal{T}"/> state immediately.
173+
/// </summary>
174+
/// <param name="state">The <see cref="OperationState{T}"/> used to set <see cref="OperationInternalBase.HasCompleted"/> and other members.</param>
175+
public void SetState(OperationState<T> state)
176+
{
177+
if (state.HasCompleted && state.HasSucceeded)
178+
{
179+
Value = state.Value!;
180+
}
181+
ApplyStateAsync(false, state.RawResponse, state.HasCompleted, state.HasSucceeded, state.OperationFailedException, throwIfFailed: false).EnsureCompleted();
182+
}
183+
171184
protected override async ValueTask<Response> UpdateStateAsync(bool async, CancellationToken cancellationToken)
172185
{
173186
OperationState<T> state = await _operation.UpdateStateAsync(async, cancellationToken).ConfigureAwait(false);

sdk/core/Azure.Core/tests/OperationInternalTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,68 @@ public void RawResponseInitialization()
8787
}
8888
}
8989

90+
[Test]
91+
public void SetStateSucceeds()
92+
{
93+
var operationInternal = CreateOperation(isOfT, UpdateResult.Pending);
94+
if (operationInternal is OperationInternal oi)
95+
{
96+
oi.SetState(OperationState.Success(mockResponse));
97+
}
98+
else if (operationInternal is OperationInternal<int> oit)
99+
{
100+
oit.SetState(OperationState<int>.Success(mockResponse, 1));
101+
}
102+
103+
Assert.IsTrue(operationInternal.HasCompleted);
104+
if (operationInternal is OperationInternal<int> oit2)
105+
{
106+
Assert.IsTrue(oit2.HasValue);
107+
Assert.AreEqual(1, oit2.Value);
108+
}
109+
}
110+
111+
[Test]
112+
public void SetStateIsPending()
113+
{
114+
var operationInternal = CreateOperation(isOfT, UpdateResult.Pending);
115+
if (operationInternal is OperationInternal oi)
116+
{
117+
oi.SetState(OperationState.Pending(mockResponse));
118+
}
119+
else if (operationInternal is OperationInternal<int> oit)
120+
{
121+
oit.SetState(OperationState<int>.Pending(mockResponse));
122+
}
123+
124+
Assert.IsFalse(operationInternal.HasCompleted);
125+
if (operationInternal is OperationInternal<int> oit2)
126+
{
127+
Assert.IsFalse(oit2.HasValue);
128+
}
129+
}
130+
131+
[Test]
132+
public void SetStateFails()
133+
{
134+
var operationInternal = CreateOperation(isOfT, UpdateResult.Pending);
135+
if (operationInternal is OperationInternal oi)
136+
{
137+
oi.SetState(OperationState.Failure(mockResponse));
138+
}
139+
else if (operationInternal is OperationInternal<int> oit)
140+
{
141+
oit.SetState(OperationState<int>.Failure(mockResponse));
142+
}
143+
144+
Assert.IsTrue(operationInternal.HasCompleted);
145+
if (operationInternal is OperationInternal<int> oit2)
146+
{
147+
Assert.IsFalse(oit2.HasValue);
148+
Assert.Throws<RequestFailedException>(() => _ = oit2.Value);
149+
}
150+
}
151+
90152
[Test]
91153
public async Task UpdateStatusWhenOperationIsPending([Values(true, false)] bool async)
92154
{

sdk/keyvault/Azure.Security.KeyVault.Certificates/src/Azure.Security.KeyVault.Certificates.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
<Compile Include="$(AzureCoreSharedSources)ContentTypeUtilities.cs" LinkBase="Shared" />
3636
<Compile Include="$(AzureCoreSharedSources)DiagnosticScope.cs" LinkBase="Shared" />
3737
<Compile Include="$(AzureCoreSharedSources)OperationHelpers.cs" LinkBase="Shared" />
38+
<Compile Include="$(AzureCoreSharedSources)OperationInternal.cs" LinkBase="Shared" />
39+
<Compile Include="$(AzureCoreSharedSources)OperationInternalBase.cs" LinkBase="Shared" />
3840
<Compile Include="$(AzureCoreSharedSources)PageResponseEnumerator.cs" LinkBase="Shared" />
3941
<Compile Include="$(AzureCoreSharedSources)DiagnosticScopeFactory.cs" LinkBase="Shared" />
4042
<Compile Include="$(AzureCoreSharedSources)TaskExtensions.cs" LinkBase="Shared" />

sdk/keyvault/Azure.Security.KeyVault.Certificates/src/CertificateProperties.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,14 @@ internal void ReadProperty(JsonProperty prop)
158158
}
159159
}
160160

161-
private void ParseId(Uri idToParse)
161+
private void ParseId(Uri id)
162162
{
163-
// We expect an identifier with either 3 or 4 segments: host + collection + name [+ version]
164-
if (idToParse.Segments.Length != 3 && idToParse.Segments.Length != 4)
165-
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid ObjectIdentifier: {0}. Bad number of segments: {1}", idToParse, idToParse.Segments.Length));
163+
KeyVaultIdentifier identifier = KeyVaultIdentifier.ParseWithCollection(id, "certificates");
166164

167-
if (!string.Equals(idToParse.Segments[1], "certificates" + "/", StringComparison.OrdinalIgnoreCase))
168-
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid ObjectIdentifier: {0}. segment [1] should be 'certificates/', found '{1}'", idToParse, idToParse.Segments[1]));
169-
170-
VaultUri = new Uri($"{idToParse.Scheme}://{idToParse.Authority}");
171-
Name = idToParse.Segments[2].Trim('/');
172-
Version = (idToParse.Segments.Length == 4) ? idToParse.Segments[3].TrimEnd('/') : null;
165+
Id = id;
166+
VaultUri = identifier.VaultUri;
167+
Name = identifier.Name;
168+
Version = identifier.Version;
173169
}
174170
}
175171
}

sdk/keyvault/Azure.Security.KeyVault.Certificates/src/DeleteCertificateOperation.cs

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,39 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Azure.Core;
8-
using Azure.Core.Pipeline;
99

1010
namespace Azure.Security.KeyVault.Certificates
1111
{
1212
/// <summary>
1313
/// A long-running operation for <see cref="CertificateClient.StartDeleteCertificate(string, CancellationToken)"/> or <see cref="CertificateClient.StartDeleteCertificateAsync(string, CancellationToken)"/>.
1414
/// </summary>
15-
public class DeleteCertificateOperation : Operation<DeletedCertificate>
15+
public class DeleteCertificateOperation : Operation<DeletedCertificate>, IOperation
1616
{
1717
private static readonly TimeSpan s_defaultPollingInterval = TimeSpan.FromSeconds(2);
1818

1919
private readonly KeyVaultPipeline _pipeline;
20+
private readonly OperationInternal _operationInternal;
2021
private readonly DeletedCertificate _value;
21-
private Response _response;
22-
private bool _completed;
2322

2423
internal DeleteCertificateOperation(KeyVaultPipeline pipeline, Response<DeletedCertificate> response)
2524
{
2625
_pipeline = pipeline;
2726
_value = response.Value ?? throw new InvalidOperationException("The response does not contain a value.");
28-
_response = response.GetRawResponse();
27+
_operationInternal = new(_pipeline.Diagnostics, this, response.GetRawResponse(), nameof(DeleteCertificateOperation), new[]
28+
{
29+
new KeyValuePair<string, string>("secret", _value.Name), // Retained for backward compatibility.
30+
new KeyValuePair<string, string>("certificate", _value.Name),
31+
});
2932

30-
// The recoveryId is only returned if soft-delete is enabled.
33+
// The recoveryId is only returned if soft delete is enabled.
3134
if (_value.RecoveryId is null)
3235
{
33-
_completed = true;
36+
// If soft delete is not enabled, deleting is immediate so set success accordingly.
37+
_operationInternal.SetState(OperationState.Success(response.GetRawResponse()));
3438
}
3539
}
3640

@@ -50,33 +54,20 @@ protected DeleteCertificateOperation() {}
5054
public override DeletedCertificate Value => _value;
5155

5256
/// <inheritdoc/>
53-
public override bool HasCompleted => _completed;
57+
public override bool HasCompleted => _operationInternal.HasCompleted;
5458

5559
/// <inheritdoc/>
5660
public override bool HasValue => true;
5761

5862
/// <inheritdoc/>
59-
public override Response GetRawResponse() => _response;
63+
public override Response GetRawResponse() => _operationInternal.RawResponse;
6064

6165
/// <inheritdoc/>
6266
public override Response UpdateStatus(CancellationToken cancellationToken = default)
6367
{
64-
if (!_completed)
68+
if (!HasCompleted)
6569
{
66-
using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(DeleteCertificateOperation)}.{nameof(UpdateStatus)}");
67-
scope.AddAttribute("secret", _value.Name);
68-
scope.Start();
69-
70-
try
71-
{
72-
_response = _pipeline.GetResponse(RequestMethod.Get, cancellationToken, CertificateClient.DeletedCertificatesPath, _value.Name);
73-
_completed = CheckCompleted(_response);
74-
}
75-
catch (Exception e)
76-
{
77-
scope.Failed(e);
78-
throw;
79-
}
70+
return _operationInternal.UpdateStatus(cancellationToken);
8071
}
8172

8273
return GetRawResponse();
@@ -85,22 +76,9 @@ public override Response UpdateStatus(CancellationToken cancellationToken = defa
8576
/// <inheritdoc/>
8677
public override async ValueTask<Response> UpdateStatusAsync(CancellationToken cancellationToken = default)
8778
{
88-
if (!_completed)
79+
if (!HasCompleted)
8980
{
90-
using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(DeleteCertificateOperation)}.{nameof(UpdateStatus)}");
91-
scope.AddAttribute("secret", _value.Name);
92-
scope.Start();
93-
94-
try
95-
{
96-
_response = await _pipeline.GetResponseAsync(RequestMethod.Get, cancellationToken, CertificateClient.DeletedCertificatesPath, _value.Name).ConfigureAwait(false);
97-
_completed = await CheckCompletedAsync(_response).ConfigureAwait(false);
98-
}
99-
catch (Exception e)
100-
{
101-
scope.Failed(e);
102-
throw;
103-
}
81+
return await _operationInternal.UpdateStatusAsync(cancellationToken).ConfigureAwait(false);
10482
}
10583

10684
return GetRawResponse();
@@ -114,34 +92,27 @@ public override ValueTask<Response<DeletedCertificate>> WaitForCompletionAsync(C
11492
public override ValueTask<Response<DeletedCertificate>> WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken) =>
11593
this.DefaultWaitForCompletionAsync(pollingInterval, cancellationToken);
11694

117-
private async ValueTask<bool> CheckCompletedAsync(Response response)
95+
async ValueTask<OperationState> IOperation.UpdateStateAsync(bool async, CancellationToken cancellationToken)
11896
{
119-
switch (response.Status)
120-
{
121-
case 200:
122-
case 403: // Access denied but proof the certificate was deleted.
123-
return true;
124-
125-
case 404:
126-
return false;
97+
Response response = async
98+
? await _pipeline.GetResponseAsync(RequestMethod.Get, cancellationToken, CertificateClient.DeletedCertificatesPath, _value.Name).ConfigureAwait(false)
99+
: _pipeline.GetResponse(RequestMethod.Get, cancellationToken, CertificateClient.DeletedCertificatesPath, _value.Name);
127100

128-
default:
129-
throw await _pipeline.Diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false);
130-
}
131-
}
132-
private bool CheckCompleted(Response response)
133-
{
134101
switch (response.Status)
135102
{
136103
case 200:
137104
case 403: // Access denied but proof the certificate was deleted.
138-
return true;
105+
return OperationState.Success(response);
139106

140107
case 404:
141-
return false;
108+
return OperationState.Pending(response);
142109

143110
default:
144-
throw _pipeline.Diagnostics.CreateRequestFailedException(response);
111+
RequestFailedException ex = async
112+
? await _pipeline.Diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false)
113+
: _pipeline.Diagnostics.CreateRequestFailedException(response);
114+
115+
return OperationState.Failure(response, ex);
145116
}
146117
}
147118
}

0 commit comments

Comments
 (0)