using OpenNest.Data;
using OpenNest.IO;
namespace OpenNest.Server.Worker;
///
/// Measures library drawing packages the server lists as unmeasured and stores each
/// , so the library can be filtered by size and holes.
/// A package that cannot be read or measured is stored as failed and not tried again; a
/// server or network failure stores nothing, so the package is tried on a later round.
///
public sealed class DrawingGeometryBackfill
{
public const int BatchSize = 100;
// A pathological drawing must not stall the rest; its worker thread is abandoned.
private static readonly TimeSpan MeasureTimeout = TimeSpan.FromSeconds(30);
private readonly RemoteDrawingGeometry _server;
private readonly ILogger _logger;
public DrawingGeometryBackfill(RemoteDrawingGeometry server, ILogger logger)
{
_server = server;
_logger = logger;
}
/// Measures one batch; returns how many summaries were stored (0 when none were waiting).
public async Task RunOnceAsync(CancellationToken cancellationToken)
{
var hashes = await _server.GetUnmeasuredAsync(DrawingGeometrySummary.CurrentVersion, BatchSize, cancellationToken);
var stored = 0;
foreach (var sha in hashes)
{
cancellationToken.ThrowIfCancellationRequested();
var package = await _server.GetPackageAsync(sha, cancellationToken);
if (package is null)
continue;
var summary = await MeasureAsync(package, cancellationToken);
if (!summary.IsMeasured)
_logger.LogInformation("Library package {Sha} could not be measured: {Error}", sha, summary.Error);
if (await _server.PutAsync(sha, summary, cancellationToken))
stored++;
}
return stored;
}
/// Reads and measures one package; a failure becomes a failed summary.
public static async Task MeasureAsync(byte[] package, CancellationToken cancellationToken)
{
try
{
return await Task.Run(() =>
{
var contents = DrawingPackage.Read(package);
return DrawingGeometrySummary.Measure(contents.Drawing, contents.Units);
}, cancellationToken).WaitAsync(MeasureTimeout, cancellationToken);
}
catch (TimeoutException)
{
return DrawingGeometrySummary.Failed($"Measuring took over {MeasureTimeout.TotalSeconds:0} s.");
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return DrawingGeometrySummary.Failed(ex.Message);
}
}
}
///
/// Runs beside the nest job worker: batch after batch
/// while packages are waiting, then every .
///
public sealed class DrawingGeometryBackfillService : BackgroundService
{
private readonly DrawingGeometryBackfill _backfill;
private readonly WorkerOptions _options;
private readonly ILogger _logger;
public DrawingGeometryBackfillService(DrawingGeometryBackfill backfill, WorkerOptions options,
ILogger logger)
{
_backfill = backfill;
_options = options;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var total = 0;
while (!stoppingToken.IsCancellationRequested)
{
var stored = 0;
try
{
stored = await _backfill.RunOnceAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
return;
}
catch (ServerTooOldException)
{
_logger.LogWarning("The nest server does not measure library drawings; update it to filter the library by size.");
}
catch (Exception ex) when (ex is IOException or HttpRequestException or TaskCanceledException)
{
_logger.LogWarning("Nest server unavailable for library measurement: {Message}", ex.Message);
}
if (stored > 0)
{
total += stored;
_logger.LogInformation("Measured {Count} library drawing package(s) so far.", total);
continue;
}
try { await Task.Delay(_options.GeometryPollInterval, stoppingToken); }
catch (OperationCanceledException) { return; }
}
}
}