Optimize Fuchs_DataService: parallel file sync, shared HttpClient, cancellation
Reviewed the data service against mfr_interface_description.md. The OData entity sync already follows @odata.nextLink and now inherits the MFR client's transient retry + timeout, so it is spec-aligned. Reliability/performance improvements: - MFRClient.GetFile no longer news up an HttpClient per call (socket-exhaustion risk); added GetFileAsync backed by one shared static HttpClient with per-request auth, and GetFile delegates to it. - GetInvoiceFiles_async now downloads + stores invoice PDFs in parallel (bounded concurrency 4) via Parallel.ForEachAsync instead of sequentially. - Threaded CancellationToken from the MfrSync job through UpdateIfNecessary_async/ UpdateRequested_async/GetInvoiceFiles_async and the entity-sync loops for graceful shutdown (cooperative checks between iterations). Entity-table sync is left sequential on purpose (referential ordering by updateneed). IFdsMfr sync methods gained optional CancellationToken params (default) — the web app only uses the read methods, so this stays source-compatible with Fuchs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -84,26 +84,46 @@ public class MFRClient : IDisposable
|
||||
return first;
|
||||
}
|
||||
|
||||
// One shared HttpClient per process avoids socket exhaustion from per-call instances.
|
||||
// Auth + user-agent are set per request so the shared instance stays stateless.
|
||||
private static readonly HttpClient _sharedHttpClient = new() { Timeout = TimeSpan.FromMinutes(5) };
|
||||
|
||||
public byte[]? GetFile(string address, bool throwErrorIfNotOk = true)
|
||||
=> GetFileAsync(address, throwErrorIfNotOk).GetAwaiter().GetResult();
|
||||
|
||||
public async Task<byte[]?> GetFileAsync(string address, bool throwErrorIfNotOk = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
byte[]? data = null;
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(DownloadUserAgent);
|
||||
var credentials = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{_clientCredentials.Username}:{_clientCredentials.Password}"));
|
||||
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
|
||||
try
|
||||
{
|
||||
data = httpClient.GetByteArrayAsync(new Uri(address)).GetAwaiter().GetResult();
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, new Uri(address));
|
||||
req.Headers.UserAgent.ParseAdd(DownloadUserAgent);
|
||||
var credentials = Convert.ToBase64String(
|
||||
System.Text.Encoding.UTF8.GetBytes($"{_clientCredentials.Username}:{_clientCredentials.Password}"));
|
||||
req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
|
||||
|
||||
using var resp = await _sharedHttpClient.SendAsync(req, cancellationToken);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("GetFile non-success status {Status} — address={Address}", resp.StatusCode, address);
|
||||
if (throwErrorIfNotOk && !HideCustomExceptions)
|
||||
throw new MFRClientException(resp.StatusCode, $"GetFile failed ({(int)resp.StatusCode})", address);
|
||||
return null;
|
||||
}
|
||||
return await resp.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "GetFile failed with HttpRequestException — address={Address}", address);
|
||||
return null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Swallow
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public async Task<string> GetEntities(bool throwErrorIfNotOk = true)
|
||||
|
||||
Reference in New Issue
Block a user