diff --git a/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure.Tests/LROTests/LongRunningOperationsTest.cs b/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure.Tests/LROTests/LongRunningOperationsTest.cs index 46bcb3b24af0..d121ea77b394 100644 --- a/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure.Tests/LROTests/LongRunningOperationsTest.cs +++ b/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure.Tests/LROTests/LongRunningOperationsTest.cs @@ -488,47 +488,7 @@ public void TestCreateOrUpdateFailedStatus() } - /// - /// Test - /// - [Fact] - public void TestCreateOrUpdateErrorHandling() - { - var tokenCredentials = new TokenCredentials("123", "abc"); - var handler = new PlaybackTestHandler(LROResponse.MockCreateOrUpdateWithImmediateServerError()); - var fakeClient = new RedisManagementClient(tokenCredentials, handler); - fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; - try - { - fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); - Assert.False(true, "Expected exception was not thrown."); - } - catch(CloudException ex) - { - Assert.Equal("The provided database ‘foo’ has an invalid username.", ex.Message); - } - } - - /// - /// Test - /// - [Fact] - public void TestCreateOrUpdateNoErrorBody() - { - var tokenCredentials = new TokenCredentials("123", "abc"); - var handler = new PlaybackTestHandler(LROResponse.MockCreateOrUpdateWithNoErrorBody()); - var fakeClient = new RedisManagementClient(tokenCredentials, handler); - fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; - try - { - fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); - Assert.False(true, "Expected exception was not thrown."); - } - catch (CloudException ex) - { - Assert.Equal(HttpStatusCode.InternalServerError, ex.Response.StatusCode); - } - } + /// @@ -573,28 +533,6 @@ public void TestDeleteWithLocationHeader() Assert.Equal(2, handler.Requests.Count); } - /// - /// Test - /// - [Fact] - public void TestDeleteWithLocationHeaderErrorHandling() - { - var tokenCredentials = new TokenCredentials("123", "abc"); - var handler = new PlaybackTestHandler(LROResponse.MockDeleteWithLocationHeaderError()); - var fakeClient = new RedisManagementClient(tokenCredentials, handler); - fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; - - try - { - fakeClient.RedisOperations.Delete("rg", "redis", "1234"); - Assert.False(true, "Expected exception was not thrown."); - } - catch (CloudException ex) - { - Assert.Null(ex.Body); - } - } - /// /// Test /// @@ -610,66 +548,6 @@ public void TestDeleteWithLocationHeaderErrorHandlingSecondTime() Assert.Equal("Long running operation failed with status 'InternalServerError'.", ex.Message); } - /// - /// Test - /// - [Fact] - public void TestLROAsynOperationFailureWith200() - { - var tokenCredentials = new TokenCredentials("123", "abc"); - var handler = new PlaybackTestHandler(LROResponse.MockLROAsyncOperationFailedWith200()); - var fakeClient = new RedisManagementClient(tokenCredentials, handler); - fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; - try - { - var foo = fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); - } - catch(Exception ex) - { - Assert.Contains("DeploymentDocument", ex.Message); - } - } - - /// - /// Test - /// - [Fact] - public void TestLROWithLocationHeaderFailureWith200() - { - var tokenCredentials = new TokenCredentials("123", "abc"); - var handler = new PlaybackTestHandler(LROResponse.MockLROLocationHeaderFailedWith200()); - var fakeClient = new RedisManagementClient(tokenCredentials, handler); - fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; - try - { - var foo = fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); - } - catch (Exception ex) - { - // If the message changes in the response, this assert will also have to be updated. - Assert.Contains("DeploymentDocument", ex.Message); - } - } - - /// - /// Test - /// - [Fact] - public void TestLROPUTWithCanceledState() - { - var tokenCredentials = new TokenCredentials("123", "abc"); - var handler = new PlaybackTestHandler(LROResponse.MockLROPUTWithCanceledStateResponse()); - var fakeClient = new RedisManagementClient(tokenCredentials, handler); - fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; - try - { - var foo = fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); - } - catch (Exception ex) - { - Assert.Contains("preempted", ex.Message); - } - } /// /// Test @@ -876,6 +754,9 @@ public void TestPatchWithLocationHeader() /// public class LRO_FailedTests { + /// + /// + /// [Fact /*(Skip = "Potential scenario that will have to be supported")*/] public void TestLROAsynOperationFailureWith200() { @@ -883,13 +764,126 @@ public void TestLROAsynOperationFailureWith200() var handler = new PlaybackTestHandler(LROFailedResponses.MockLROAsyncOperationFailedOnlyStatus()); var fakeClient = new RedisManagementClient(tokenCredentials, handler); fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; + Assert.Throws(() => + { + try + { + fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); + } + catch (Exception ex) + { + Assert.Contains("Unable to deserilize body", ex.Message); + throw ex; + } + }); + } + + /// + /// Test + /// + [Fact] + public void TestCreateOrUpdateErrorHandling() + { + var tokenCredentials = new TokenCredentials("123", "abc"); + var handler = new PlaybackTestHandler(LROResponse.MockCreateOrUpdateWithImmediateServerError()); + var fakeClient = new RedisManagementClient(tokenCredentials, handler); + fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; + try + { + fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); + Assert.False(true, "Expected exception was not thrown."); + } + catch (CloudException ex) + { + Assert.Equal("The provided database ‘foo’ has an invalid username.", ex.Message); + } + } + + /// + /// Test + /// + [Fact] + public void TestCreateOrUpdateNoErrorBody() + { + var tokenCredentials = new TokenCredentials("123", "abc"); + var handler = new PlaybackTestHandler(LROResponse.MockCreateOrUpdateWithNoErrorBody()); + var fakeClient = new RedisManagementClient(tokenCredentials, handler); + fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; + try + { + fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); + Assert.False(true, "Expected exception was not thrown."); + } + catch (CloudException ex) + { + Assert.Equal(HttpStatusCode.InternalServerError, ex.Response.StatusCode); + } + } + + /// + /// Test + /// + [Fact] + public void TestDeleteWithLocationHeaderErrorHandling() + { + var tokenCredentials = new TokenCredentials("123", "abc"); + var handler = new PlaybackTestHandler(LROResponse.MockDeleteWithLocationHeaderError()); + var fakeClient = new RedisManagementClient(tokenCredentials, handler); + fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; + + try + { + fakeClient.RedisOperations.Delete("rg", "redis", "1234"); + Assert.False(true, "Expected exception was not thrown."); + } + catch (CloudException ex) + { + Assert.Null(ex.Body); + } + } + + /// + /// Test + /// + [Fact] + public void TestLROWithLocationHeaderFailureWith200() + { + var tokenCredentials = new TokenCredentials("123", "abc"); + var handler = new PlaybackTestHandler(LROResponse.MockLROLocationHeaderFailedWith200()); + var fakeClient = new RedisManagementClient(tokenCredentials, handler); + fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; + + Assert.Throws(() => + { + try + { + fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); + } + catch(Exception ex) + { + Assert.Contains("DeploymentDocument", ex.Message); + throw ex; + } + }); + } + + /// + /// Test + /// + [Fact] + public void TestLROPUTWithCanceledState() + { + var tokenCredentials = new TokenCredentials("123", "abc"); + var handler = new PlaybackTestHandler(LROResponse.MockLROPUTWithCanceledStateResponse()); + var fakeClient = new RedisManagementClient(tokenCredentials, handler); + fakeClient.LongRunningOperationInitialTimeout = fakeClient.LongRunningOperationRetryTimeout = 0; try { var foo = fakeClient.RedisOperations.CreateOrUpdate("rg", "redis", new RedisCreateOrUpdateParameters(), "1234"); } catch (Exception ex) { - Assert.Contains("Unable to deserilize body", ex.Message); + Assert.Contains("preempted", ex.Message); } } } @@ -928,8 +922,5 @@ public void TestPUT_WithMultipleHeaders() Assert.Equal(HttpMethod.Get, handler.Requests[3].Method); Assert.Equal("https://management.azure.com/subscriptions/1234/resourceGroups/rg/providers/Microsoft.Cache/Redis/redis", handler.Requests[3].RequestUri.ToString()); } - - - } } diff --git a/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/Base/AzureLRO.cs b/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/Base/AzureLRO.cs index b8b5f64e12f7..728697c81404 100644 --- a/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/Base/AzureLRO.cs +++ b/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/Base/AzureLRO.cs @@ -67,7 +67,7 @@ public virtual async Task BeginLROAsync() InitializeAsyncHeadersToUse(); await StartPollingAsync(); await PostPollingAsync(); - CheckForErrors(); + CheckFinalErrors(); IsLROTaskCompleted = true; } @@ -88,7 +88,20 @@ public virtual async Task #endregion #region Protected functions - + + /// + /// Check for errors at the end of LRO operation + /// Last chance to check any final errors + /// + protected virtual void CheckFinalErrors() + { + if (!string.IsNullOrEmpty(CurrentPollingState.LastSerializationExceptionMessage)) + { + throw new CloudException(string.Format(Resources.BodyDeserializationError, CurrentPollingState.LastSerializationExceptionMessage)); + } + } + + /// /// Does basic validation on initial response from RP, prior to start LRO process /// @@ -181,11 +194,9 @@ protected virtual void UpdatePollingState() #region Check provisionState CurrentPollingState.CurrentStatusCode = CurrentPollingState.Response.StatusCode; - if (!string.IsNullOrEmpty(CurrentPollingState.AsyncOperationResponseBody?.Status) - && - (!string.IsNullOrEmpty(CurrentPollingState.AzureAsyncOperationHeaderLink))) + if (IsAzureAsyncOperationResponseStateValid() == true) { - CurrentPollingState.Status = CurrentPollingState.AsyncOperationResponseBody.Status; + CurrentPollingState.Status = GetAzureAsyncResponseState(); } else { @@ -289,6 +300,85 @@ protected virtual string GetValidAbsoluteUri(string url, bool throwForInvalidUri return absoluteUri; } + + #endregion + + #region Private functions + /// + /// Get Valid status + /// There are cases where there is an error sent from the service and in that case, the status should be one of the valid FailedStatuses + /// But there are cases where there is a customized error sent by service and they do not fall under Failed/Success statuses, in that case we fall back on response status + /// + /// e.g. The response status is OK, but the error body has the status as "TestFailed" (which do not fall under valid failed status, so we fall back to OK) + /// + /// + private string GetAzureAsyncResponseState() + { + string validStatus = string.Empty; + if (!string.IsNullOrEmpty(CurrentPollingState.AsyncOperationResponseBody?.Status)) + { + if (AzureAsyncOperation.FailedStatuses.Any( + s => s.Equals(CurrentPollingState.AsyncOperationResponseBody.Status, StringComparison.OrdinalIgnoreCase))) + { + validStatus = CurrentPollingState.AsyncOperationResponseBody.Status; + } + else if (AzureAsyncOperation.TerminalStatuses.Any(s => s.Equals(CurrentPollingState.AsyncOperationResponseBody.Status, StringComparison.OrdinalIgnoreCase))) + { + validStatus = CurrentPollingState.AsyncOperationResponseBody.Status; + } + else if (string.IsNullOrEmpty(validStatus)) + { + validStatus = CurrentPollingState.Response.StatusCode.ToString(); + } + } + + return validStatus; + } + + /// + /// This function determines if you are running your polling under Azure-Async header or if the response status falls under terminal/failed status + /// + /// + private bool IsAzureAsyncOperationResponseStateValid() + { + if (CurrentPollingState?.AsyncOperationResponseBody != null && !string.IsNullOrEmpty(CurrentPollingState.AsyncOperationResponseBody?.Status)) + { + if (AzureAsyncOperation.FailedStatuses.Any( + s => s.Equals(CurrentPollingState.AsyncOperationResponseBody.Status, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + else if (AzureAsyncOperation.TerminalStatuses.Any(s => s.Equals(CurrentPollingState.AsyncOperationResponseBody.Status, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + else if(IsUriEqual(CurrentPollingState.PollingUrlToUse, CurrentPollingState.AzureAsyncOperationHeaderLink)) + { + return true; + } + } + + return false; + } + + /// + /// Check URI for equality including differences in trailing slash and compare case insensitive + /// + /// Url + /// Url to compare against + /// + private bool IsUriEqual(string leftUrl, string rightUrl) + { + if (string.IsNullOrEmpty(leftUrl)) return false; + if (string.IsNullOrEmpty(rightUrl)) return false; + + Uri left = new Uri(leftUrl); + Uri right = new Uri(rightUrl); + + int result = Uri.Compare(left, right, UriComponents.Fragment, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase); + return (result == 0); + } + #endregion } } diff --git a/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/LROPollState.cs b/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/LROPollState.cs index 7a28598634f1..cdf19684e804 100644 --- a/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/LROPollState.cs +++ b/src/SdkCommon/ClientRuntime.Azure/ClientRuntime.Azure/LRO/LROPollState.cs @@ -243,6 +243,14 @@ internal async Task> GetRawAsync( try { body = JObject.Parse(responseContent); + + // We only keep last serialization expcetion that occured in the last LRO poll cycle + // even if we got serialziation exception in the last iteration but the next response does not result + // + if(!string.IsNullOrEmpty(this.LastSerializationExceptionMessage)) + { + this.LastSerializationExceptionMessage = string.Empty; + } } catch(Exception ex) {