From 2e30dd0a2839b669ae6da09e9844a1150ad89741 Mon Sep 17 00:00:00 2001 From: Pallab Paul Date: Thu, 3 Aug 2023 14:16:17 -0700 Subject: [PATCH 1/7] 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 2fa1b35f607b7141a2cd7bcf3ce14e345440b21a Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Tue, 12 Aug 2025 10:35:38 -0700 Subject: [PATCH 2/7] add base failover endpoint implementation --- .../src/Generated/ConfidentialLedgerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs index 1cc7f7def116..490ddc363792 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs @@ -22,6 +22,7 @@ public partial class ConfidentialLedgerClient private readonly HttpPipeline _pipeline; private readonly Uri _ledgerEndpoint; private readonly string _apiVersion; + private readonly ConfidentialLedgerFailoverService _failoverService; /// The ClientDiagnostics is used to provide tracing support for the client library. internal ClientDiagnostics ClientDiagnostics { get; } From af7213a8c9c318938338b32e57eedf0509f0aeb5 Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Tue, 12 Aug 2025 10:37:11 -0700 Subject: [PATCH 3/7] add base failover implementation --- .../CHANGELOG.md | 5 + .../Azure.Security.ConfidentialLedger.csproj | 2 +- .../src/ConfidentialLedgerClient.cs | 1 + .../src/ConfidentialLedgerFailoverService.cs | 294 ++++++++++++++++++ .../src/Generated/ConfidentialLedgerClient.cs | 46 ++- 5 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md index c1b61f43125d..833fa0598be3 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.0-beta.1 (Unreleased) + +### Features Added +- Added support for routing to failover ledgers. + ## 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..54ba2535b2d3 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.0-beta.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..a70f941c1dee 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs @@ -81,6 +81,7 @@ internal ConfidentialLedgerClient(Uri ledgerEndpoint, TokenCredential credential new ConfidentialLedgerResponseClassifier()); _ledgerEndpoint = ledgerEndpoint; _apiVersion = actualOptions.Version; + _failoverService = new ConfidentialLedgerFailoverService(_pipeline, ClientDiagnostics); } internal class ConfidentialLedgerResponseClassifier : ResponseClassifier diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs new file mode 100644 index 000000000000..cfe839919a70 --- /dev/null +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Azure.Security.ConfidentialLedger +{ + internal class ConfidentialLedgerFailoverService + { + private readonly HttpPipeline _pipeline; + private readonly ClientDiagnostics _clientDiagnostics; + + private static ResponseClassifier _responseClassifier200; + private static ResponseClassifier ResponseClassifier200 => _responseClassifier200 ??= new StatusCodeClassifier(stackalloc ushort[] { 200 }); + + public ConfidentialLedgerFailoverService(HttpPipeline pipeline, ClientDiagnostics clientDiagnostics) + { + _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); + _clientDiagnostics = clientDiagnostics ?? throw new ArgumentNullException(nameof(clientDiagnostics)); + } + + public async Task ExecuteWithFailoverAsync( + Uri primaryEndpoint, + Func> operationAsync, + string operationName, + CancellationToken cancellationToken = default) + { + using var scope = _clientDiagnostics.CreateScope($"ConfidentialLedgerClient.{operationName}"); + scope.Start(); + + Exception lastException = null; + + try + { + Console.WriteLine($"[Failover] Primary attempt for {operationName} at {primaryEndpoint}"); + cancellationToken.ThrowIfCancellationRequested(); + return await operationAsync(primaryEndpoint).ConfigureAwait(false); + } + catch (RequestFailedException ex) when (IsRetriableFailure(ex)) + { + Console.WriteLine($"[Failover] Primary failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Will attempt failover."); + lastException = ex; + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + Console.WriteLine("[Failover] Primary attempt timeout. Will attempt failover."); + lastException = ex; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + + Console.WriteLine("[Failover] Discovering failover endpoints (async)..."); + List failoverEndpoints = await GetFailoverEndpointsAsync(primaryEndpoint, cancellationToken).ConfigureAwait(false); + Console.WriteLine($"[Failover] Found {failoverEndpoints.Count} failover endpoint(s)."); + + foreach (Uri endpoint in failoverEndpoints) + { + Console.WriteLine($"[Failover] Attempting {operationName} on {endpoint}"); + try + { + cancellationToken.ThrowIfCancellationRequested(); + return await operationAsync(endpoint).ConfigureAwait(false); + } + catch (RequestFailedException ex) when (IsRetriableFailure(ex)) + { + Console.WriteLine($"[Failover] Endpoint {endpoint} failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Trying next."); + lastException = ex; + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + Console.WriteLine($"[Failover] Endpoint {endpoint} timeout. Trying next."); + lastException = ex; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + } + + scope.Failed(lastException); + throw lastException ?? new RequestFailedException("All endpoints failed"); + } + + public T ExecuteWithFailover( + Uri primaryEndpoint, + Func operationSync, + string operationName, + CancellationToken cancellationToken = default) + { + Console.WriteLine($"Executing operation {operationName} on primary endpoint: {primaryEndpoint}"); + using var scope = _clientDiagnostics.CreateScope($"ConfidentialLedgerClient.{operationName}"); + scope.Start(); + + Exception lastException = null; + + try + { + Console.WriteLine($"[Failover] Primary attempt for {operationName} at {primaryEndpoint}"); + cancellationToken.ThrowIfCancellationRequested(); + return operationSync(primaryEndpoint); + } + catch (RequestFailedException ex) when (IsRetriableFailure(ex)) + { + Console.WriteLine($"[Failover] Primary failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Will attempt failover."); + lastException = ex; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + + Console.WriteLine("[Failover] Discovering failover endpoints (sync)..."); + List failoverEndpoints = GetFailoverEndpoints(primaryEndpoint, cancellationToken); + Console.WriteLine($"[Failover] Found {failoverEndpoints.Count} failover endpoint(s)."); + + foreach (Uri endpoint in failoverEndpoints) + { + try + { + Console.WriteLine($"[Failover] Attempting {operationName} on {endpoint}"); + cancellationToken.ThrowIfCancellationRequested(); + return operationSync(endpoint); + } + catch (RequestFailedException ex) when (IsRetriableFailure(ex)) + { + Console.WriteLine($"[Failover] Endpoint {endpoint} failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Trying next."); + lastException = ex; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + } + + scope.Failed(lastException); + throw lastException ?? new RequestFailedException("All endpoints failed"); + } + + private async Task> GetFailoverEndpointsAsync( + Uri primaryEndpoint, + CancellationToken cancellationToken = default) + { + var failoverEndpoints = new List(); + + try + { + string ledgerId = primaryEndpoint.Host.Substring(0, primaryEndpoint.Host.IndexOf('.')); + + Uri failoverUrl = new UriBuilder(primaryEndpoint) + { + Host = "localhost", // update when failover endpoint logic is merged in + Path = $"/failover/{ledgerId}" + }.Uri; + + using HttpMessage message = CreateFailoverRequest(failoverUrl); + Response response = await _pipeline.ProcessMessageAsync(message, new RequestContext()).ConfigureAwait(false); + + if (response.Status == 200) + { + Console.WriteLine("[Failover] Metadata request succeeded."); + using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); + if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) + { + int count = 0; + foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) + { + string failoverLedgerId = failoverLedger.GetString(); + if (!string.IsNullOrEmpty(failoverLedgerId)) + { + Uri endpoint = new UriBuilder(primaryEndpoint) + { + Host = $"{failoverLedgerId}.confidential-ledger.azure.com" + }.Uri; + failoverEndpoints.Add(endpoint); + count++; + } + } + Console.WriteLine($"[Failover] Parsed {count} failover ledger id(s)."); + } + else + { + Console.WriteLine("[Failover] No 'failoverLedgers' property in metadata response."); + } + } + else + { + Console.WriteLine($"[Failover] Metadata request returned status {response.Status}. No endpoints extracted."); + } + } + catch (Exception ex) + { + Console.WriteLine($"[Failover] Suppressed exception during metadata retrieval: {ex.Message}"); + } + + return failoverEndpoints; + } + + private List GetFailoverEndpoints( + Uri primaryEndpoint, + CancellationToken cancellationToken = default) + { + var failoverEndpoints = new List(); + + try + { + Console.WriteLine($"Retrieving failover endpoints for primary endpoint: {primaryEndpoint}"); + string ledgerId = primaryEndpoint.Host.Substring(0, primaryEndpoint.Host.IndexOf('.')); + + Uri failoverUrl = new UriBuilder(primaryEndpoint) + { + Host = "localhost", + Path = $"/failover/{ledgerId}" + }.Uri; + + using HttpMessage message = CreateFailoverRequest(failoverUrl); + Response response = _pipeline.ProcessMessage(message, new RequestContext()); + + if (response.Status == 200) + { + Console.WriteLine("[Failover] Metadata request succeeded."); + using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); + if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) + { + int count = 0; + foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) + { + string failoverLedgerId = failoverLedger.GetString(); + if (!string.IsNullOrEmpty(failoverLedgerId)) + { + Uri endpoint = new UriBuilder(primaryEndpoint) + { + Host = $"{failoverLedgerId}.confidentialledger.azure.com" + }.Uri; + failoverEndpoints.Add(endpoint); + count++; + } + } + Console.WriteLine($"[Failover] Parsed {count} failover ledger id(s)."); + } + else + { + Console.WriteLine("[Failover] No 'failoverLedgers' property in metadata response."); + } + } + else + { + Console.WriteLine($"[Failover] Metadata request returned status {response.Status}. No endpoints extracted."); + } + } + catch (Exception ex) + { + Console.WriteLine($"[Failover] Suppressed exception during metadata retrieval: {ex.Message}"); + } + + return failoverEndpoints; + } + + private HttpMessage CreateFailoverRequest(Uri failoverUrl) + { + HttpMessage message = _pipeline.CreateMessage(new RequestContext(), ResponseClassifier200); + Request request = message.Request; + + request.Method = RequestMethod.Get; + + var uri = new RawRequestUriBuilder(); + uri.Reset(failoverUrl); + request.Uri = uri; + + request.Headers.Add("Accept", "application/json"); + + return message; + } + + private static bool IsRetriableFailure(RequestFailedException ex) + { + // Include 404 and specific UnknownLedgerEntry error code. + return ex.Status == 404 || + string.Equals(ex.ErrorCode, "UnknownLedgerEntry", StringComparison.OrdinalIgnoreCase) || + ex.Status >= 500 || + ex.Status == 408 || + ex.Status == 429 || + ex.Status == 503 || + ex.Status == 504; + } + } +} diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs index 490ddc363792..e5e75ca1b162 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs @@ -6,6 +6,8 @@ #nullable disable using System; +using System.Collections.Generic; +using System.Text.Json; using System.Threading.Tasks; using Autorest.CSharp.Core; using Azure.Core; @@ -253,8 +255,16 @@ 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); + Console.WriteLine("[GetLedgerEntryAsync] Starting request. txn={0} collection={1}", transactionId, collectionId); + return await _failoverService.ExecuteWithFailoverAsync( + _ledgerEndpoint, + async (endpoint) => + { + using HttpMessage message = CreateGetLedgerEntryRequest(endpoint, transactionId, collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + }, + nameof(GetLedgerEntryAsync), + context?.CancellationToken ?? default).ConfigureAwait(false); } catch (Exception e) { @@ -289,8 +299,16 @@ public virtual Response GetLedgerEntry(string transactionId, string collectionId scope.Start(); try { - using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); - return _pipeline.ProcessMessage(message, context); + var resp = _failoverService.ExecuteWithFailover( + _ledgerEndpoint, + (endpoint) => + { + using HttpMessage message = CreateGetLedgerEntryRequest(endpoint, transactionId, collectionId, context); + return _pipeline.ProcessMessage(message, context); + }, + nameof(GetLedgerEntry), + context?.CancellationToken ?? default); + return resp; } catch (Exception e) { @@ -2188,6 +2206,26 @@ internal HttpMessage CreateGetLedgerEntryRequest(string transactionId, string co return message; } + // Overload used for failover calls against alternate ledger endpoints. + internal HttpMessage CreateGetLedgerEntryRequest(Uri endpoint, string transactionId, string collectionId, RequestContext context) + { + var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var request = message.Request; + request.Method = RequestMethod.Get; + var uri = new RawRequestUriBuilder(); + uri.Reset(endpoint); + uri.AppendPath("/app/transactions/", false); + uri.AppendPath(transactionId, true); + uri.AppendQuery("api-version", _apiVersion, true); + if (collectionId != null) + { + uri.AppendQuery("collectionId", collectionId, true); + } + request.Uri = uri; + request.Headers.Add("Accept", "application/json"); + return message; + } + internal HttpMessage CreateGetReceiptRequest(string transactionId, RequestContext context) { var message = _pipeline.CreateMessage(context, ResponseClassifier200); From cb79689e9abf210067275410cc804784797101d1 Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Tue, 12 Aug 2025 10:40:04 -0700 Subject: [PATCH 4/7] 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); } From bf8d04e96b22c7482d03d718afe7dbd5f2e48b2f Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Wed, 13 Aug 2025 16:46:49 -0700 Subject: [PATCH 5/7] address collection id cases --- .../src/ConfidentialLedgerFailoverService.cs | 204 ++++++++++++++++- .../src/Generated/ConfidentialLedgerClient.cs | 205 +++++++++++++++++- 2 files changed, 396 insertions(+), 13 deletions(-) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs index cfe839919a70..442d4dde5d81 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs @@ -144,6 +144,37 @@ public T ExecuteWithFailover( throw lastException ?? new RequestFailedException("All endpoints failed"); } + // Overloads with collectionId gating: if collectionId is null/empty, skip failover entirely. + public Task ExecuteWithFailoverAsync( + Uri primaryEndpoint, + Func> operationAsync, + string operationName, + string collectionIdGate, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(collectionIdGate)) + { + Console.WriteLine($"[Failover] collectionId not provided; skipping failover for {operationName} and using primary endpoint {primaryEndpoint}"); + return operationAsync(primaryEndpoint); + } + return ExecuteWithFailoverAsync(primaryEndpoint, operationAsync, operationName, cancellationToken); + } + + public T ExecuteWithFailover( + Uri primaryEndpoint, + Func operationSync, + string operationName, + string collectionIdGate, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(collectionIdGate)) + { + Console.WriteLine($"[Failover] collectionId not provided; skipping failover for {operationName} and using primary endpoint {primaryEndpoint}"); + return operationSync(primaryEndpoint); + } + return ExecuteWithFailover(primaryEndpoint, operationSync, operationName, cancellationToken); + } + private async Task> GetFailoverEndpointsAsync( Uri primaryEndpoint, CancellationToken cancellationToken = default) @@ -156,7 +187,7 @@ private async Task> GetFailoverEndpointsAsync( Uri failoverUrl = new UriBuilder(primaryEndpoint) { - Host = "localhost", // update when failover endpoint logic is merged in + Host = "localhost", Path = $"/failover/{ledgerId}" }.Uri; @@ -167,12 +198,59 @@ private async Task> GetFailoverEndpointsAsync( { Console.WriteLine("[Failover] Metadata request succeeded."); using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); + if (jsonDoc.RootElement.TryGetProperty("ledgerId", out JsonElement primaryLedgerIdElem) && primaryLedgerIdElem.ValueKind == JsonValueKind.String) + { + Console.WriteLine($"[Failover] Metadata primary ledgerId: {primaryLedgerIdElem.GetString()}"); + } + if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) { int count = 0; foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) { - string failoverLedgerId = failoverLedger.GetString(); + string failoverLedgerId = null; + string access = null; + try + { + switch (failoverLedger.ValueKind) + { + case JsonValueKind.String: + failoverLedgerId = failoverLedger.GetString(); + break; + case JsonValueKind.Object: + // Expected shape: { "name": "ledgerName", ... } + if (failoverLedger.TryGetProperty("name", out JsonElement nameProp) && nameProp.ValueKind == JsonValueKind.String) + { + failoverLedgerId = nameProp.GetString(); + } + else + { + // Fall back: look for any string property that could represent the id + foreach (JsonProperty prop in failoverLedger.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.String && + string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) + { + failoverLedgerId = prop.Value.GetString(); + break; + } + } + } + if (failoverLedger.TryGetProperty("access", out JsonElement accessProp) && accessProp.ValueKind == JsonValueKind.String) + { + access = accessProp.GetString(); + } + break; + default: + Console.WriteLine($"[Failover] Unexpected element kind {failoverLedger.ValueKind} in failoverLedgers array; skipping."); + break; + } + } + catch (Exception exElem) + { + Console.WriteLine($"[Failover] Suppressed exception parsing failover ledger element: {exElem.Message}"); + } + if (!string.IsNullOrEmpty(failoverLedgerId)) { Uri endpoint = new UriBuilder(primaryEndpoint) @@ -180,8 +258,13 @@ private async Task> GetFailoverEndpointsAsync( Host = $"{failoverLedgerId}.confidential-ledger.azure.com" }.Uri; failoverEndpoints.Add(endpoint); + Console.WriteLine($"[Failover] Added failover endpoint {endpoint} (access={access ?? "unknown"})"); count++; } + else + { + Console.WriteLine("[Failover] Could not extract ledger id from element; skipping."); + } } Console.WriteLine($"[Failover] Parsed {count} failover ledger id(s)."); } @@ -227,21 +310,71 @@ private List GetFailoverEndpoints( { Console.WriteLine("[Failover] Metadata request succeeded."); using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); + if (jsonDoc.RootElement.TryGetProperty("ledgerId", out JsonElement primaryLedgerIdElem) && primaryLedgerIdElem.ValueKind == JsonValueKind.String) + { + Console.WriteLine($"[Failover] Metadata primary ledgerId: {primaryLedgerIdElem.GetString()}"); + } + if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) { int count = 0; foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) { - string failoverLedgerId = failoverLedger.GetString(); + string failoverLedgerId = null; + string access = null; + try + { + switch (failoverLedger.ValueKind) + { + case JsonValueKind.String: + failoverLedgerId = failoverLedger.GetString(); + break; + case JsonValueKind.Object: + if (failoverLedger.TryGetProperty("name", out JsonElement nameProp) && nameProp.ValueKind == JsonValueKind.String) + { + failoverLedgerId = nameProp.GetString(); + } + else + { + foreach (JsonProperty prop in failoverLedger.EnumerateObject()) + { + if (prop.Value.ValueKind == JsonValueKind.String && + string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) + { + failoverLedgerId = prop.Value.GetString(); + break; + } + } + } + if (failoverLedger.TryGetProperty("access", out JsonElement accessProp) && accessProp.ValueKind == JsonValueKind.String) + { + access = accessProp.GetString(); + } + break; + default: + Console.WriteLine($"[Failover] Unexpected element kind {failoverLedger.ValueKind} in failoverLedgers array; skipping."); + break; + } + } + catch (Exception exElem) + { + Console.WriteLine($"[Failover] Suppressed exception parsing failover ledger element: {exElem.Message}"); + } + if (!string.IsNullOrEmpty(failoverLedgerId)) { Uri endpoint = new UriBuilder(primaryEndpoint) { - Host = $"{failoverLedgerId}.confidentialledger.azure.com" + Host = $"{failoverLedgerId}.confidential-ledger.azure.com" }.Uri; failoverEndpoints.Add(endpoint); + Console.WriteLine($"[Failover] Added failover endpoint {endpoint} (access={access ?? "unknown"})"); count++; } + else + { + Console.WriteLine("[Failover] Could not extract ledger id from element; skipping."); + } } Console.WriteLine($"[Failover] Parsed {count} failover ledger id(s)."); } @@ -290,5 +423,68 @@ private static bool IsRetriableFailure(RequestFailedException ex) ex.Status == 503 || ex.Status == 504; } + + // Execute an operation only against discovered failover endpoints (skips primary). Used for specialized fallback flows. + public async Task ExecuteOnFailoversOnlyAsync( + Uri primaryEndpoint, + Func> operationAsync, + string operationName, + CancellationToken cancellationToken = default) + { + Console.WriteLine($"[Failover] Discovering failover endpoints for {operationName} (failovers-only mode)..."); + List endpoints = await GetFailoverEndpointsAsync(primaryEndpoint, cancellationToken).ConfigureAwait(false); + Console.WriteLine($"[Failover] Found {endpoints.Count} failover endpoint(s) for {operationName}."); + Exception last = null; + foreach (var ep in endpoints) + { + Console.WriteLine($"[Failover] (FailoversOnly) Attempting {operationName} on {ep}"); + try + { + cancellationToken.ThrowIfCancellationRequested(); + return await operationAsync(ep).ConfigureAwait(false); + } + catch (RequestFailedException ex) when (IsRetriableFailure(ex)) + { + Console.WriteLine($"[Failover] (FailoversOnly) Endpoint {ep} failed (Status {ex.Status}, Code {ex.ErrorCode}). Trying next."); + last = ex; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + } + throw last ?? new RequestFailedException("All failover endpoints failed in failovers-only mode"); + } + + public T ExecuteOnFailoversOnly( + Uri primaryEndpoint, + Func operationSync, + string operationName, + CancellationToken cancellationToken = default) + { + Console.WriteLine($"[Failover] Discovering failover endpoints for {operationName} (failovers-only mode)..."); + List endpoints = GetFailoverEndpoints(primaryEndpoint, cancellationToken); + Console.WriteLine($"[Failover] Found {endpoints.Count} failover endpoint(s) for {operationName}."); + Exception last = null; + foreach (var ep in endpoints) + { + Console.WriteLine($"[Failover] (FailoversOnly) Attempting {operationName} on {ep}"); + try + { + cancellationToken.ThrowIfCancellationRequested(); + return operationSync(ep); + } + catch (RequestFailedException ex) when (IsRetriableFailure(ex)) + { + Console.WriteLine($"[Failover] (FailoversOnly) Endpoint {ep} failed (Status {ex.Status}, Code {ex.ErrorCode}). Trying next."); + last = ex; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + } + throw last ?? new RequestFailedException("All failover endpoints failed in failovers-only mode"); + } } } diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs index e5e75ca1b162..91ac111baa7e 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs @@ -4,6 +4,7 @@ // #nullable disable +using System.IO; using System; using System.Collections.Generic; @@ -255,15 +256,49 @@ public virtual async Task GetLedgerEntryAsync(string transactionId, st scope.Start(); try { - Console.WriteLine("[GetLedgerEntryAsync] Starting request. txn={0} collection={1}", transactionId, collectionId); + Console.WriteLine("[GetLedgerEntryAsync] Starting request. txn={0} collection={1}", transactionId, collectionId); + // Primary attempt with fallback to current entry before invoking failover service. + try + { + using HttpMessage primaryMessage = CreateGetLedgerEntryRequest(_ledgerEndpoint, transactionId, collectionId, context); + Response primaryResponse = await _pipeline.ProcessMessageAsync(primaryMessage, context).ConfigureAwait(false); + // Success on primary; return immediately. + return primaryResponse; + } + catch (RequestFailedException ex) when (ShouldFallbackToCurrent(ex) && !string.IsNullOrEmpty(collectionId)) + { + Console.WriteLine("[GetLedgerEntryAsync] Primary returned not-found/unknown for txn {0}. Attempting GetCurrentLedgerEntry across failover endpoints for collection {1}.", transactionId, collectionId); + try + { + Response currentResponse = await _failoverService.ExecuteOnFailoversOnlyAsync( + _ledgerEndpoint, + async (endpoint) => + { + using HttpMessage msg = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return await _pipeline.ProcessMessageAsync(msg, context).ConfigureAwait(false); + }, + nameof(GetCurrentLedgerEntryAsync), + context?.CancellationToken ?? default).ConfigureAwait(false); + Response synthesized = SynthesizeLedgerEntryFromCurrent(currentResponse); + Console.WriteLine("[GetLedgerEntryAsync] Failover current-entry fallback succeeded."); + return synthesized; + } + catch (Exception fallbackFailoverEx) + { + Console.WriteLine("[GetLedgerEntryAsync] Failover current-entry fallback failed: {0}. Proceeding to full failover attempts with original txn.", fallbackFailoverEx.Message); + } + } + + // If we got here, either primary failed with retriable condition (without fallback) or fallback failed; use failover service. return await _failoverService.ExecuteWithFailoverAsync( _ledgerEndpoint, async (endpoint) => { using HttpMessage message = CreateGetLedgerEntryRequest(endpoint, transactionId, collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); }, nameof(GetLedgerEntryAsync), + collectionId, context?.CancellationToken ?? default).ConfigureAwait(false); } catch (Exception e) @@ -299,16 +334,47 @@ public virtual Response GetLedgerEntry(string transactionId, string collectionId scope.Start(); try { - var resp = _failoverService.ExecuteWithFailover( + Console.WriteLine("[GetLedgerEntry] Starting request. txn={0} collection={1}", transactionId, collectionId); + // Primary attempt with fallback + try + { + using HttpMessage primaryMessage = CreateGetLedgerEntryRequest(_ledgerEndpoint, transactionId, collectionId, context); + Response primaryResponse = _pipeline.ProcessMessage(primaryMessage, context); + return primaryResponse; + } + catch (RequestFailedException ex) when (ShouldFallbackToCurrent(ex) && !string.IsNullOrEmpty(collectionId)) + { + Console.WriteLine("[GetLedgerEntry] Primary returned not-found/unknown for txn {0}. Attempting GetCurrentLedgerEntry across failover endpoints for collection {1}.", transactionId, collectionId); + try + { + Response currentResponse = _failoverService.ExecuteOnFailoversOnly( + _ledgerEndpoint, + (endpoint) => + { + using HttpMessage msg = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return _pipeline.ProcessMessage(msg, context); + }, + nameof(GetCurrentLedgerEntry), + context?.CancellationToken ?? default); + Response synthesized = SynthesizeLedgerEntryFromCurrent(currentResponse); + Console.WriteLine("[GetLedgerEntry] Failover current-entry fallback succeeded."); + return synthesized; + } + catch (Exception fallbackFailoverEx) + { + Console.WriteLine("[GetLedgerEntry] Failover current-entry fallback failed: {0}. Proceeding to full failover attempts with original txn.", fallbackFailoverEx.Message); + } + } + return _failoverService.ExecuteWithFailover( _ledgerEndpoint, (endpoint) => { using HttpMessage message = CreateGetLedgerEntryRequest(endpoint, transactionId, collectionId, context); - return _pipeline.ProcessMessage(message, context); + return _pipeline.ProcessMessage(message, context); }, nameof(GetLedgerEntry), + collectionId, context?.CancellationToken ?? default); - return resp; } catch (Exception e) { @@ -478,8 +544,17 @@ public virtual async Task GetCurrentLedgerEntryAsync(string collection scope.Start(); try { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + Console.WriteLine("[GetCurrentLedgerEntryAsync] Starting request. collection={0}", collectionId); + return await _failoverService.ExecuteWithFailoverAsync( + _ledgerEndpoint, + async (endpoint) => + { + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + }, + nameof(GetCurrentLedgerEntryAsync), + collectionId, + context?.CancellationToken ?? default).ConfigureAwait(false); } catch (Exception e) { @@ -509,8 +584,17 @@ public virtual Response GetCurrentLedgerEntry(string collectionId = null, Reques scope.Start(); try { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); - return _pipeline.ProcessMessage(message, context); + Console.WriteLine("[GetCurrentLedgerEntry] Starting request. collection={0}", collectionId); + return _failoverService.ExecuteWithFailover( + _ledgerEndpoint, + (endpoint) => + { + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return _pipeline.ProcessMessage(message, context); + }, + nameof(GetCurrentLedgerEntry), + collectionId, + context?.CancellationToken ?? default); } catch (Exception e) { @@ -2276,6 +2360,25 @@ internal HttpMessage CreateGetCurrentLedgerEntryRequest(string collectionId, Req return message; } + // Overload used for failover calls against alternate ledger endpoints. + internal HttpMessage CreateGetCurrentLedgerEntryRequest(Uri endpoint, string collectionId, RequestContext context) + { + var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var request = message.Request; + request.Method = RequestMethod.Get; + var uri = new RawRequestUriBuilder(); + uri.Reset(endpoint); + uri.AppendPath("/app/transactions/current", false); + uri.AppendQuery("api-version", _apiVersion, true); + if (collectionId != null) + { + uri.AppendQuery("collectionId", collectionId, true); + } + request.Uri = uri; + request.Headers.Add("Accept", "application/json"); + return message; + } + internal HttpMessage CreateGetUsersRequest(RequestContext context) { var message = _pipeline.CreateMessage(context, ResponseClassifier200); @@ -2700,5 +2803,89 @@ internal HttpMessage CreateGetUserDefinedFunctionsNextPageRequest(string nextLin private static ResponseClassifier ResponseClassifier201 => _responseClassifier201 ??= new StatusCodeClassifier(stackalloc ushort[] { 201 }); private static ResponseClassifier _responseClassifier200201; private static ResponseClassifier ResponseClassifier200201 => _responseClassifier200201 ??= new StatusCodeClassifier(stackalloc ushort[] { 200, 201 }); + + private static bool ShouldFallbackToCurrent(RequestFailedException ex) + { + if (ex == null) return false; + if (ex.Status == 404) return true; + if (string.Equals(ex.ErrorCode, "UnknownLedgerEntry", StringComparison.OrdinalIgnoreCase)) return true; + return false; + } + + // Synthesize a GetLedgerEntry-shaped response from a GetCurrentLedgerEntry response body. + // Expected current entry body: { "collectionId":"...", "contents":"...", "transactionId":"..." } + // Desired ledger entry body: { "entry": { same fields }, "state": "Ready" } + private Response SynthesizeLedgerEntryFromCurrent(Response currentResponse) + { + try + { + if (currentResponse?.ContentStream == null) + { + return currentResponse; // nothing to do + } + currentResponse.ContentStream.Position = 0; + using (var doc = System.Text.Json.JsonDocument.Parse(currentResponse.ContentStream)) + { + var root = doc.RootElement; + using var ms = new System.IO.MemoryStream(); + using (var writer = new System.Text.Json.Utf8JsonWriter(ms)) + { + writer.WriteStartObject(); + writer.WritePropertyName("entry"); + writer.WriteStartObject(); + if (root.TryGetProperty("collectionId", out var col)) writer.WriteString("collectionId", col.GetString()); + if (root.TryGetProperty("contents", out var contents)) writer.WriteString("contents", contents.GetString()); + if (root.TryGetProperty("transactionId", out var tx)) writer.WriteString("transactionId", tx.GetString()); + writer.WriteEndObject(); + writer.WriteString("state", "Ready"); + writer.WriteEndObject(); + } + ms.Position = 0; + // Wrap in a synthetic Response that mimics the original status/headers but with new content. + return new SyntheticResponse(currentResponse, ms.ToArray()); + } + } + catch (Exception ex) + { + Console.WriteLine($"[GetLedgerEntry] Failed to synthesize response from current entry: {ex.Message}"); + return currentResponse; // fall back to original + } + } + + private sealed class SyntheticResponse : Response + { + private readonly Response _inner; + private readonly byte[] _content; + private System.IO.MemoryStream _stream; + + public SyntheticResponse(Response inner, byte[] content) + { + _inner = inner; + _content = content ?? Array.Empty(); + _stream = new System.IO.MemoryStream(_content, writable: false); + } + + public override int Status => _inner.Status; + public override string ReasonPhrase => _inner.ReasonPhrase; + public override Stream ContentStream + { + get => _stream; + set => _stream = value as System.IO.MemoryStream ?? new System.IO.MemoryStream(); + } + public override string ClientRequestId + { + get => _inner.ClientRequestId; + set => _inner.ClientRequestId = value; + } + public override void Dispose() + { + _stream?.Dispose(); + _inner?.Dispose(); + } + protected override bool ContainsHeader(string name) => _inner.Headers.Contains(name); + protected override IEnumerable EnumerateHeaders() => _inner.Headers; + protected override bool TryGetHeader(string name, out string value) => _inner.Headers.TryGetValue(name, out value); + protected override bool TryGetHeaderValues(string name, out IEnumerable values) => _inner.Headers.TryGetValues(name, out values); + } } } From 19870c26a1aed775105ebd4a32ca16591efe9afe Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Thu, 14 Aug 2025 15:21:09 -0700 Subject: [PATCH 6/7] cleaned up failover service and client logic --- .../CHANGELOG.md | 2 +- .../src/ConfidentialLedgerFailoverService.cs | 264 ++---------------- .../src/Generated/ConfidentialLedgerClient.cs | 179 +++++------- 3 files changed, 103 insertions(+), 342 deletions(-) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md index 833fa0598be3..7e81615b1281 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md @@ -3,7 +3,7 @@ ## 1.5.0-beta.1 (Unreleased) ### Features Added -- Added support for routing to failover ledgers. +- Added support to route to failover ledgers for the `GetLedgerEntry`, `GetLedgerEntryAsync`, `GetCurrentLedgerEntry`, and `GetCurrentLedgerEntryAsync` methods. ## 1.4.1-beta.3 (Unreleased) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs index 442d4dde5d81..7183b520b260 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs @@ -24,128 +24,8 @@ public ConfidentialLedgerFailoverService(HttpPipeline pipeline, ClientDiagnostic _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); _clientDiagnostics = clientDiagnostics ?? throw new ArgumentNullException(nameof(clientDiagnostics)); } - - public async Task ExecuteWithFailoverAsync( - Uri primaryEndpoint, - Func> operationAsync, - string operationName, - CancellationToken cancellationToken = default) - { - using var scope = _clientDiagnostics.CreateScope($"ConfidentialLedgerClient.{operationName}"); - scope.Start(); - - Exception lastException = null; - - try - { - Console.WriteLine($"[Failover] Primary attempt for {operationName} at {primaryEndpoint}"); - cancellationToken.ThrowIfCancellationRequested(); - return await operationAsync(primaryEndpoint).ConfigureAwait(false); - } - catch (RequestFailedException ex) when (IsRetriableFailure(ex)) - { - Console.WriteLine($"[Failover] Primary failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Will attempt failover."); - lastException = ex; - } - catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) - { - Console.WriteLine("[Failover] Primary attempt timeout. Will attempt failover."); - lastException = ex; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - - Console.WriteLine("[Failover] Discovering failover endpoints (async)..."); - List failoverEndpoints = await GetFailoverEndpointsAsync(primaryEndpoint, cancellationToken).ConfigureAwait(false); - Console.WriteLine($"[Failover] Found {failoverEndpoints.Count} failover endpoint(s)."); - - foreach (Uri endpoint in failoverEndpoints) - { - Console.WriteLine($"[Failover] Attempting {operationName} on {endpoint}"); - try - { - cancellationToken.ThrowIfCancellationRequested(); - return await operationAsync(endpoint).ConfigureAwait(false); - } - catch (RequestFailedException ex) when (IsRetriableFailure(ex)) - { - Console.WriteLine($"[Failover] Endpoint {endpoint} failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Trying next."); - lastException = ex; - } - catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) - { - Console.WriteLine($"[Failover] Endpoint {endpoint} timeout. Trying next."); - lastException = ex; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - } - - scope.Failed(lastException); - throw lastException ?? new RequestFailedException("All endpoints failed"); - } - - public T ExecuteWithFailover( - Uri primaryEndpoint, - Func operationSync, - string operationName, - CancellationToken cancellationToken = default) - { - Console.WriteLine($"Executing operation {operationName} on primary endpoint: {primaryEndpoint}"); - using var scope = _clientDiagnostics.CreateScope($"ConfidentialLedgerClient.{operationName}"); - scope.Start(); - - Exception lastException = null; - - try - { - Console.WriteLine($"[Failover] Primary attempt for {operationName} at {primaryEndpoint}"); - cancellationToken.ThrowIfCancellationRequested(); - return operationSync(primaryEndpoint); - } - catch (RequestFailedException ex) when (IsRetriableFailure(ex)) - { - Console.WriteLine($"[Failover] Primary failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Will attempt failover."); - lastException = ex; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - - Console.WriteLine("[Failover] Discovering failover endpoints (sync)..."); - List failoverEndpoints = GetFailoverEndpoints(primaryEndpoint, cancellationToken); - Console.WriteLine($"[Failover] Found {failoverEndpoints.Count} failover endpoint(s)."); - - foreach (Uri endpoint in failoverEndpoints) - { - try - { - Console.WriteLine($"[Failover] Attempting {operationName} on {endpoint}"); - cancellationToken.ThrowIfCancellationRequested(); - return operationSync(endpoint); - } - catch (RequestFailedException ex) when (IsRetriableFailure(ex)) - { - Console.WriteLine($"[Failover] Endpoint {endpoint} failed (Status {ex.Status}, ErrorCode '{ex.ErrorCode}'). Trying next."); - lastException = ex; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - } - - scope.Failed(lastException); - throw lastException ?? new RequestFailedException("All endpoints failed"); - } - - // Overloads with collectionId gating: if collectionId is null/empty, skip failover entirely. - public Task ExecuteWithFailoverAsync( + // Overloads for failover-only execution with collectionId gating. + public Task ExecuteOnFailoversAsync( Uri primaryEndpoint, Func> operationAsync, string operationName, @@ -154,13 +34,12 @@ public Task ExecuteWithFailoverAsync( { if (string.IsNullOrEmpty(collectionIdGate)) { - Console.WriteLine($"[Failover] collectionId not provided; skipping failover for {operationName} and using primary endpoint {primaryEndpoint}"); - return operationAsync(primaryEndpoint); + return operationAsync(primaryEndpoint); // collection gating: no failover } - return ExecuteWithFailoverAsync(primaryEndpoint, operationAsync, operationName, cancellationToken); + return ExecuteOnFailoversAsync(primaryEndpoint, operationAsync, operationName, cancellationToken); } - public T ExecuteWithFailover( + public T ExecuteOnFailovers( Uri primaryEndpoint, Func operationSync, string operationName, @@ -169,10 +48,9 @@ public T ExecuteWithFailover( { if (string.IsNullOrEmpty(collectionIdGate)) { - Console.WriteLine($"[Failover] collectionId not provided; skipping failover for {operationName} and using primary endpoint {primaryEndpoint}"); return operationSync(primaryEndpoint); } - return ExecuteWithFailover(primaryEndpoint, operationSync, operationName, cancellationToken); + return ExecuteOnFailovers(primaryEndpoint, operationSync, operationName, cancellationToken); } private async Task> GetFailoverEndpointsAsync( @@ -185,31 +63,20 @@ private async Task> GetFailoverEndpointsAsync( { string ledgerId = primaryEndpoint.Host.Substring(0, primaryEndpoint.Host.IndexOf('.')); - Uri failoverUrl = new UriBuilder(primaryEndpoint) - { - Host = "localhost", - Path = $"/failover/{ledgerId}" - }.Uri; + Uri failoverUrl = new Uri($"https://identity.confidential-ledger.core.azure.com/failover/{ledgerId}"); using HttpMessage message = CreateFailoverRequest(failoverUrl); Response response = await _pipeline.ProcessMessageAsync(message, new RequestContext()).ConfigureAwait(false); if (response.Status == 200) { - Console.WriteLine("[Failover] Metadata request succeeded."); using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); - if (jsonDoc.RootElement.TryGetProperty("ledgerId", out JsonElement primaryLedgerIdElem) && primaryLedgerIdElem.ValueKind == JsonValueKind.String) - { - Console.WriteLine($"[Failover] Metadata primary ledgerId: {primaryLedgerIdElem.GetString()}"); - } - + jsonDoc.RootElement.TryGetProperty("ledgerId", out _); // fire & forget if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) { - int count = 0; foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) { string failoverLedgerId = null; - string access = null; try { switch (failoverLedger.ValueKind) @@ -218,69 +85,41 @@ private async Task> GetFailoverEndpointsAsync( failoverLedgerId = failoverLedger.GetString(); break; case JsonValueKind.Object: - // Expected shape: { "name": "ledgerName", ... } if (failoverLedger.TryGetProperty("name", out JsonElement nameProp) && nameProp.ValueKind == JsonValueKind.String) { failoverLedgerId = nameProp.GetString(); } else { - // Fall back: look for any string property that could represent the id foreach (JsonProperty prop in failoverLedger.EnumerateObject()) { - if (prop.Value.ValueKind == JsonValueKind.String && - string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) + if (prop.Value.ValueKind == JsonValueKind.String && string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) { failoverLedgerId = prop.Value.GetString(); break; } } } - if (failoverLedger.TryGetProperty("access", out JsonElement accessProp) && accessProp.ValueKind == JsonValueKind.String) - { - access = accessProp.GetString(); - } - break; - default: - Console.WriteLine($"[Failover] Unexpected element kind {failoverLedger.ValueKind} in failoverLedgers array; skipping."); break; } } - catch (Exception exElem) + catch (Exception) { - Console.WriteLine($"[Failover] Suppressed exception parsing failover ledger element: {exElem.Message}"); + // ignore element issues } if (!string.IsNullOrEmpty(failoverLedgerId)) { - Uri endpoint = new UriBuilder(primaryEndpoint) - { - Host = $"{failoverLedgerId}.confidential-ledger.azure.com" - }.Uri; + Uri endpoint = new UriBuilder(primaryEndpoint) { Host = $"{failoverLedgerId}.confidential-ledger.azure.com" }.Uri; failoverEndpoints.Add(endpoint); - Console.WriteLine($"[Failover] Added failover endpoint {endpoint} (access={access ?? "unknown"})"); - count++; - } - else - { - Console.WriteLine("[Failover] Could not extract ledger id from element; skipping."); } } - Console.WriteLine($"[Failover] Parsed {count} failover ledger id(s)."); } - else - { - Console.WriteLine("[Failover] No 'failoverLedgers' property in metadata response."); - } - } - else - { - Console.WriteLine($"[Failover] Metadata request returned status {response.Status}. No endpoints extracted."); } } - catch (Exception ex) + catch (Exception) { - Console.WriteLine($"[Failover] Suppressed exception during metadata retrieval: {ex.Message}"); + // suppress metadata retrieval exception } return failoverEndpoints; @@ -294,34 +133,23 @@ private List GetFailoverEndpoints( try { - Console.WriteLine($"Retrieving failover endpoints for primary endpoint: {primaryEndpoint}"); + // retrieving sync metadata string ledgerId = primaryEndpoint.Host.Substring(0, primaryEndpoint.Host.IndexOf('.')); - Uri failoverUrl = new UriBuilder(primaryEndpoint) - { - Host = "localhost", - Path = $"/failover/{ledgerId}" - }.Uri; + Uri failoverUrl = new Uri($"https://identity.confidential-ledger.core.azure.com/failover/{ledgerId}"); using HttpMessage message = CreateFailoverRequest(failoverUrl); Response response = _pipeline.ProcessMessage(message, new RequestContext()); if (response.Status == 200) { - Console.WriteLine("[Failover] Metadata request succeeded."); using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); - if (jsonDoc.RootElement.TryGetProperty("ledgerId", out JsonElement primaryLedgerIdElem) && primaryLedgerIdElem.ValueKind == JsonValueKind.String) - { - Console.WriteLine($"[Failover] Metadata primary ledgerId: {primaryLedgerIdElem.GetString()}"); - } - + jsonDoc.RootElement.TryGetProperty("ledgerId", out _); if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) { - int count = 0; foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) { string failoverLedgerId = null; - string access = null; try { switch (failoverLedger.ValueKind) @@ -338,59 +166,33 @@ private List GetFailoverEndpoints( { foreach (JsonProperty prop in failoverLedger.EnumerateObject()) { - if (prop.Value.ValueKind == JsonValueKind.String && - string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) + if (prop.Value.ValueKind == JsonValueKind.String && string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) { failoverLedgerId = prop.Value.GetString(); break; } } } - if (failoverLedger.TryGetProperty("access", out JsonElement accessProp) && accessProp.ValueKind == JsonValueKind.String) - { - access = accessProp.GetString(); - } - break; - default: - Console.WriteLine($"[Failover] Unexpected element kind {failoverLedger.ValueKind} in failoverLedgers array; skipping."); break; } } - catch (Exception exElem) + catch (Exception) { - Console.WriteLine($"[Failover] Suppressed exception parsing failover ledger element: {exElem.Message}"); + // ignore element issues } if (!string.IsNullOrEmpty(failoverLedgerId)) { - Uri endpoint = new UriBuilder(primaryEndpoint) - { - Host = $"{failoverLedgerId}.confidential-ledger.azure.com" - }.Uri; + Uri endpoint = new UriBuilder(primaryEndpoint) { Host = $"{failoverLedgerId}.confidential-ledger.azure.com" }.Uri; failoverEndpoints.Add(endpoint); - Console.WriteLine($"[Failover] Added failover endpoint {endpoint} (access={access ?? "unknown"})"); - count++; - } - else - { - Console.WriteLine("[Failover] Could not extract ledger id from element; skipping."); } } - Console.WriteLine($"[Failover] Parsed {count} failover ledger id(s)."); - } - else - { - Console.WriteLine("[Failover] No 'failoverLedgers' property in metadata response."); } } - else - { - Console.WriteLine($"[Failover] Metadata request returned status {response.Status}. No endpoints extracted."); - } } - catch (Exception ex) + catch (Exception) { - Console.WriteLine($"[Failover] Suppressed exception during metadata retrieval: {ex.Message}"); + // suppress metadata retrieval exception } return failoverEndpoints; @@ -425,19 +227,17 @@ private static bool IsRetriableFailure(RequestFailedException ex) } // Execute an operation only against discovered failover endpoints (skips primary). Used for specialized fallback flows. - public async Task ExecuteOnFailoversOnlyAsync( + public async Task ExecuteOnFailoversAsync( Uri primaryEndpoint, Func> operationAsync, string operationName, CancellationToken cancellationToken = default) { - Console.WriteLine($"[Failover] Discovering failover endpoints for {operationName} (failovers-only mode)..."); List endpoints = await GetFailoverEndpointsAsync(primaryEndpoint, cancellationToken).ConfigureAwait(false); - Console.WriteLine($"[Failover] Found {endpoints.Count} failover endpoint(s) for {operationName}."); Exception last = null; foreach (var ep in endpoints) { - Console.WriteLine($"[Failover] (FailoversOnly) Attempting {operationName} on {ep}"); + // attempt endpoint try { cancellationToken.ThrowIfCancellationRequested(); @@ -445,7 +245,7 @@ public async Task ExecuteOnFailoversOnlyAsync( } catch (RequestFailedException ex) when (IsRetriableFailure(ex)) { - Console.WriteLine($"[Failover] (FailoversOnly) Endpoint {ep} failed (Status {ex.Status}, Code {ex.ErrorCode}). Trying next."); + // endpoint failed, continue last = ex; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -453,22 +253,20 @@ public async Task ExecuteOnFailoversOnlyAsync( throw; } } - throw last ?? new RequestFailedException("All failover endpoints failed in failovers-only mode"); + throw last ?? new RequestFailedException("All failover endpoints failed in failovers mode"); } - public T ExecuteOnFailoversOnly( + public T ExecuteOnFailovers( Uri primaryEndpoint, Func operationSync, string operationName, CancellationToken cancellationToken = default) { - Console.WriteLine($"[Failover] Discovering failover endpoints for {operationName} (failovers-only mode)..."); List endpoints = GetFailoverEndpoints(primaryEndpoint, cancellationToken); - Console.WriteLine($"[Failover] Found {endpoints.Count} failover endpoint(s) for {operationName}."); Exception last = null; foreach (var ep in endpoints) { - Console.WriteLine($"[Failover] (FailoversOnly) Attempting {operationName} on {ep}"); + // attempt endpoint try { cancellationToken.ThrowIfCancellationRequested(); @@ -476,7 +274,7 @@ public T ExecuteOnFailoversOnly( } catch (RequestFailedException ex) when (IsRetriableFailure(ex)) { - Console.WriteLine($"[Failover] (FailoversOnly) Endpoint {ep} failed (Status {ex.Status}, Code {ex.ErrorCode}). Trying next."); + // endpoint failed last = ex; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -484,7 +282,7 @@ public T ExecuteOnFailoversOnly( throw; } } - throw last ?? new RequestFailedException("All failover endpoints failed in failovers-only mode"); + throw last ?? new RequestFailedException("All failover endpoints failed in failovers mode"); } } } diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs index 91ac111baa7e..5878cfd30be3 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs @@ -256,50 +256,25 @@ public virtual async Task GetLedgerEntryAsync(string transactionId, st scope.Start(); try { - Console.WriteLine("[GetLedgerEntryAsync] Starting request. txn={0} collection={1}", transactionId, collectionId); - // Primary attempt with fallback to current entry before invoking failover service. try { using HttpMessage primaryMessage = CreateGetLedgerEntryRequest(_ledgerEndpoint, transactionId, collectionId, context); - Response primaryResponse = await _pipeline.ProcessMessageAsync(primaryMessage, context).ConfigureAwait(false); - // Success on primary; return immediately. - return primaryResponse; + return await _pipeline.ProcessMessageAsync(primaryMessage, context).ConfigureAwait(false); } - catch (RequestFailedException ex) when (ShouldFallbackToCurrent(ex) && !string.IsNullOrEmpty(collectionId)) + catch (Exception) { - Console.WriteLine("[GetLedgerEntryAsync] Primary returned not-found/unknown for txn {0}. Attempting GetCurrentLedgerEntry across failover endpoints for collection {1}.", transactionId, collectionId); - try - { - Response currentResponse = await _failoverService.ExecuteOnFailoversOnlyAsync( - _ledgerEndpoint, - async (endpoint) => - { - using HttpMessage msg = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); - return await _pipeline.ProcessMessageAsync(msg, context).ConfigureAwait(false); - }, - nameof(GetCurrentLedgerEntryAsync), - context?.CancellationToken ?? default).ConfigureAwait(false); - Response synthesized = SynthesizeLedgerEntryFromCurrent(currentResponse); - Console.WriteLine("[GetLedgerEntryAsync] Failover current-entry fallback succeeded."); - return synthesized; - } - catch (Exception fallbackFailoverEx) - { - Console.WriteLine("[GetLedgerEntryAsync] Failover current-entry fallback failed: {0}. Proceeding to full failover attempts with original txn.", fallbackFailoverEx.Message); - } + Response failoverCurrent = await _failoverService.ExecuteOnFailoversAsync( + _ledgerEndpoint, + async (endpoint) => + { + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + }, + nameof(GetCurrentLedgerEntryAsync), + collectionId, + context?.CancellationToken ?? default).ConfigureAwait(false); + return FormatLedgerEntry(failoverCurrent); } - - // If we got here, either primary failed with retriable condition (without fallback) or fallback failed; use failover service. - return await _failoverService.ExecuteWithFailoverAsync( - _ledgerEndpoint, - async (endpoint) => - { - using HttpMessage message = CreateGetLedgerEntryRequest(endpoint, transactionId, collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); - }, - nameof(GetLedgerEntryAsync), - collectionId, - context?.CancellationToken ?? default).ConfigureAwait(false); } catch (Exception e) { @@ -334,47 +309,25 @@ public virtual Response GetLedgerEntry(string transactionId, string collectionId scope.Start(); try { - Console.WriteLine("[GetLedgerEntry] Starting request. txn={0} collection={1}", transactionId, collectionId); - // Primary attempt with fallback try { using HttpMessage primaryMessage = CreateGetLedgerEntryRequest(_ledgerEndpoint, transactionId, collectionId, context); - Response primaryResponse = _pipeline.ProcessMessage(primaryMessage, context); - return primaryResponse; + return _pipeline.ProcessMessage(primaryMessage, context); } - catch (RequestFailedException ex) when (ShouldFallbackToCurrent(ex) && !string.IsNullOrEmpty(collectionId)) + catch (Exception) { - Console.WriteLine("[GetLedgerEntry] Primary returned not-found/unknown for txn {0}. Attempting GetCurrentLedgerEntry across failover endpoints for collection {1}.", transactionId, collectionId); - try - { - Response currentResponse = _failoverService.ExecuteOnFailoversOnly( - _ledgerEndpoint, - (endpoint) => - { - using HttpMessage msg = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); - return _pipeline.ProcessMessage(msg, context); - }, - nameof(GetCurrentLedgerEntry), - context?.CancellationToken ?? default); - Response synthesized = SynthesizeLedgerEntryFromCurrent(currentResponse); - Console.WriteLine("[GetLedgerEntry] Failover current-entry fallback succeeded."); - return synthesized; - } - catch (Exception fallbackFailoverEx) - { - Console.WriteLine("[GetLedgerEntry] Failover current-entry fallback failed: {0}. Proceeding to full failover attempts with original txn.", fallbackFailoverEx.Message); - } + Response failoverCurrent = _failoverService.ExecuteOnFailovers( + _ledgerEndpoint, + (endpoint) => + { + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return _pipeline.ProcessMessage(message, context); + }, + nameof(GetCurrentLedgerEntry), + collectionId, + context?.CancellationToken ?? default); + return FormatLedgerEntry(failoverCurrent); } - return _failoverService.ExecuteWithFailover( - _ledgerEndpoint, - (endpoint) => - { - using HttpMessage message = CreateGetLedgerEntryRequest(endpoint, transactionId, collectionId, context); - return _pipeline.ProcessMessage(message, context); - }, - nameof(GetLedgerEntry), - collectionId, - context?.CancellationToken ?? default); } catch (Exception e) { @@ -544,17 +497,24 @@ public virtual async Task GetCurrentLedgerEntryAsync(string collection scope.Start(); try { - Console.WriteLine("[GetCurrentLedgerEntryAsync] Starting request. collection={0}", collectionId); - return await _failoverService.ExecuteWithFailoverAsync( - _ledgerEndpoint, - async (endpoint) => - { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); - }, - nameof(GetCurrentLedgerEntryAsync), - collectionId, - context?.CancellationToken ?? default).ConfigureAwait(false); + try + { + using HttpMessage primaryMessage = CreateGetCurrentLedgerEntryRequest(_ledgerEndpoint, collectionId, context); + return await _pipeline.ProcessMessageAsync(primaryMessage, context).ConfigureAwait(false); + } + catch (Exception) + { + return await _failoverService.ExecuteOnFailoversAsync( + _ledgerEndpoint, + async (endpoint) => + { + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + }, + nameof(GetCurrentLedgerEntryAsync), + collectionId, + context?.CancellationToken ?? default).ConfigureAwait(false); + } } catch (Exception e) { @@ -584,17 +544,24 @@ public virtual Response GetCurrentLedgerEntry(string collectionId = null, Reques scope.Start(); try { - Console.WriteLine("[GetCurrentLedgerEntry] Starting request. collection={0}", collectionId); - return _failoverService.ExecuteWithFailover( - _ledgerEndpoint, - (endpoint) => - { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); - return _pipeline.ProcessMessage(message, context); - }, - nameof(GetCurrentLedgerEntry), - collectionId, - context?.CancellationToken ?? default); + try + { + using HttpMessage primaryMessage = CreateGetCurrentLedgerEntryRequest(_ledgerEndpoint, collectionId, context); + return _pipeline.ProcessMessage(primaryMessage, context); + } + catch (Exception) + { + return _failoverService.ExecuteOnFailovers( + _ledgerEndpoint, + (endpoint) => + { + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(endpoint, collectionId, context); + return _pipeline.ProcessMessage(message, context); + }, + nameof(GetCurrentLedgerEntry), + collectionId, + context?.CancellationToken ?? default); + } } catch (Exception e) { @@ -2804,18 +2771,10 @@ internal HttpMessage CreateGetUserDefinedFunctionsNextPageRequest(string nextLin private static ResponseClassifier _responseClassifier200201; private static ResponseClassifier ResponseClassifier200201 => _responseClassifier200201 ??= new StatusCodeClassifier(stackalloc ushort[] { 200, 201 }); - private static bool ShouldFallbackToCurrent(RequestFailedException ex) - { - if (ex == null) return false; - if (ex.Status == 404) return true; - if (string.Equals(ex.ErrorCode, "UnknownLedgerEntry", StringComparison.OrdinalIgnoreCase)) return true; - return false; - } - - // Synthesize a GetLedgerEntry-shaped response from a GetCurrentLedgerEntry response body. + // Format a GetLedgerEntry-shaped response from a GetCurrentLedgerEntry response body. // Expected current entry body: { "collectionId":"...", "contents":"...", "transactionId":"..." } // Desired ledger entry body: { "entry": { same fields }, "state": "Ready" } - private Response SynthesizeLedgerEntryFromCurrent(Response currentResponse) + private Response FormatLedgerEntry(Response currentResponse) { try { @@ -2828,7 +2787,12 @@ private Response SynthesizeLedgerEntryFromCurrent(Response currentResponse) { var root = doc.RootElement; using var ms = new System.IO.MemoryStream(); - using (var writer = new System.Text.Json.Utf8JsonWriter(ms)) + var jsonWriterOptions = new System.Text.Json.JsonWriterOptions + { + Indented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + using (var writer = new System.Text.Json.Utf8JsonWriter(ms, jsonWriterOptions)) { writer.WriteStartObject(); writer.WritePropertyName("entry"); @@ -2845,9 +2809,8 @@ private Response SynthesizeLedgerEntryFromCurrent(Response currentResponse) return new SyntheticResponse(currentResponse, ms.ToArray()); } } - catch (Exception ex) + catch (Exception) { - Console.WriteLine($"[GetLedgerEntry] Failed to synthesize response from current entry: {ex.Message}"); return currentResponse; // fall back to original } } From 409c510317e29cc2854ce6f3db21531ff9a2875f Mon Sep 17 00:00:00 2001 From: pallabpaul Date: Thu, 14 Aug 2025 15:57:00 -0700 Subject: [PATCH 7/7] address copilot comments --- .../src/ConfidentialLedgerFailoverService.cs | 164 ++++++++---------- 1 file changed, 69 insertions(+), 95 deletions(-) diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs index 7183b520b260..314434874221 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerFailoverService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text.Json; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -16,6 +17,9 @@ internal class ConfidentialLedgerFailoverService private readonly HttpPipeline _pipeline; private readonly ClientDiagnostics _clientDiagnostics; + internal const string IdentityServiceBaseUrl = "https://identity.confidential-ledger.core.azure.com"; + internal const string LedgerDomainSuffix = "confidential-ledger.azure.com"; + private static ResponseClassifier _responseClassifier200; private static ResponseClassifier ResponseClassifier200 => _responseClassifier200 ??= new StatusCodeClassifier(stackalloc ushort[] { 200 }); @@ -57,145 +61,115 @@ private async Task> GetFailoverEndpointsAsync( Uri primaryEndpoint, CancellationToken cancellationToken = default) { - var failoverEndpoints = new List(); - try { string ledgerId = primaryEndpoint.Host.Substring(0, primaryEndpoint.Host.IndexOf('.')); - Uri failoverUrl = new Uri($"https://identity.confidential-ledger.core.azure.com/failover/{ledgerId}"); + Uri failoverUrl = new Uri($"{IdentityServiceBaseUrl}/failover/{ledgerId}"); using HttpMessage message = CreateFailoverRequest(failoverUrl); Response response = await _pipeline.ProcessMessageAsync(message, new RequestContext()).ConfigureAwait(false); - - if (response.Status == 200) - { - using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); - jsonDoc.RootElement.TryGetProperty("ledgerId", out _); // fire & forget - if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) - { - foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) - { - string failoverLedgerId = null; - try - { - switch (failoverLedger.ValueKind) - { - case JsonValueKind.String: - failoverLedgerId = failoverLedger.GetString(); - break; - case JsonValueKind.Object: - if (failoverLedger.TryGetProperty("name", out JsonElement nameProp) && nameProp.ValueKind == JsonValueKind.String) - { - failoverLedgerId = nameProp.GetString(); - } - else - { - foreach (JsonProperty prop in failoverLedger.EnumerateObject()) - { - if (prop.Value.ValueKind == JsonValueKind.String && string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) - { - failoverLedgerId = prop.Value.GetString(); - break; - } - } - } - break; - } - } - catch (Exception) - { - // ignore element issues - } - - if (!string.IsNullOrEmpty(failoverLedgerId)) - { - Uri endpoint = new UriBuilder(primaryEndpoint) { Host = $"{failoverLedgerId}.confidential-ledger.azure.com" }.Uri; - failoverEndpoints.Add(endpoint); - } - } - } - } + return ParseFailoverEndpoints(primaryEndpoint, response); } catch (Exception) { // suppress metadata retrieval exception } - - return failoverEndpoints; + return new List(); } private List GetFailoverEndpoints( Uri primaryEndpoint, CancellationToken cancellationToken = default) { - var failoverEndpoints = new List(); - try { // retrieving sync metadata string ledgerId = primaryEndpoint.Host.Substring(0, primaryEndpoint.Host.IndexOf('.')); - Uri failoverUrl = new Uri($"https://identity.confidential-ledger.core.azure.com/failover/{ledgerId}"); + Uri failoverUrl = new Uri($"{IdentityServiceBaseUrl}/failover/{ledgerId}"); using HttpMessage message = CreateFailoverRequest(failoverUrl); Response response = _pipeline.ProcessMessage(message, new RequestContext()); + return ParseFailoverEndpoints(primaryEndpoint, response); + } + catch (Exception) + { + // suppress metadata retrieval exception + } + return new List(); + } - if (response.Status == 200) + private static List ParseFailoverEndpoints(Uri primaryEndpoint, Response response) + { + var endpoints = new List(); + if (response?.Status != 200) + { + return endpoints; + } + try + { + using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); + jsonDoc.RootElement.TryGetProperty("ledgerId", out _); // optional + if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) { - using JsonDocument jsonDoc = JsonDocument.Parse(response.Content); - jsonDoc.RootElement.TryGetProperty("ledgerId", out _); - if (jsonDoc.RootElement.TryGetProperty("failoverLedgers", out JsonElement failoverArray)) + foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) { - foreach (JsonElement failoverLedger in failoverArray.EnumerateArray()) + string failoverLedgerId = null; + try { - string failoverLedgerId = null; - try + switch (failoverLedger.ValueKind) { - switch (failoverLedger.ValueKind) - { - case JsonValueKind.String: - failoverLedgerId = failoverLedger.GetString(); - break; - case JsonValueKind.Object: - if (failoverLedger.TryGetProperty("name", out JsonElement nameProp) && nameProp.ValueKind == JsonValueKind.String) - { - failoverLedgerId = nameProp.GetString(); - } - else + case JsonValueKind.String: + failoverLedgerId = failoverLedger.GetString(); + break; + case JsonValueKind.Object: + if (failoverLedger.TryGetProperty("name", out JsonElement nameProp) && nameProp.ValueKind == JsonValueKind.String) + { + failoverLedgerId = nameProp.GetString(); + } + else + { + foreach (JsonProperty prop in failoverLedger.EnumerateObject()) { - foreach (JsonProperty prop in failoverLedger.EnumerateObject()) + if (prop.Value.ValueKind == JsonValueKind.String && string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) { - if (prop.Value.ValueKind == JsonValueKind.String && string.Equals(prop.Name, "id", StringComparison.OrdinalIgnoreCase)) - { - failoverLedgerId = prop.Value.GetString(); - break; - } + failoverLedgerId = prop.Value.GetString(); + break; } } - break; - } - } - catch (Exception) - { - // ignore element issues + } + break; } + } + catch (JsonException jex) + { +#if DEBUG + Debug.WriteLine($"[ConfidentialLedgerFailoverService] JSON parse issue for failoverLedger element: {jex.Message}"); +#endif + _ = jex; // suppress unused warning in non-DEBUG builds + } + catch (InvalidOperationException ioex) + { +#if DEBUG + Debug.WriteLine($"[ConfidentialLedgerFailoverService] Invalid operation while parsing failoverLedger element: {ioex.Message}"); +#endif + _ = ioex; // suppress unused warning in non-DEBUG builds + } - if (!string.IsNullOrEmpty(failoverLedgerId)) - { - Uri endpoint = new UriBuilder(primaryEndpoint) { Host = $"{failoverLedgerId}.confidential-ledger.azure.com" }.Uri; - failoverEndpoints.Add(endpoint); - } + if (!string.IsNullOrEmpty(failoverLedgerId)) + { + Uri endpoint = new UriBuilder(primaryEndpoint) { Host = $"{failoverLedgerId}.{LedgerDomainSuffix}" }.Uri; + endpoints.Add(endpoint); } } } } catch (Exception) { - // suppress metadata retrieval exception + // ignore entire parse failure } - - return failoverEndpoints; + return endpoints; } private HttpMessage CreateFailoverRequest(Uri failoverUrl)