0954876295
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
77 lines
3.0 KiB
C#
77 lines
3.0 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
public sealed class PdfRenderOptions
|
|
{
|
|
// One browser at a time until the deployment's memory/CPU budget is measured (R04).
|
|
[Range(1, 8)] public int MaxConcurrentRenders { get; set; } = 1;
|
|
[Range(1, 60)] public int RenderTimeoutSeconds { get; set; } = 60;
|
|
}
|
|
|
|
public sealed class PdfCapacityException : Exception
|
|
{
|
|
public PdfCapacityException() : base("PDF generation is busy. Please try again shortly.") { }
|
|
}
|
|
|
|
public sealed class PdfRenderTimeoutException : TimeoutException
|
|
{
|
|
public PdfRenderTimeoutException() : base("PDF generation took too long. Please try again.") { }
|
|
}
|
|
|
|
// Registered once for the API process, shared even by separately constructed exporters.
|
|
// ponytail: zero waiting, one active render per document owner; measure before adding a scheduler.
|
|
public sealed class PdfRenderAdmission
|
|
{
|
|
private readonly HashSet<string> _owners = new(StringComparer.Ordinal);
|
|
private readonly int _limit;
|
|
private readonly ILogger<PdfRenderAdmission> _logger;
|
|
private bool _disabled;
|
|
public TimeSpan RenderTimeout { get; }
|
|
|
|
public PdfRenderAdmission(IOptions<PdfRenderOptions> options, ILogger<PdfRenderAdmission> logger)
|
|
{
|
|
var value = options.Value;
|
|
Validator.ValidateObject(value, new ValidationContext(value), validateAllProperties: true);
|
|
_limit = value.MaxConcurrentRenders;
|
|
RenderTimeout = TimeSpan.FromSeconds(value.RenderTimeoutSeconds);
|
|
_logger = logger;
|
|
}
|
|
|
|
public IDisposable Enter(string ownerUserId, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(ownerUserId);
|
|
lock (_owners)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (_disabled || _owners.Count >= _limit || _owners.Contains(ownerUserId))
|
|
{
|
|
_logger.LogDebug("PDF admission rejected; active={Active}, limit={Limit}, disabled={Disabled}", _owners.Count, _limit, _disabled);
|
|
throw new PdfCapacityException();
|
|
}
|
|
// Log before reserving: even a failing logging provider must not strand a slot.
|
|
_logger.LogDebug("PDF admission granted; active={Active}, limit={Limit}", _owners.Count + 1, _limit);
|
|
_owners.Add(ownerUserId);
|
|
return new Lease(this, ownerUserId);
|
|
}
|
|
}
|
|
|
|
// If the OS cannot confirm termination, never replace possibly still-running work.
|
|
// This is a visible fail-closed fault, requiring operator inspection/restart.
|
|
public void Disable()
|
|
{
|
|
lock (_owners) _disabled = true;
|
|
}
|
|
|
|
private sealed class Lease(PdfRenderAdmission admission, string owner) : IDisposable
|
|
{
|
|
private int _disposed;
|
|
public void Dispose()
|
|
{
|
|
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
|
|
lock (admission._owners) admission._owners.Remove(owner);
|
|
}
|
|
}
|
|
}
|