feat: Refactor RaceUploadCommunicationClient and RaceUploadTabView to improve HttpClient management and resource disposal
This commit is contained in:
parent
b29cc95a1e
commit
bdf503c627
3 changed files with 145 additions and 81 deletions
|
|
@ -2,6 +2,8 @@ using System.Net;
|
||||||
using Catalog.Communication.Abstractions;
|
using Catalog.Communication.Abstractions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Catalog.Communication.DependencyInjection;
|
namespace Catalog.Communication.DependencyInjection;
|
||||||
|
|
@ -25,22 +27,30 @@ public static class CatalogCommunicationServiceCollectionExtensions
|
||||||
|
|
||||||
services.TryAddSingleton<CookieContainer>();
|
services.TryAddSingleton<CookieContainer>();
|
||||||
|
|
||||||
services
|
// Create the HttpClient only when the communication client is requested.
|
||||||
.AddHttpClient<IRaceUploadCommunicationClient, RaceUploadCommunicationClient>((sp, client) =>
|
// This avoids constructing the DefaultHttpClientFactory (and its background cleanup timer)
|
||||||
|
// if the race-upload feature is never used.
|
||||||
|
services.AddTransient<IRaceUploadCommunicationClient>(sp =>
|
||||||
|
{
|
||||||
|
var options = sp.GetRequiredService<IOptions<CatalogCommunicationOptions>>().Value;
|
||||||
|
var logger = sp.GetService<ILogger<RaceUploadCommunicationClient>>() ?? NullLogger<RaceUploadCommunicationClient>.Instance;
|
||||||
|
var cookieContainer = sp.GetRequiredService<CookieContainer>();
|
||||||
|
|
||||||
|
var handler = new HttpClientHandler
|
||||||
{
|
{
|
||||||
var options = sp.GetRequiredService<IOptions<CatalogCommunicationOptions>>().Value;
|
UseCookies = true,
|
||||||
client.BaseAddress = options.BaseUri;
|
CookieContainer = cookieContainer,
|
||||||
})
|
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
|
||||||
.ConfigurePrimaryHttpMessageHandler(sp =>
|
};
|
||||||
|
|
||||||
|
var httpClient = new HttpClient(handler, disposeHandler: true)
|
||||||
{
|
{
|
||||||
var cookieContainer = sp.GetRequiredService<CookieContainer>();
|
BaseAddress = options.BaseUri,
|
||||||
return new HttpClientHandler
|
Timeout = options.RequestTimeout,
|
||||||
{
|
};
|
||||||
UseCookies = true,
|
|
||||||
CookieContainer = cookieContainer,
|
return new RaceUploadCommunicationClient(httpClient, sp.GetRequiredService<IOptions<CatalogCommunicationOptions>>(), logger);
|
||||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Catalog.Communication;
|
namespace Catalog.Communication;
|
||||||
|
|
||||||
public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient
|
public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient, IDisposable
|
||||||
{
|
{
|
||||||
private const string AdminMenuPath = "admin/menu/Menu4.abl";
|
private const string AdminMenuPath = "admin/menu/Menu4.abl";
|
||||||
private const string PublicLogonPath = "Logon.abl";
|
private const string PublicLogonPath = "Logon.abl";
|
||||||
|
|
@ -27,6 +27,7 @@ public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClie
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<RaceUploadCommunicationClient> _logger;
|
private readonly ILogger<RaceUploadCommunicationClient> _logger;
|
||||||
private readonly IOptions<CatalogCommunicationOptions> _options;
|
private readonly IOptions<CatalogCommunicationOptions> _options;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
public RaceUploadCommunicationClient(
|
public RaceUploadCommunicationClient(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
|
|
@ -38,6 +39,18 @@ public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClie
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_httpClient.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<RawEndpointResponse> LoginAdminAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
public Task<RawEndpointResponse> LoginAdminAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,21 @@ namespace ImageCatalog_2.AvaloniaViews;
|
||||||
|
|
||||||
public partial class RaceUploadTabView : Avalonia.Controls.UserControl
|
public partial class RaceUploadTabView : Avalonia.Controls.UserControl
|
||||||
{
|
{
|
||||||
private readonly IRaceUploadCommunicationClient _apiClient;
|
|
||||||
private readonly ILogger<RaceUploadTabView> _logger;
|
private readonly ILogger<RaceUploadTabView> _logger;
|
||||||
|
|
||||||
public RaceUploadTabView()
|
public RaceUploadTabView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_apiClient = Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
|
|
||||||
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
|
|
||||||
_logger = Program.ServiceProvider.GetService(typeof(ILogger<RaceUploadTabView>)) as ILogger<RaceUploadTabView>
|
_logger = Program.ServiceProvider.GetService(typeof(ILogger<RaceUploadTabView>)) as ILogger<RaceUploadTabView>
|
||||||
?? NullLogger<RaceUploadTabView>.Instance;
|
?? NullLogger<RaceUploadTabView>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IRaceUploadCommunicationClient CreateClient()
|
||||||
|
{
|
||||||
|
return Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
|
||||||
|
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
|
||||||
|
}
|
||||||
|
|
||||||
private async void CreateRace_Click(object? sender, RoutedEventArgs e)
|
private async void CreateRace_Click(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
|
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
|
||||||
|
|
@ -76,32 +79,43 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
|
||||||
|
|
||||||
var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
|
var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
|
||||||
|
|
||||||
var saveResponse = await _apiClient.SaveRaceAsync(
|
RawEndpointResponse saveResponse;
|
||||||
new RaceSaveRequest
|
RawEndpointResponse createPointsResponse;
|
||||||
{
|
long raceId;
|
||||||
IdGara = 0,
|
|
||||||
Description = sanitizedDescription,
|
|
||||||
StartDate = startDate,
|
|
||||||
EndDate = endDate,
|
|
||||||
TipoGaraId = tipoGaraId,
|
|
||||||
EventoInLinea = model.ApiEventoInLineaIndex,
|
|
||||||
TipoIndicizzazione = model.ApiTipoIndexValue,
|
|
||||||
FreeEvent = model.ApiFreeEventIndex,
|
|
||||||
PathBase = model.ApiPathBase?.Trim(),
|
|
||||||
Localita = model.ApiLocalita?.Trim(),
|
|
||||||
},
|
|
||||||
CancellationToken.None);
|
|
||||||
|
|
||||||
var raceId = ExtractRaceId(saveResponse.Body);
|
var client = CreateClient();
|
||||||
if (raceId <= 0)
|
try
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
|
saveResponse = await client.SaveRaceAsync(
|
||||||
|
new RaceSaveRequest
|
||||||
|
{
|
||||||
|
IdGara = 0,
|
||||||
|
Description = sanitizedDescription,
|
||||||
|
StartDate = startDate,
|
||||||
|
EndDate = endDate,
|
||||||
|
TipoGaraId = tipoGaraId,
|
||||||
|
EventoInLinea = model.ApiEventoInLineaIndex,
|
||||||
|
TipoIndicizzazione = model.ApiTipoIndexValue,
|
||||||
|
FreeEvent = model.ApiFreeEventIndex,
|
||||||
|
PathBase = model.ApiPathBase?.Trim(),
|
||||||
|
Localita = model.ApiLocalita?.Trim(),
|
||||||
|
},
|
||||||
|
CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
raceId = ExtractRaceId(saveResponse.Body);
|
||||||
|
if (raceId <= 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
|
||||||
|
}
|
||||||
|
|
||||||
|
model.ApiRaceId = raceId.ToString();
|
||||||
|
|
||||||
|
createPointsResponse = await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(client as IDisposable)?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
model.ApiRaceId = raceId.ToString();
|
|
||||||
|
|
||||||
var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
|
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
|
||||||
sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
|
sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
|
||||||
|
|
@ -197,41 +211,50 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"File da inviare: {files.Count}");
|
sb.AppendLine($"File da inviare: {files.Count}");
|
||||||
|
|
||||||
foreach (var file in files)
|
List<long> pointIds;
|
||||||
|
|
||||||
|
var client = CreateClient();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var relativePath = Path.GetRelativePath(model.DestinationPath, file);
|
foreach (var file in files)
|
||||||
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
|
|
||||||
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
|
|
||||||
|
|
||||||
await using var stream = File.OpenRead(file);
|
|
||||||
await _apiClient.UploadFileToReceiverAsync(
|
|
||||||
new ReceiveFileUploadRequest
|
|
||||||
{
|
|
||||||
FileName = Path.GetFileName(file),
|
|
||||||
FileStream = stream,
|
|
||||||
DestinationPath = remotePath,
|
|
||||||
OverwriteRemoteFile = true,
|
|
||||||
},
|
|
||||||
CancellationToken.None).ConfigureAwait(true);
|
|
||||||
|
|
||||||
uploaded++;
|
|
||||||
if (uploaded % 20 == 0 || uploaded == files.Count)
|
|
||||||
{
|
{
|
||||||
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
|
var relativePath = Path.GetRelativePath(model.DestinationPath, file);
|
||||||
|
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
|
||||||
|
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
|
||||||
|
|
||||||
|
await using var stream = File.OpenRead(file);
|
||||||
|
await client.UploadFileToReceiverAsync(
|
||||||
|
new ReceiveFileUploadRequest
|
||||||
|
{
|
||||||
|
FileName = Path.GetFileName(file),
|
||||||
|
FileStream = stream,
|
||||||
|
DestinationPath = remotePath,
|
||||||
|
OverwriteRemoteFile = true,
|
||||||
|
},
|
||||||
|
CancellationToken.None).ConfigureAwait(true);
|
||||||
|
|
||||||
|
uploaded++;
|
||||||
|
if (uploaded % 20 == 0 || uploaded == files.Count)
|
||||||
|
{
|
||||||
|
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
|
||||||
|
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
|
||||||
|
|
||||||
|
await client.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
|
||||||
|
pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
|
||||||
|
|
||||||
|
foreach (var pointId in pointIds)
|
||||||
|
{
|
||||||
|
await client.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
|
|
||||||
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
|
|
||||||
|
|
||||||
await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
|
|
||||||
var pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
|
|
||||||
|
|
||||||
foreach (var pointId in pointIds)
|
|
||||||
{
|
{
|
||||||
await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
|
(client as IDisposable)?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
|
sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
|
||||||
outputBox.Text = sb.ToString();
|
outputBox.Text = sb.ToString();
|
||||||
statusBlock.Text = "Upload e indicizzazione completati.";
|
statusBlock.Text = "Upload e indicizzazione completati.";
|
||||||
|
|
@ -251,31 +274,49 @@ public partial class RaceUploadTabView : Avalonia.Controls.UserControl
|
||||||
|
|
||||||
private async Task<RawEndpointResponse> LoginAsync(string login, string password)
|
private async Task<RawEndpointResponse> LoginAsync(string login, string password)
|
||||||
{
|
{
|
||||||
return await _apiClient.LoginAdminAsync(
|
var client = CreateClient();
|
||||||
new AdminLoginRequest
|
try
|
||||||
{
|
{
|
||||||
Login = login,
|
return await client.LoginAdminAsync(
|
||||||
Password = password,
|
new AdminLoginRequest
|
||||||
Command = "check",
|
{
|
||||||
},
|
Login = login,
|
||||||
CancellationToken.None).ConfigureAwait(false);
|
Password = password,
|
||||||
|
Command = "check",
|
||||||
|
},
|
||||||
|
CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(client as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<long>> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
|
private async Task<List<long>> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const int maxAttempts = 10;
|
const int maxAttempts = 10;
|
||||||
|
|
||||||
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
var client = CreateClient();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var response = await _apiClient.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
|
for (var attempt = 1; attempt <= maxAttempts; attempt++)
|
||||||
var ids = ExtractPointIds(response.Body);
|
|
||||||
|
|
||||||
if (ids.Count > 0)
|
|
||||||
{
|
{
|
||||||
return ids;
|
var response = await client.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
|
||||||
|
var ids = ExtractPointIds(response.Body);
|
||||||
|
|
||||||
|
if (ids.Count > 0)
|
||||||
|
{
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
|
return new List<long>();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(client as IDisposable)?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue