From d4b1a4921c1a2c2eefd99b7182134b3a9a21b701 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 22 Jan 2026 09:51:35 +0100 Subject: [PATCH] Refactor worker to use config, DI, and Quartz scheduling - Add WorkerSettings class and update appsettings for config-driven setup - Integrate Quartz.NET for job scheduling (FinalizeDocumentJob, APIEnvelopeJob) - Refactor Program.cs for DI of services (TempFileManager, PDFBurner, etc.) - Implement TempFileManager for temp folder management and cleanup - Rewrite Worker class for config validation, DB check, and lifecycle logging - Update csproj to include Quartz and EnvelopeGenerator.Jobs references - Improve maintainability, error handling, and logging throughout --- .../Configuration/WorkerSettings.cs | 14 ++++ .../EnvelopeGenerator.WorkerService.csproj | 5 ++ EnvelopeGenerator.WorkerService/Program.cs | 60 +++++++++++++++ .../Services/TempFileManager.cs | 74 ++++++++++++++++++ EnvelopeGenerator.WorkerService/Worker.cs | 75 +++++++++++++++---- .../appsettings.Development.json | 24 +++++- .../appsettings.json | 13 ++++ 7 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 EnvelopeGenerator.WorkerService/Configuration/WorkerSettings.cs create mode 100644 EnvelopeGenerator.WorkerService/Services/TempFileManager.cs diff --git a/EnvelopeGenerator.WorkerService/Configuration/WorkerSettings.cs b/EnvelopeGenerator.WorkerService/Configuration/WorkerSettings.cs new file mode 100644 index 00000000..dc4d2c91 --- /dev/null +++ b/EnvelopeGenerator.WorkerService/Configuration/WorkerSettings.cs @@ -0,0 +1,14 @@ +using EnvelopeGenerator.Jobs.FinalizeDocument; + +namespace EnvelopeGenerator.WorkerService.Configuration; + +public sealed class WorkerSettings +{ + public string ConnectionString { get; set; } = string.Empty; + + public bool Debug { get; set; } + + public int IntervalMinutes { get; set; } = 1; + + public PDFBurnerParams PdfBurner { get; set; } = new(); +} diff --git a/EnvelopeGenerator.WorkerService/EnvelopeGenerator.WorkerService.csproj b/EnvelopeGenerator.WorkerService/EnvelopeGenerator.WorkerService.csproj index 868eb4a9..80e64767 100644 --- a/EnvelopeGenerator.WorkerService/EnvelopeGenerator.WorkerService.csproj +++ b/EnvelopeGenerator.WorkerService/EnvelopeGenerator.WorkerService.csproj @@ -9,5 +9,10 @@ + + + + + diff --git a/EnvelopeGenerator.WorkerService/Program.cs b/EnvelopeGenerator.WorkerService/Program.cs index b7adaaf1..03e8e055 100644 --- a/EnvelopeGenerator.WorkerService/Program.cs +++ b/EnvelopeGenerator.WorkerService/Program.cs @@ -1,6 +1,66 @@ +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Jobs.APIBackendJobs; +using EnvelopeGenerator.Jobs.FinalizeDocument; using EnvelopeGenerator.WorkerService; +using EnvelopeGenerator.WorkerService.Configuration; +using EnvelopeGenerator.WorkerService.Services; +using Quartz; var builder = Host.CreateApplicationBuilder(args); + +builder.Services.Configure(builder.Configuration.GetSection("WorkerSettings")); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(provider => +{ + var settings = provider.GetRequiredService>().Value; + var logger = provider.GetRequiredService>(); + return new PDFBurner(logger, settings.PdfBurner); +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddQuartz(q => +{ + q.UseMicrosoftDependencyInjectionJobFactory(); + q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 5); + + var settings = new WorkerSettings(); + builder.Configuration.GetSection("WorkerSettings").Bind(settings); + var intervalMinutes = Math.Max(1, settings.IntervalMinutes); + + var finalizeJobKey = new JobKey("FinalizeDocumentJob"); + q.AddJob(opts => opts + .WithIdentity(finalizeJobKey) + .UsingJobData(Value.DATABASE, settings.ConnectionString)); + + q.AddTrigger(opts => opts + .ForJob(finalizeJobKey) + .WithIdentity("FinalizeDocumentJob-trigger") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(intervalMinutes) + .RepeatForever())); + + var apiJobKey = new JobKey("APIEnvelopeJob"); + q.AddJob(opts => opts + .WithIdentity(apiJobKey) + .UsingJobData(Value.DATABASE, settings.ConnectionString)); + + q.AddTrigger(opts => opts + .ForJob(apiJobKey) + .WithIdentity("APIEnvelopeJob-trigger") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(intervalMinutes) + .RepeatForever())); +}); + +builder.Services.AddQuartzHostedService(options => +{ + options.WaitForJobsToComplete = true; +}); + builder.Services.AddHostedService(); var host = builder.Build(); diff --git a/EnvelopeGenerator.WorkerService/Services/TempFileManager.cs b/EnvelopeGenerator.WorkerService/Services/TempFileManager.cs new file mode 100644 index 00000000..f98e1c09 --- /dev/null +++ b/EnvelopeGenerator.WorkerService/Services/TempFileManager.cs @@ -0,0 +1,74 @@ +using System.IO; +using Microsoft.Extensions.Logging; + +namespace EnvelopeGenerator.WorkerService.Services; + +public sealed class TempFileManager +{ + private readonly ILogger _logger; + + public TempFileManager(ILogger logger) + { + _logger = logger; + TempPath = Path.Combine(Path.GetTempPath(), "EnvelopeGenerator"); + } + + public string TempPath { get; } + + public Task CreateAsync(CancellationToken cancellationToken = default) + { + try + { + if (!Directory.Exists(TempPath)) + { + Directory.CreateDirectory(TempPath); + _logger.LogDebug("Created temp folder {TempPath}", TempPath); + } + else + { + CleanUpFiles(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create temp folder {TempPath}", TempPath); + throw; + } + + return Task.CompletedTask; + } + + public Task CleanupAsync(CancellationToken cancellationToken = default) + { + try + { + if (Directory.Exists(TempPath)) + { + _logger.LogDebug("Deleting temp folder {TempPath}", TempPath); + Directory.Delete(TempPath, recursive: true); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to clean up temp folder {TempPath}", TempPath); + } + + return Task.CompletedTask; + } + + private void CleanUpFiles() + { + foreach (var file in Directory.GetFiles(TempPath)) + { + try + { + _logger.LogDebug("Deleting temp file {File}", file); + File.Delete(file); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete temp file {File}", file); + } + } + } +} diff --git a/EnvelopeGenerator.WorkerService/Worker.cs b/EnvelopeGenerator.WorkerService/Worker.cs index 9e5f2f76..d88c3e63 100644 --- a/EnvelopeGenerator.WorkerService/Worker.cs +++ b/EnvelopeGenerator.WorkerService/Worker.cs @@ -1,24 +1,71 @@ -namespace EnvelopeGenerator.WorkerService +using EnvelopeGenerator.WorkerService.Configuration; +using EnvelopeGenerator.WorkerService.Services; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.WorkerService; + +public class Worker : BackgroundService { - public class Worker : BackgroundService + private readonly ILogger _logger; + private readonly WorkerSettings _settings; + private readonly TempFileManager _tempFiles; + + public Worker( + ILogger logger, + IOptions settings, + TempFileManager tempFiles) + { + _logger = logger; + _settings = settings.Value; + _tempFiles = tempFiles; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting EnvelopeGenerator worker..."); + _logger.LogInformation("Debug mode: {Debug}", _settings.Debug); + + ValidateConfiguration(); + await EnsureDatabaseConnectionAsync(cancellationToken); + await _tempFiles.CreateAsync(cancellationToken); + + await base.StartAsync(cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - private readonly ILogger _logger; + _logger.LogInformation("EnvelopeGenerator worker is running. Jobs are scheduled every {Interval} minute(s).", Math.Max(1, _settings.IntervalMinutes)); + await Task.Delay(Timeout.Infinite, stoppingToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping EnvelopeGenerator worker..."); + await _tempFiles.CleanupAsync(cancellationToken); + await base.StopAsync(cancellationToken); + } - public Worker(ILogger logger) + private void ValidateConfiguration() + { + if (string.IsNullOrWhiteSpace(_settings.ConnectionString)) { - _logger = logger; + throw new InvalidOperationException("Connection string cannot be empty. Configure 'WorkerSettings:ConnectionString'."); } + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + private async Task EnsureDatabaseConnectionAsync(CancellationToken cancellationToken) + { + try + { + await using var connection = new SqlConnection(_settings.ConnectionString); + await connection.OpenAsync(cancellationToken); + _logger.LogInformation("Database connection established successfully."); + } + catch (Exception ex) { - while (!stoppingToken.IsCancellationRequested) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); - } - await Task.Delay(1000, stoppingToken); - } + _logger.LogError(ex, "Database connection could not be established."); + throw; } } } diff --git a/EnvelopeGenerator.WorkerService/appsettings.Development.json b/EnvelopeGenerator.WorkerService/appsettings.Development.json index b2dcdb67..9e3a0d90 100644 --- a/EnvelopeGenerator.WorkerService/appsettings.Development.json +++ b/EnvelopeGenerator.WorkerService/appsettings.Development.json @@ -1,8 +1,30 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.Hosting.Lifetime": "Information" } + }, + "WorkerSettings": { + "ConnectionString": "", + "Debug": true, + "IntervalMinutes": 1, + "PdfBurner": { + "IgnoredLabels": [ + "Date", + "Datum", + "ZIP", + "PLZ", + "Place", + "Ort", + "Position", + "Stellung" + ], + "TopMargin": 0.1, + "YOffset": -0.3, + "FontName": "Arial", + "FontSize": 8, + "FontStyle": "Italic" + } } } diff --git a/EnvelopeGenerator.WorkerService/appsettings.json b/EnvelopeGenerator.WorkerService/appsettings.json index b2dcdb67..b5f3f582 100644 --- a/EnvelopeGenerator.WorkerService/appsettings.json +++ b/EnvelopeGenerator.WorkerService/appsettings.json @@ -4,5 +4,18 @@ "Default": "Information", "Microsoft.Hosting.Lifetime": "Information" } + }, + "WorkerSettings": { + "ConnectionString": "", + "Debug": false, + "IntervalMinutes": 1, + "PdfBurner": { + "IgnoredLabels": ["Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung"], + "TopMargin": 0.1, + "YOffset": -0.3, + "FontName": "Arial", + "FontSize": 8, + "FontStyle": "Italic" + } } }