Make remote localization startup non-blocking
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Localization;
|
||||
|
||||
namespace StellaOps.Localization.Tests;
|
||||
|
||||
public sealed class RemoteBundleProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadAsync_ReturnsEmptyBundle_WhenRemoteFetchTimesOut()
|
||||
{
|
||||
using var client = new HttpClient(new BlockingMessageHandler());
|
||||
var provider = new RemoteBundleProvider(
|
||||
Options.Create(new TranslationOptions
|
||||
{
|
||||
EnableRemoteBundles = true,
|
||||
RemoteBundleUrl = "http://platform.stella-ops.local",
|
||||
RemoteBundleRequestTimeout = TimeSpan.FromMilliseconds(100)
|
||||
}),
|
||||
NullLogger<RemoteBundleProvider>.Instance,
|
||||
new FixedHttpClientFactory(client));
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var bundle = await provider.LoadAsync("en-US", TestContext.Current.CancellationToken);
|
||||
stopwatch.Stop();
|
||||
|
||||
Assert.Empty(bundle);
|
||||
Assert.InRange(stopwatch.Elapsed, TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private sealed class FixedHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public FixedHttpClientFactory(HttpClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public HttpClient CreateClient(string name) => _client;
|
||||
}
|
||||
|
||||
private sealed class BlockingMessageHandler : HttpMessageHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = JsonContent.Create(new { locale = "en-US", strings = new Dictionary<string, string>() })
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Localization/StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Localization;
|
||||
|
||||
namespace StellaOps.Localization.Tests;
|
||||
|
||||
public sealed class TranslationRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoadAsync_LoadsLocalesConcurrentlyWithinSingleProvider()
|
||||
{
|
||||
var provider = new BlockingProvider("en-US", "de-DE", "bg-BG");
|
||||
var registry = new TranslationRegistry(
|
||||
Options.Create(new TranslationOptions
|
||||
{
|
||||
DefaultLocale = "en-US",
|
||||
SupportedLocales = ["en-US", "de-DE", "bg-BG"]
|
||||
}),
|
||||
NullLogger<TranslationRegistry>.Instance);
|
||||
|
||||
var loadTask = registry.LoadAsync([provider], TestContext.Current.CancellationToken);
|
||||
|
||||
await provider.AllLocalesStarted.Task.WaitAsync(TestContext.Current.CancellationToken);
|
||||
Assert.True(provider.MaxConcurrentLoads >= 2);
|
||||
|
||||
provider.Release();
|
||||
await loadTask;
|
||||
|
||||
Assert.Equal("bundle:de-DE", registry.GetBundle("de-DE")["translation.loaded"]);
|
||||
Assert.Equal("bundle:bg-BG", registry.GetBundle("bg-BG")["translation.loaded"]);
|
||||
}
|
||||
|
||||
private sealed class BlockingProvider : ITranslationBundleProvider
|
||||
{
|
||||
private readonly IReadOnlyList<string> _locales;
|
||||
private readonly TaskCompletionSource _release = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private int _activeLoads;
|
||||
private int _startedLoads;
|
||||
private int _maxConcurrentLoads;
|
||||
|
||||
public BlockingProvider(params string[] locales)
|
||||
{
|
||||
_locales = locales;
|
||||
}
|
||||
|
||||
public int Priority => 10;
|
||||
|
||||
public int MaxConcurrentLoads => _maxConcurrentLoads;
|
||||
|
||||
public TaskCompletionSource AllLocalesStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public Task<IReadOnlyList<string>> GetAvailableLocalesAsync(CancellationToken ct)
|
||||
=> Task.FromResult(_locales);
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, string>> LoadAsync(string locale, CancellationToken ct)
|
||||
{
|
||||
var concurrentLoads = Interlocked.Increment(ref _activeLoads);
|
||||
UpdateMaxConcurrentLoads(concurrentLoads);
|
||||
|
||||
if (Interlocked.Increment(ref _startedLoads) == _locales.Count)
|
||||
{
|
||||
AllLocalesStarted.TrySetResult();
|
||||
}
|
||||
|
||||
await _release.Task.WaitAsync(ct);
|
||||
Interlocked.Decrement(ref _activeLoads);
|
||||
|
||||
return new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["translation.loaded"] = $"bundle:{locale}"
|
||||
};
|
||||
}
|
||||
|
||||
public void Release() => _release.TrySetResult();
|
||||
|
||||
private void UpdateMaxConcurrentLoads(int concurrentLoads)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var snapshot = _maxConcurrentLoads;
|
||||
if (concurrentLoads <= snapshot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _maxConcurrentLoads, concurrentLoads, snapshot) == snapshot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user