Skip to content

Commit ccd26d7

Browse files
Fix nullable support in OperationInternal and OperationInternal<T> (Azure#25438)
* Fix nullable support in OperationInternal and OperationInternal<T> * Extract common operations into `OperationInternalBase` * Undo tests in changes * Make OperationFailedException protected * Fix comments
1 parent 2e5072e commit ccd26d7

File tree

8 files changed

+309
-285
lines changed

8 files changed

+309
-285
lines changed

sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<Compile Include="$(AzureCoreSharedSources)HttpMessageSanitizer.cs" LinkBase="Shared" />
3838
<Compile Include="$(AzureCoreSharedSources)OperationHelpers.cs" LinkBase="Shared" />
3939
<Compile Include="$(AzureCoreSharedSources)OperationInternal.cs" LinkBase="Shared" />
40+
<Compile Include="$(AzureCoreSharedSources)OperationInternalBase.cs" LinkBase="Shared" />
4041
<Compile Include="$(AzureCoreSharedSources)PemReader.cs" LinkBase="Shared" />
4142
<Compile Include="$(AzureCoreSharedSources)TaskExtensions.cs" LinkBase="Shared" />
4243
<Compile Include="$(AzureCoreSharedSources)LightweightPkcs8Decoder.cs" LinkBase="Shared" />

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

Lines changed: 24 additions & 219 deletions
Large diffs are not rendered by default.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Azure.Core.Pipeline;
10+
11+
#nullable enable
12+
13+
namespace Azure.Core
14+
{
15+
internal abstract class OperationInternalBase
16+
{
17+
private readonly ClientDiagnostics _diagnostics;
18+
private readonly string _updateStatusScopeName;
19+
private readonly IReadOnlyDictionary<string, string>? _scopeAttributes;
20+
private const string RetryAfterHeaderName = "Retry-After";
21+
private const string RetryAfterMsHeaderName = "retry-after-ms";
22+
private const string XRetryAfterMsHeaderName = "x-ms-retry-after-ms";
23+
24+
protected OperationInternalBase(ClientDiagnostics clientDiagnostics, Response rawResponse, string operationTypeName, IEnumerable<KeyValuePair<string, string>>? scopeAttributes = null)
25+
{
26+
_diagnostics = clientDiagnostics;
27+
_updateStatusScopeName = $"{operationTypeName}.UpdateStatus";
28+
_scopeAttributes = scopeAttributes?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
29+
RawResponse = rawResponse;
30+
DefaultPollingInterval = TimeSpan.FromSeconds(1);
31+
}
32+
33+
/// <summary>
34+
/// The last HTTP response received from the server. Its update already handled in calls to "<c>UpdateStatus</c>" and
35+
/// "<c>WaitForCompletionAsync</c>", but custom methods not supported by this class, such as "<c>CancelOperation</c>",
36+
/// must update it as well.
37+
/// <example>Usage example:
38+
/// <code>
39+
/// public Response GetRawResponse() => _operationInternal.RawResponse;
40+
/// </code>
41+
/// </example>
42+
/// </summary>
43+
public Response RawResponse { get; set; }
44+
45+
/// <summary>
46+
/// Returns <c>true</c> if the long-running operation has completed.
47+
/// <example>Usage example:
48+
/// <code>
49+
/// public bool HasCompleted => _operationInternal.HasCompleted;
50+
/// </code>
51+
/// </example>
52+
/// </summary>
53+
public bool HasCompleted { get; protected set; }
54+
55+
/// <summary>
56+
/// Can be set to control the default interval used between service calls in <see cref="WaitForCompletionResponseAsync(CancellationToken)"/>.
57+
/// Defaults to 1 second.
58+
/// </summary>
59+
public TimeSpan DefaultPollingInterval { get; set; }
60+
61+
protected RequestFailedException? OperationFailedException { get; private set; }
62+
63+
/// <summary>
64+
/// Calls the server to get the latest status of the long-running operation, handling diagnostic scope creation for distributed
65+
/// tracing. The default scope name can be changed with the "<c>operationTypeName</c>" parameter passed to the constructor.
66+
/// <example>Usage example:
67+
/// <code>
68+
/// public async ValueTask&lt;Response&gt; UpdateStatusAsync(CancellationToken cancellationToken) =>
69+
/// await _operationInternal.UpdateStatusAsync(cancellationToken).ConfigureAwait(false);
70+
/// </code>
71+
/// </example>
72+
/// </summary>
73+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
74+
/// <returns>The HTTP response received from the server.</returns>
75+
/// <remarks>
76+
/// After a successful run, this method will update <see cref="RawResponse"/> and might update <see cref="HasCompleted"/>.
77+
/// </remarks>
78+
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
79+
public async ValueTask<Response> UpdateStatusAsync(CancellationToken cancellationToken) =>
80+
await UpdateStatusAsync(async: true, cancellationToken).ConfigureAwait(false);
81+
82+
/// <summary>
83+
/// Calls the server to get the latest status of the long-running operation, handling diagnostic scope creation for distributed
84+
/// tracing. The default scope name can be changed with the "<c>operationTypeName</c>" parameter passed to the constructor.
85+
/// <example>Usage example:
86+
/// <code>
87+
/// public Response UpdateStatus(CancellationToken cancellationToken) => _operationInternal.UpdateStatus(cancellationToken);
88+
/// </code>
89+
/// </example>
90+
/// </summary>
91+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
92+
/// <returns>The HTTP response received from the server.</returns>
93+
/// <remarks>
94+
/// After a successful run, this method will update <see cref="RawResponse"/> and might update <see cref="HasCompleted"/>.
95+
/// </remarks>
96+
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
97+
public Response UpdateStatus(CancellationToken cancellationToken) =>
98+
UpdateStatusAsync(async: false, cancellationToken).EnsureCompleted();
99+
100+
/// <summary>
101+
/// Periodically calls <see cref="UpdateStatusAsync(CancellationToken)"/> until the long-running operation completes. The interval
102+
/// between calls is defined by the property <see cref="DefaultPollingInterval"/>, but it can change based on information returned
103+
/// from the server. After each service call, a retry-after header may be returned to communicate that there is no reason to poll
104+
/// for status change until the specified time has passed. In this case, the maximum value between the <see cref="DefaultPollingInterval"/>
105+
/// property and the retry-after header is chosen as the wait interval. Headers supported are: "Retry-After", "retry-after-ms",
106+
/// and "x-ms-retry-after-ms".
107+
/// <example>Usage example:
108+
/// <code>
109+
/// public async ValueTask&lt;Response&lt;T&gt;&gt; WaitForCompletionAsync(CancellationToken cancellationToken) =>
110+
/// await _operationInternal.WaitForCompletionAsync(cancellationToken).ConfigureAwait(false);
111+
/// </code>
112+
/// </example>
113+
/// </summary>
114+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
115+
/// <returns>The last HTTP response received from the server, including the final result of the long-running operation.</returns>
116+
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
117+
public virtual async ValueTask<Response> WaitForCompletionResponseAsync(CancellationToken cancellationToken) =>
118+
await WaitForCompletionResponseAsync(DefaultPollingInterval, cancellationToken).ConfigureAwait(false);
119+
120+
/// <summary>
121+
/// Periodically calls <see cref="UpdateStatusAsync(CancellationToken)"/> until the long-running operation completes. The interval
122+
/// between calls is defined by the parameter <paramref name="pollingInterval"/>, but it can change based on information returned
123+
/// from the server. After each service call, a retry-after header may be returned to communicate that there is no reason to poll
124+
/// for status change until the specified time has passed. In this case, the maximum value between the <paramref name="pollingInterval"/>
125+
/// parameter and the retry-after header is chosen as the wait interval. Headers supported are: "Retry-After", "retry-after-ms",
126+
/// and "x-ms-retry-after-ms".
127+
/// <example>Usage example:
128+
/// <code>
129+
/// public async ValueTask&lt;Response&lt;T&gt;&gt; WaitForCompletionAsync(TimeSpan pollingInterval, CancellationToken cancellationToken) =>
130+
/// await _operationInternal.WaitForCompletionAsync(pollingInterval, cancellationToken).ConfigureAwait(false);
131+
/// </code>
132+
/// </example>
133+
/// </summary>
134+
/// <param name="pollingInterval">The interval between status requests to the server.</param>
135+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
136+
/// <returns>The last HTTP response received from the server, including the final result of the long-running operation.</returns>
137+
/// <exception cref="RequestFailedException">Thrown if there's been any issues during the connection, or if the operation has completed with failures.</exception>
138+
public virtual async ValueTask<Response> WaitForCompletionResponseAsync(TimeSpan pollingInterval, CancellationToken cancellationToken)
139+
{
140+
while (true)
141+
{
142+
Response response = await UpdateStatusAsync(cancellationToken).ConfigureAwait(false);
143+
144+
if (HasCompleted)
145+
{
146+
return response;
147+
}
148+
149+
TimeSpan delay = GetServerDelay(response, pollingInterval);
150+
await WaitAsync(delay, cancellationToken).ConfigureAwait(false);
151+
}
152+
}
153+
154+
protected virtual async Task WaitAsync(TimeSpan delay, CancellationToken cancellationToken) =>
155+
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
156+
157+
private async ValueTask<Response> UpdateStatusAsync(bool async, CancellationToken cancellationToken)
158+
{
159+
using DiagnosticScope scope = _diagnostics.CreateScope(_updateStatusScopeName);
160+
161+
if (_scopeAttributes != null)
162+
{
163+
foreach (KeyValuePair<string, string> attribute in _scopeAttributes)
164+
{
165+
scope.AddAttribute(attribute.Key, attribute.Value);
166+
}
167+
}
168+
169+
scope.Start();
170+
171+
try
172+
{
173+
return await UpdateStateAsync(async, cancellationToken).ConfigureAwait(false);
174+
}
175+
catch (Exception e)
176+
{
177+
scope.Failed(e);
178+
throw;
179+
}
180+
}
181+
182+
protected async ValueTask<Response> ApplyStateAsync(bool async, Response response, bool hasCompleted, bool hasSucceeded, RequestFailedException? requestFailedException)
183+
{
184+
RawResponse = response;
185+
186+
if (!hasCompleted)
187+
{
188+
return response;
189+
}
190+
191+
HasCompleted = true;
192+
if (hasSucceeded)
193+
{
194+
return response;
195+
}
196+
197+
OperationFailedException = requestFailedException ??
198+
(async
199+
? await _diagnostics.CreateRequestFailedExceptionAsync(response).ConfigureAwait(false)
200+
: _diagnostics.CreateRequestFailedException(response));
201+
throw OperationFailedException;
202+
}
203+
204+
protected static TimeSpan GetServerDelay(Response response, TimeSpan pollingInterval)
205+
{
206+
TimeSpan serverDelay = pollingInterval;
207+
if (response.Headers.TryGetValue(RetryAfterMsHeaderName, out string? retryAfterValue) ||
208+
response.Headers.TryGetValue(XRetryAfterMsHeaderName, out retryAfterValue))
209+
{
210+
if (int.TryParse(retryAfterValue, out int serverDelayInMilliseconds))
211+
{
212+
serverDelay = TimeSpan.FromMilliseconds(serverDelayInMilliseconds);
213+
}
214+
}
215+
else if (response.Headers.TryGetValue(RetryAfterHeaderName, out retryAfterValue))
216+
{
217+
if (int.TryParse(retryAfterValue, out int serverDelayInSeconds))
218+
{
219+
serverDelay = TimeSpan.FromSeconds(serverDelayInSeconds);
220+
}
221+
}
222+
223+
return serverDelay > pollingInterval
224+
? serverDelay
225+
: pollingInterval;
226+
}
227+
228+
protected abstract ValueTask<Response> UpdateStateAsync(bool async, CancellationToken cancellationToken);
229+
}
230+
}

0 commit comments

Comments
 (0)