Skip to content

Commit

Permalink
Add retrier to repository requests. (#99)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
tusmester authored Mar 30, 2023
1 parent 920cb90 commit 0b882a1
Show file tree
Hide file tree
Showing 20 changed files with 176 additions and 61 deletions.
45 changes: 38 additions & 7 deletions src/SenseNet.Client.IntegrationTests/Initializer.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,54 @@
namespace SenseNet.Client.IntegrationTests;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SenseNet.Extensions.DependencyInjection;

namespace SenseNet.Client.IntegrationTests;

[TestClass]
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<Initializer>()
.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<IRepositoryCollection>();
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
Expand Down
2 changes: 1 addition & 1 deletion src/SenseNet.Client.IntegrationTests/Legacy/ActionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}


Expand Down
4 changes: 2 additions & 2 deletions src/SenseNet.Client.IntegrationTests/Legacy/ContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions src/SenseNet.Client.IntegrationTests/Legacy/FieldTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion src/SenseNet.Client.IntegrationTests/Legacy/RenameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>

<UserSecretsId>22be19f4-4194-458c-b7c4-26cec471f8e6</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/SenseNet.Client.TestsForDocs/BasicConcepts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ public async Task Docs2_BasicConcepts_AutoFilters()
}, cancel)/*/<doc>*/.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"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();

Expand Down Expand Up @@ -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<ClientIntegrationTestBase>()
.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<IRepositoryCollection>();
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<Content> EnsureContentAsync(string path, string typeName)
Expand Down Expand Up @@ -144,7 +161,7 @@ protected IRepositoryCollection GetRepositoryCollection(Action<IServiceCollectio

var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true)
//.AddUserSecrets<ContentTests>()
.AddUserSecrets<ClientIntegrationTestBase>()
.Build();

services
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>

<UserSecretsId>a6572a95-e6b3-4d89-aa3c-c7ccf00e82d0</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand Down
34 changes: 27 additions & 7 deletions src/SenseNet.Client/DefaultRestCaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> GetResponseStringAsync(Uri uri, ServerContext server, CancellationToken cancel,
private readonly IRetrier _retrier;

public DefaultRestCaller(IRetrier retrier)
{
_retrier = retrier;
}

public Task<string> 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<dynamic> 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<dynamic> 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<dynamic> 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);
}
}
44 changes: 43 additions & 1 deletion src/SenseNet.Client/RESTExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
Expand All @@ -18,5 +22,43 @@ public static void AppendParameter(this StringBuilder sb, string key, object val

sb.AppendFormat("{0}={1}", key, value);
}

/// <summary>
/// Returns whether the system should retry the operation in case of an exception.
/// This method does not throw an exception.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="exception">An exception if there was an error during execution.</param>
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
};
}
}
}
2 changes: 1 addition & 1 deletion src/SenseNet.Client/Repository/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface IRepository
/// A context object that represents a connection to a sensenet service.
/// </summary>
/// <remarks>A repository instance always belongs to a single sensenet service.</remarks>
public ServerContext Server { get; internal set; }
public ServerContext Server { get; set; }

/// <summary>
/// Gets the registered repository-independent content types.
Expand Down
Loading

0 comments on commit 0b882a1

Please sign in to comment.