using Microsoft.Extensions.Options; using PrintService.Models; using System.Collections.Concurrent; using System.Text.Json; namespace PrintService.Services; /// /// Manages the print job queue with persistence /// public class PrintQueueService { private readonly ConcurrentQueue _queue = new(); private readonly AppSettings _settings; private readonly ILogger _logger; private readonly SemaphoreSlim _semaphore = new(1, 1); public PrintQueueService(IOptions settings, ILogger logger) { _settings = settings.Value; _logger = logger; LoadQueueState(); } /// /// Enqueue a new print job /// public void Enqueue(PrintJob job) { _queue.Enqueue(job); _logger.LogInformation("Job {JobId} enqueued for document type: {DocType}", job.Id, job.DocumentType); SaveQueueState(); } /// /// Try to dequeue the next pending job /// public bool TryDequeue(out PrintJob? job) { bool result = _queue.TryDequeue(out job); if (result && job != null) { _logger.LogInformation("Job {JobId} dequeued", job.Id); SaveQueueState(); } return result; } /// /// Peek at the next job without removing it /// public bool TryPeek(out PrintJob? job) { return _queue.TryPeek(out job); } /// /// Get count of jobs in queue /// public int Count => _queue.Count; /// /// Save queue state to disk /// private void SaveQueueState() { try { _semaphore.Wait(); var jobs = _queue.ToArray(); var json = JsonSerializer.Serialize(jobs, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(_settings.QueueStateFile, json); } catch (Exception ex) { _logger.LogError(ex, "Failed to save queue state"); } finally { _semaphore.Release(); } } /// /// Load queue state from disk /// private void LoadQueueState() { try { if (File.Exists(_settings.QueueStateFile)) { var json = File.ReadAllText(_settings.QueueStateFile); var jobs = JsonSerializer.Deserialize(json); if (jobs != null) { foreach (var job in jobs) { // Only reload pending or retry-scheduled jobs if (job.Status == PrintJobStatus.Pending || job.Status == PrintJobStatus.RetryScheduled) { _queue.Enqueue(job); } } _logger.LogInformation("Loaded {Count} jobs from queue state", _queue.Count); } } } catch (Exception ex) { _logger.LogError(ex, "Failed to load queue state"); } } /// /// Requeue a job (for retry) /// public void Requeue(PrintJob job) { job.RetryCount++; job.Status = PrintJobStatus.RetryScheduled; job.UpdatedAt = DateTime.Now; if (job.RetryCount >= _settings.MaxRetryAttempts) { _logger.LogWarning("Job {JobId} exceeded max retries, moving to error folder", job.Id); MoveToErrorFolder(job); } else { _logger.LogInformation("Requeueing job {JobId}, attempt {Retry}", job.Id, job.RetryCount); _queue.Enqueue(job); SaveQueueState(); } } /// /// Move a failed job to error folder /// private void MoveToErrorFolder(PrintJob job) { try { var errorFileName = Path.Combine(_settings.ErrorPath, $"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}.txt"); if (File.Exists(job.QueuedFilePath)) { File.Move(job.QueuedFilePath, errorFileName); } // Save error details var errorInfo = Path.Combine(_settings.ErrorPath, $"{Path.GetFileNameWithoutExtension(job.OriginalFilename)}_{job.Id}_error.json"); var json = JsonSerializer.Serialize(job, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(errorInfo, json); job.Status = PrintJobStatus.Failed; SaveQueueState(); } catch (Exception ex) { _logger.LogError(ex, "Failed to move job {JobId} to error folder", job.Id); } } }