From 0b882a11b8534e7693457185a1fffb7ecd727698 Mon Sep 17 00:00:00 2001 From: tusmester Date: Thu, 30 Mar 2023 16:49:38 +0200 Subject: [PATCH] Add retrier to repository requests. (#99) * Initialize server in tests. * Use the new api for getting the server in tests. * Fallback to action call if the token cannot be parsed in GetCurrentUser. * Add Retrier feature to default restcaller. * Remove internal flag from Server property setter in IRepository. * Add logging service in the client feature registration method. * Add more explanation to retry exception handling. --- .../Initializer.cs | 45 ++++++++++++++++--- .../Legacy/ActionTests.cs | 2 +- .../Legacy/BinaryStreamTests.cs | 4 +- .../Legacy/CertificateValidationTests.cs | 4 +- .../Legacy/ContentTests.cs | 4 +- .../Legacy/ErrorHandlingTests.cs | 4 +- .../Legacy/FieldTests.cs | 4 +- .../Legacy/LoadContentTests.cs | 4 +- .../Legacy/RenameTests.cs | 2 +- .../SenseNet.Client.IntegrationTests.csproj | 2 + .../BasicConcepts.cs | 2 +- .../ClientIntegrationTestBase.cs | 45 +++++++++++++------ .../SenseNet.Client.TestsForDocs.csproj | 2 + src/SenseNet.Client/DefaultRestCaller.cs | 34 +++++++++++--- src/SenseNet.Client/RESTExtensions.cs | 44 +++++++++++++++++- src/SenseNet.Client/Repository/IRepository.cs | 2 +- src/SenseNet.Client/Repository/Repository.cs | 12 +---- src/SenseNet.Client/RepositoryExtensions.cs | 6 +-- src/SenseNet.Client/SenseNet.Client.csproj | 4 +- src/SenseNet.Client/ServerContext.cs | 11 +++++ 20 files changed, 176 insertions(+), 61 deletions(-) diff --git a/src/SenseNet.Client.IntegrationTests/Initializer.cs b/src/SenseNet.Client.IntegrationTests/Initializer.cs index 7f4c5bf..dcdbf49 100644 --- a/src/SenseNet.Client.IntegrationTests/Initializer.cs +++ b/src/SenseNet.Client.IntegrationTests/Initializer.cs @@ -1,4 +1,8 @@ -namespace SenseNet.Client.IntegrationTests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using SenseNet.Extensions.DependencyInjection; + +namespace SenseNet.Client.IntegrationTests; [TestClass] public class Initializer @@ -6,18 +10,45 @@ public class Initializer [AssemblyInitialize] public static void InitializeAllTests(TestContext context) { - InitializeServer(); + InitializeServer(context); } - public static void InitializeServer() + public static void InitializeServer(TestContext? context = null) { + var server = new ServerContext + { + Url = "https://localhost:44362" + }; + + if (context != null) + { + // workaround for authenticating using the configured client id and secret + var config = new ConfigurationBuilder() + .SetBasePath(context.DeploymentDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddUserSecrets() + .Build(); + + // create a service collection and register the sensenet client + var services = new ServiceCollection() + .AddSenseNetClient() + .ConfigureSenseNetRepository(repositoryOptions => + { + config.GetSection("sensenet:repository").Bind(repositoryOptions); + }) + .BuildServiceProvider(); + + // get the repository amd extract the server context + var repositories = services.GetRequiredService(); + var repository = repositories.GetRepositoryAsync(CancellationToken.None).GetAwaiter().GetResult(); + + server = repository.Server; + } + ClientContext.Current.RemoveAllServers(); ClientContext.Current.AddServers(new[] { - new ServerContext - { - Url = "https://localhost:44362" - } + server }); // for testing purposes diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/ActionTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/ActionTests.cs index 2bfaf18..fcd5505 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/ActionTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/ActionTests.cs @@ -59,7 +59,7 @@ public async Task Dynamic_action_POST() [ClassInitialize] public static void Cleanup(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); var root = Content.LoadAsync(RootPath).Result; root?.DeleteAsync().ConfigureAwait(false).GetAwaiter().GetResult(); diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/BinaryStreamTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/BinaryStreamTests.cs index c66f588..794ef4c 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/BinaryStreamTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/BinaryStreamTests.cs @@ -6,9 +6,9 @@ namespace SenseNet.Client.IntegrationTests.Legacy public class BinaryStreamTests { [ClassInitialize] - public static void ClassInitializer(TestContext _) + public static void ClassInitializer(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); } [TestMethod] diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/CertificateValidationTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/CertificateValidationTests.cs index 0268b46..04b0cdd 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/CertificateValidationTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/CertificateValidationTests.cs @@ -7,9 +7,9 @@ namespace SenseNet.Client.IntegrationTests.Legacy public class CertificateValidationTests { [ClassInitialize] - public static void ClassInitializer(TestContext _) + public static void ClassInitializer(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); } diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/ContentTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/ContentTests.cs index 82497a3..66782b5 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/ContentTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/ContentTests.cs @@ -9,9 +9,9 @@ namespace SenseNet.Client.IntegrationTests.Legacy public class ContentTests { [ClassInitialize] - public static void ClassInitializer(TestContext _) + public static void ClassInitializer(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); } [TestMethod] diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/ErrorHandlingTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/ErrorHandlingTests.cs index 54baaba..a2dd2fa 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/ErrorHandlingTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/ErrorHandlingTests.cs @@ -6,9 +6,9 @@ namespace SenseNet.Client.IntegrationTests.Legacy public class ErrorHandlingTests { [ClassInitialize] - public static void ClassInitializer(TestContext _) + public static void ClassInitializer(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); } [TestMethod] diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/FieldTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/FieldTests.cs index 7d6ab31..9763eda 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/FieldTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/FieldTests.cs @@ -6,9 +6,9 @@ namespace SenseNet.Client.IntegrationTests.Legacy public class FieldTests { [ClassInitialize] - public static void ClassInitializer(TestContext _) + public static void ClassInitializer(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); } [TestMethod] diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/LoadContentTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/LoadContentTests.cs index fe96d6f..591467b 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/LoadContentTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/LoadContentTests.cs @@ -6,9 +6,9 @@ namespace SenseNet.Client.IntegrationTests.Legacy public class LoadContentTests { [ClassInitialize] - public static void ClassInitializer(TestContext _) + public static void ClassInitializer(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); } [TestMethod] diff --git a/src/SenseNet.Client.IntegrationTests/Legacy/RenameTests.cs b/src/SenseNet.Client.IntegrationTests/Legacy/RenameTests.cs index acd8007..3a0c262 100644 --- a/src/SenseNet.Client.IntegrationTests/Legacy/RenameTests.cs +++ b/src/SenseNet.Client.IntegrationTests/Legacy/RenameTests.cs @@ -30,7 +30,7 @@ public async Task Rename_Folder_01() [ClassInitialize] public static void Cleanup(TestContext context) { - Initializer.InitializeServer(); + Initializer.InitializeServer(context); var root = Content.LoadAsync(ROOTPATH).Result; root?.DeleteAsync().ConfigureAwait(false).GetAwaiter().GetResult(); diff --git a/src/SenseNet.Client.IntegrationTests/SenseNet.Client.IntegrationTests.csproj b/src/SenseNet.Client.IntegrationTests/SenseNet.Client.IntegrationTests.csproj index 0d14bed..51ce8be 100644 --- a/src/SenseNet.Client.IntegrationTests/SenseNet.Client.IntegrationTests.csproj +++ b/src/SenseNet.Client.IntegrationTests/SenseNet.Client.IntegrationTests.csproj @@ -6,6 +6,8 @@ enable false + + 22be19f4-4194-458c-b7c4-26cec471f8e6 diff --git a/src/SenseNet.Client.TestsForDocs/BasicConcepts.cs b/src/SenseNet.Client.TestsForDocs/BasicConcepts.cs index 62a73f9..66d21f2 100644 --- a/src/SenseNet.Client.TestsForDocs/BasicConcepts.cs +++ b/src/SenseNet.Client.TestsForDocs/BasicConcepts.cs @@ -652,7 +652,7 @@ public async Task Docs2_BasicConcepts_AutoFilters() }, cancel)/*/*/.ConfigureAwait(false); var contents = result.ToArray(); - Assert.IsTrue(2 < contents.Length); + Assert.IsTrue(contents.Any()); var types = contents.Select(c => c["Type"].ToString()).Distinct().OrderBy(x => x).ToArray(); Assert.IsTrue(types.Contains("SystemFolder")); } diff --git a/src/SenseNet.Client.TestsForDocs/Infrastructure/ClientIntegrationTestBase.cs b/src/SenseNet.Client.TestsForDocs/Infrastructure/ClientIntegrationTestBase.cs index 8545b41..f3cc7bf 100644 --- a/src/SenseNet.Client.TestsForDocs/Infrastructure/ClientIntegrationTestBase.cs +++ b/src/SenseNet.Client.TestsForDocs/Infrastructure/ClientIntegrationTestBase.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; +using SenseNet.Client.Authentication; using SenseNet.Diagnostics; using SenseNet.Extensions.DependencyInjection; @@ -18,14 +19,9 @@ public class ClientIntegrationTestBase public static readonly string Url = "https://localhost:44362"; [AssemblyInitialize] - public static void InititalizeAllTests(TestContext testContext) + public static void InitializeAllTests(TestContext context) { - ClientContext.Current.AddServer(new ServerContext - { - Url = Url, - Username = "builtin\\admin", - Password = "admin" - }); + InitServer(context); EnsureBasicStructureAsync().ConfigureAwait(false).GetAwaiter().GetResult(); @@ -94,14 +90,35 @@ private static async Task EnsureBasicStructureAsync() [TestInitialize] public void InitializeTest() { + } + + private static void InitServer(TestContext context) + { + // workaround for authenticating using the configured client id and secret + var config = new ConfigurationBuilder() + .SetBasePath(context.DeploymentDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddUserSecrets() + .Build(); + + // create a service collection and register the sensenet client + var services = new ServiceCollection() + .AddSenseNetClient() + .ConfigureSenseNetRepository(repositoryOptions => + { + config.GetSection("sensenet:repository").Bind(repositoryOptions); + }) + .BuildServiceProvider(); + + // get the repository amd extract the server context + var repositories = services.GetRequiredService(); + var repository = repositories.GetRepositoryAsync(CancellationToken.None).GetAwaiter().GetResult(); + + var server = repository.Server; + var ctx = ClientContext.Current; ctx.RemoveServers(ctx.Servers); - ctx.AddServer(new ServerContext - { - Url = Url, - Username = "builtin\\admin", - Password = "admin" - }); + ctx.AddServer(server); } protected Task EnsureContentAsync(string path, string typeName) @@ -144,7 +161,7 @@ protected IRepositoryCollection GetRepositoryCollection(Action() + .AddUserSecrets() .Build(); services diff --git a/src/SenseNet.Client.TestsForDocs/SenseNet.Client.TestsForDocs.csproj b/src/SenseNet.Client.TestsForDocs/SenseNet.Client.TestsForDocs.csproj index c88e23c..0f748dd 100644 --- a/src/SenseNet.Client.TestsForDocs/SenseNet.Client.TestsForDocs.csproj +++ b/src/SenseNet.Client.TestsForDocs/SenseNet.Client.TestsForDocs.csproj @@ -6,6 +6,8 @@ enable false + + a6572a95-e6b3-4d89-aa3c-c7ccf00e82d0 diff --git a/src/SenseNet.Client/DefaultRestCaller.cs b/src/SenseNet.Client/DefaultRestCaller.cs index be3da71..9442de5 100644 --- a/src/SenseNet.Client/DefaultRestCaller.cs +++ b/src/SenseNet.Client/DefaultRestCaller.cs @@ -2,31 +2,51 @@ using System; using System.Net.Http; using System.Threading; +using SenseNet.Tools; namespace SenseNet.Client; public class DefaultRestCaller : IRestCaller { - public async Task GetResponseStringAsync(Uri uri, ServerContext server, CancellationToken cancel, + private readonly IRetrier _retrier; + + public DefaultRestCaller(IRetrier retrier) + { + _retrier = retrier; + } + + public Task GetResponseStringAsync(Uri uri, ServerContext server, CancellationToken cancel, HttpMethod method = null, string jsonBody = null) { - if (method == null) - return await RESTCaller.GetResponseStringAsync(uri, server).ConfigureAwait(false); - return await RESTCaller.GetResponseStringAsync(uri, server, method, jsonBody).ConfigureAwait(false); + return _retrier.RetryAsync( + () => method == null + ? RESTCaller.GetResponseStringAsync(uri, server) + : RESTCaller.GetResponseStringAsync(uri, server, method, jsonBody), + shouldRetryOnError: (ex, _) => ex.ShouldRetry(), + cancel: cancel); } public Task PostContentAsync(string parentPath, object postData, ServerContext server, CancellationToken cancel) { - return RESTCaller.PostContentAsync(parentPath, postData, server); + return _retrier.RetryAsync( + () => RESTCaller.PostContentAsync(parentPath, postData, server), + shouldRetryOnError: (ex, _) => ex.ShouldRetry(), + cancel: cancel); } public Task PatchContentAsync(int contentId, object postData, ServerContext server, CancellationToken cancel) { - return RESTCaller.PatchContentAsync(contentId, postData, server); + return _retrier.RetryAsync( + () => RESTCaller.PatchContentAsync(contentId, postData, server), + shouldRetryOnError: (ex, _) => ex.ShouldRetry(), + cancel: cancel); } public Task PatchContentAsync(string path, object postData, ServerContext server, CancellationToken cancel) { - return RESTCaller.PatchContentAsync(path, postData, server); + return _retrier.RetryAsync( + () => RESTCaller.PatchContentAsync(path, postData, server), + shouldRetryOnError: (ex, _) => ex.ShouldRetry(), + cancel: cancel); } } \ No newline at end of file diff --git a/src/SenseNet.Client/RESTExtensions.cs b/src/SenseNet.Client/RESTExtensions.cs index 92ade49..80701bf 100644 --- a/src/SenseNet.Client/RESTExtensions.cs +++ b/src/SenseNet.Client/RESTExtensions.cs @@ -1,7 +1,11 @@ -using System.Text; +using System.Net.Http; +using System.Net; +using System; +using System.Text; namespace SenseNet.Client { + // ReSharper disable once InconsistentNaming internal static class RESTExtensions { /// @@ -18,5 +22,43 @@ public static void AppendParameter(this StringBuilder sb, string key, object val sb.AppendFormat("{0}={1}", key, value); } + + /// + /// Returns whether the system should retry the operation in case of an exception. + /// This method does not throw an exception. + /// + /// + /// Checks if there was an exception during execution. In case there wasn't, the result + /// is FALSE and the retry cycle breaks. + /// In case there was an error and it is one of the well-known exceptions, this method + /// returns TRUE and the system will retry the operation. + /// If the exception is unknown, the result is FALSE and Retrier will throw the exception by default. + /// + /// An exception if there was an error during execution. + public static bool ShouldRetry(this Exception exception) + { + return exception switch + { + null => false, + +#if NET6_0_OR_GREATER + // HttpStatusCode.TooManyRequests is not available in netstandard 2.0 + ClientException { StatusCode: HttpStatusCode.TooManyRequests } => true, +#endif + ClientException { StatusCode: HttpStatusCode.RequestTimeout or HttpStatusCode.GatewayTimeout } => true, + ClientException { InnerException: HttpRequestException rex } when + rex.Message.Contains("The SSL connection could not be established") || + rex.Message.Contains("An error occurred while sending the request") + => true, + ClientException { StatusCode: HttpStatusCode.InternalServerError } cex + when cex.Message.Contains("Error in datastore when loading nodes.") || + cex.Message.Contains("Data layer timeout occurred.") => true, + + // Add more well-known exceptions that can be retried here. + // All unknown exceptions should be thrown immediately + // (FALSE means do not retry). + _ => false + }; + } } } diff --git a/src/SenseNet.Client/Repository/IRepository.cs b/src/SenseNet.Client/Repository/IRepository.cs index caaee40..4736b8f 100644 --- a/src/SenseNet.Client/Repository/IRepository.cs +++ b/src/SenseNet.Client/Repository/IRepository.cs @@ -16,7 +16,7 @@ public interface IRepository /// A context object that represents a connection to a sensenet service. /// /// A repository instance always belongs to a single sensenet service. - public ServerContext Server { get; internal set; } + public ServerContext Server { get; set; } /// /// Gets the registered repository-independent content types. diff --git a/src/SenseNet.Client/Repository/Repository.cs b/src/SenseNet.Client/Repository/Repository.cs index 540fbb1..24d9378 100644 --- a/src/SenseNet.Client/Repository/Repository.cs +++ b/src/SenseNet.Client/Repository/Repository.cs @@ -9,8 +9,6 @@ using System.Net.Http; using Microsoft.Extensions.Options; using System.Text.RegularExpressions; -using System.IO; -using System.Net.Mime; // ReSharper disable once CheckNamespace namespace SenseNet.Client; @@ -21,14 +19,8 @@ internal class Repository : IRepository private readonly IServiceProvider _services; private readonly ILogger _logger; - internal ServerContext Server { get; set; } - - ServerContext IRepository.Server - { - get => this.Server; - set => this.Server = value; - } - + public ServerContext Server { get; set; } + public RegisteredContentTypes GlobalContentTypes { get; } public Repository(IRestCaller restCaller, IServiceProvider services, IOptions globalContentTypes, ILogger logger) diff --git a/src/SenseNet.Client/RepositoryExtensions.cs b/src/SenseNet.Client/RepositoryExtensions.cs index f8c2b1b..7f16d59 100644 --- a/src/SenseNet.Client/RepositoryExtensions.cs +++ b/src/SenseNet.Client/RepositoryExtensions.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Mime; using Microsoft.Extensions.DependencyInjection; using SenseNet.Client; @@ -49,6 +45,8 @@ public static IServiceCollection AddSenseNetClient(this IServiceCollection servi .AddSingleton() .AddTransient() .AddTransient() + .AddSenseNetRetrier() + .AddLogging() .Configure(_ => { }); } diff --git a/src/SenseNet.Client/SenseNet.Client.csproj b/src/SenseNet.Client/SenseNet.Client.csproj index a673a75..ef2e8ef 100644 --- a/src/SenseNet.Client/SenseNet.Client.csproj +++ b/src/SenseNet.Client/SenseNet.Client.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;net6.0 SenseNet.Client SenseNet.Client 3.0.0 @@ -33,7 +33,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SenseNet.Client/ServerContext.cs b/src/SenseNet.Client/ServerContext.cs index 770f287..9d46a68 100644 --- a/src/SenseNet.Client/ServerContext.cs +++ b/src/SenseNet.Client/ServerContext.cs @@ -90,6 +90,17 @@ public Task GetCurrentUserAsync(string[] select = null, string[] expand Select = select, Expand = expand }; + else + { + // subject not available, but this still can be a valid token + request = new ODataRequest(this) + { + Path = "/Root", + ActionName = "GetCurrentUser", + Select = select, + Expand = expand + }; + } } catch (Exception ex) {