From 2e30dd0a2839b669ae6da09e9844a1150ad89741 Mon Sep 17 00:00:00 2001 From: Pallab Paul Date: Thu, 3 Aug 2023 14:16:17 -0700 Subject: [PATCH 1/3] Update PostLedgerEntryOperation retry logic --- .../CHANGELOG.md | 6 +++ .../assets.json | 2 +- .../Azure.Security.ConfidentialLedger.csproj | 2 +- .../src/PostLedgerEntryOperation.cs | 37 ++++++++++++++++--- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md index f29226a512f2..638d37c73a01 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## 1.2.0 (2023-08-03) + +### Bugs Fixed + +- Allow some `HttpStatusCode.NotFound` occurrences in `PostLedgerEntryOperation` to account for unexpected loss of session stickiness. These errors may occur when the connected node changes and transactions have not been fully replicated. + ## 1.2.0-beta.1 (Unreleased) ### Features Added diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/assets.json b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/assets.json index 445e1f9bf331..f19574db14f6 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/assets.json +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/confidentialledger/Azure.Security.ConfidentialLedger", - "Tag": "net/confidentialledger/Azure.Security.ConfidentialLedger_48488df791" + "Tag": "net/confidentialledger/Azure.Security.ConfidentialLedger_5657482b45" } diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj index 150fe921ab55..8fc08c597c37 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj @@ -2,7 +2,7 @@ Client SDK for the Azure Confidential Ledger service Azure Confidential Ledger - 1.2.0-beta.1 + 1.2.0 1.1.0 Azure ConfidentialLedger diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs index 372caaefec5e..7e3ce27aa22c 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs @@ -55,12 +55,36 @@ public override Response UpdateStatus(CancellationToken cancellationToken = defa async ValueTask IOperation.UpdateStateAsync(bool async, CancellationToken cancellationToken) { - var statusResponse = async - ? await _client.GetTransactionStatusAsync( - Id, - new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }) - .ConfigureAwait(false) - : _client.GetTransactionStatus(Id, new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }); + int retryCount = 0; + Azure.Response statusResponse = null; + while (retryCount < 3) + { + statusResponse = async + ? await _client.GetTransactionStatusAsync( + Id, + new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }) + .ConfigureAwait(false) + : _client.GetTransactionStatus(Id, new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }); + + // The transaction may not be found due to unexpected loss of session stickiness. + // This may occur when the connected node changes and transactions have not been fully replicated. + // We will perform retry logic to ensure that we have waited for the transactions to fully replicate before throwing an error. + if (statusResponse.Status == (int)HttpStatusCode.NotFound) + { + ++retryCount; + } + else + { + break; + } + + // Add a 0.5 second delay between retries. + if (async) { + await Task.Delay(500).ConfigureAwait(false); + } else { + Thread.Sleep(500); + } + } if (statusResponse.Status != (int)HttpStatusCode.OK) { @@ -76,6 +100,7 @@ async ValueTask IOperation.UpdateStateAsync(bool async, Cancella { return OperationState.Success(statusResponse); } + return OperationState.Pending(statusResponse); } From 0c445eb4e1f9ab993c491b645d843eab2c69e526 Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Thu, 7 Aug 2025 18:13:06 -0700 Subject: [PATCH 2/3] add secondary ledger uri init --- .../CHANGELOG.md | 5 ++ .../Azure.Security.ConfidentialLedger.csproj | 2 +- .../src/ConfidentialLedgerClient.cs | 25 +++++- .../src/ConfidentialLedgerClientOptions.cs | 6 ++ .../src/Generated/ConfidentialLedgerClient.cs | 87 ++++++++++++++++--- 5 files changed, 109 insertions(+), 16 deletions(-) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md index c1b61f43125d..ece0633d8fec 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 1.5.3-preview.1 (Unreleased) + +### Features Added +- Added secondaryLedgerUri parameter for failover support in ConfidentialLedgerClientOptions. + ## 1.4.1-beta.3 (Unreleased) ### Features Added diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj index 6c9e88527448..742088155259 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj @@ -2,7 +2,7 @@ Client SDK for the Azure Confidential Ledger service Azure Confidential Ledger - 1.4.1-beta.3 + 1.5.3-preview.1 1.3.0 Azure ConfidentialLedger diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs index 3971677f1d25..5118210fbbf4 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs @@ -81,6 +81,29 @@ internal ConfidentialLedgerClient(Uri ledgerEndpoint, TokenCredential credential new ConfidentialLedgerResponseClassifier()); _ledgerEndpoint = ledgerEndpoint; _apiVersion = actualOptions.Version; + + // Set up secondary endpoint if configured + _secondaryLedgerEndpoint = actualOptions.SecondaryLedgerEndpoint; + if (_secondaryLedgerEndpoint != null) + { + // Get certificate for secondary endpoint if needed + X509Certificate2 secondaryServiceCert = identityServiceCert ?? GetIdentityServerTlsCert(_secondaryLedgerEndpoint, certificateClientOptions ?? new ConfidentialLedgerCertificateClientOptions(), ledgerOptions: ledgerOptions).Cert; + + var secondaryTransportOptions = GetIdentityServerTlsCertAndTrust(secondaryServiceCert, ledgerOptions?.VerifyConnection ?? true); + if (clientCertificate != null) + { + secondaryTransportOptions.ClientCertificates.Add(clientCertificate); + } + + _secondaryPipeline = HttpPipelineBuilder.Build( + actualOptions, + Array.Empty(), + _tokenCredential == null ? + Array.Empty() : + new HttpPipelinePolicy[] { new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes) }, + secondaryTransportOptions, + new ConfidentialLedgerResponseClassifier()); + } } internal class ConfidentialLedgerResponseClassifier : ResponseClassifier @@ -154,7 +177,7 @@ public virtual Operation PostLedgerEntry( /// } /// /// - /// if the method should wait to return until the long-running operation has completed on the service; if it should return after starting the operation. For more information on long-running operations, please see Azure.Core Long-Running Operation samples. + /// if the method should wait to return until the long-running operation hascompleted on the service; if it should return after starting the operation. For more information on long-running operations, please see Azure.Core Long-Running Operation samples. /// The content to send as the body of the request. /// The collection id. /// The tags. diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs index 14c08ceb10e0..e0962fd663ef 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs @@ -25,6 +25,12 @@ public partial class ConfidentialLedgerClientOptions : ClientOptions /// public bool VerifyConnection { get; set; } + /// + /// The secondary ledger endpoint URL for failover scenarios. + /// + /// + public Uri SecondaryLedgerEndpoint { get; set; } + /// The version of the service to use. public enum ServiceVersion { diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs index 1cc7f7def116..2487e64ce957 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs @@ -23,6 +23,10 @@ public partial class ConfidentialLedgerClient private readonly Uri _ledgerEndpoint; private readonly string _apiVersion; + // Secondary endpoint support for failover + private readonly Uri _secondaryLedgerEndpoint; + private readonly HttpPipeline _secondaryPipeline; + /// The ClientDiagnostics is used to provide tracing support for the client library. internal ClientDiagnostics ClientDiagnostics { get; } @@ -252,8 +256,21 @@ public virtual async Task GetLedgerEntryAsync(string transactionId, st scope.Start(); try { - using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("primary endpoint failed: " + ex.Message); + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return await pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } } catch (Exception e) { @@ -288,8 +305,20 @@ public virtual Response GetLedgerEntry(string transactionId, string collectionId scope.Start(); try { - using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); - return _pipeline.ProcessMessage(message, context); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); + return _pipeline.ProcessMessage(message, context); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return pipeline.ProcessMessage(message, context); + } } catch (Exception e) { @@ -459,8 +488,20 @@ public virtual async Task GetCurrentLedgerEntryAsync(string collection scope.Start(); try { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return await pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } } catch (Exception e) { @@ -490,8 +531,20 @@ public virtual Response GetCurrentLedgerEntry(string collectionId = null, Reques scope.Start(); try { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); - return _pipeline.ProcessMessage(message, context); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); + return _pipeline.ProcessMessage(message, context); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return pipeline.ProcessMessage(message, context); + } } catch (Exception e) { @@ -2168,13 +2221,16 @@ internal HttpMessage CreateCreateLedgerEntryRequest(RequestContent content, stri return message; } - internal HttpMessage CreateGetLedgerEntryRequest(string transactionId, string collectionId, RequestContext context) + internal HttpMessage CreateGetLedgerEntryRequest(string transactionId, string collectionId, RequestContext context, bool useSecondaryEndpoint = false) { - var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var endpoint = useSecondaryEndpoint && _secondaryLedgerEndpoint != null ? _secondaryLedgerEndpoint : _ledgerEndpoint; + var pipeline = useSecondaryEndpoint && _secondaryPipeline != null ? _secondaryPipeline : _pipeline; + + var message = pipeline.CreateMessage(context, ResponseClassifier200); var request = message.Request; request.Method = RequestMethod.Get; var uri = new RawRequestUriBuilder(); - uri.Reset(_ledgerEndpoint); + uri.Reset(endpoint); uri.AppendPath("/app/transactions/", false); uri.AppendPath(transactionId, true); uri.AppendQuery("api-version", _apiVersion, true); @@ -2219,13 +2275,16 @@ internal HttpMessage CreateGetTransactionStatusRequest(string transactionId, Req return message; } - internal HttpMessage CreateGetCurrentLedgerEntryRequest(string collectionId, RequestContext context) + internal HttpMessage CreateGetCurrentLedgerEntryRequest(string collectionId, RequestContext context, bool useSecondaryEndpoint = false) { - var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var endpoint = useSecondaryEndpoint && _secondaryLedgerEndpoint != null ? _secondaryLedgerEndpoint : _ledgerEndpoint; + var pipeline = useSecondaryEndpoint && _secondaryPipeline != null ? _secondaryPipeline : _pipeline; + + var message = pipeline.CreateMessage(context, ResponseClassifier200); var request = message.Request; request.Method = RequestMethod.Get; var uri = new RawRequestUriBuilder(); - uri.Reset(_ledgerEndpoint); + uri.Reset(endpoint); uri.AppendPath("/app/transactions/current", false); uri.AppendQuery("api-version", _apiVersion, true); if (collectionId != null) From ff7c0869ecac8fb35b1ce00636c3a01ead17f5f5 Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Thu, 7 Aug 2025 18:17:47 -0700 Subject: [PATCH 3/3] clean --- .../src/PostLedgerEntryOperation.cs | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs index 11e73e1d648b..7fb2743698f5 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/PostLedgerEntryOperation.cs @@ -53,36 +53,12 @@ public override Response UpdateStatus(CancellationToken cancellationToken = defa async ValueTask IOperation.UpdateStateAsync(bool async, CancellationToken cancellationToken) { - int retryCount = 0; - Azure.Response statusResponse = null; - while (retryCount < 3) - { - statusResponse = async - ? await _client.GetTransactionStatusAsync( - Id, - new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }) - .ConfigureAwait(false) - : _client.GetTransactionStatus(Id, new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }); - - // The transaction may not be found due to unexpected loss of session stickiness. - // This may occur when the connected node changes and transactions have not been fully replicated. - // We will perform retry logic to ensure that we have waited for the transactions to fully replicate before throwing an error. - if (statusResponse.Status == (int)HttpStatusCode.NotFound) - { - ++retryCount; - } - else - { - break; - } - - // Add a 0.5 second delay between retries. - if (async) { - await Task.Delay(500).ConfigureAwait(false); - } else { - Thread.Sleep(500); - } - } + var statusResponse = async + ? await _client.GetTransactionStatusAsync( + Id, + new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }) + .ConfigureAwait(false) + : _client.GetTransactionStatus(Id, new RequestContext { CancellationToken = cancellationToken, ErrorOptions = ErrorOptions.NoThrow }); if (statusResponse.Status != (int)HttpStatusCode.OK) { @@ -98,7 +74,6 @@ async ValueTask IOperation.UpdateStateAsync(bool async, Cancella { return OperationState.Success(statusResponse); } - return OperationState.Pending(statusResponse); }