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 _owners = new(StringComparer.Ordinal); private readonly int _limit; private readonly ILogger _logger; private bool _disabled; public TimeSpan RenderTimeout { get; } public PdfRenderAdmission(IOptions options, ILogger 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); } } }