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
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
@@ -9,5 +9,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.9.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Jobs\EnvelopeGenerator.Jobs.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +1,66 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Jobs.APIBackendJobs;
|
||||||
|
using EnvelopeGenerator.Jobs.FinalizeDocument;
|
||||||
using EnvelopeGenerator.WorkerService;
|
using EnvelopeGenerator.WorkerService;
|
||||||
|
using EnvelopeGenerator.WorkerService.Configuration;
|
||||||
|
using EnvelopeGenerator.WorkerService.Services;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.Configure<WorkerSettings>(builder.Configuration.GetSection("WorkerSettings"));
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<TempFileManager>();
|
||||||
|
builder.Services.AddSingleton(provider =>
|
||||||
|
{
|
||||||
|
var settings = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<WorkerSettings>>().Value;
|
||||||
|
var logger = provider.GetRequiredService<ILogger<PDFBurner>>();
|
||||||
|
return new PDFBurner(logger, settings.PdfBurner);
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<PDFMerger>();
|
||||||
|
builder.Services.AddSingleton<ReportCreator>();
|
||||||
|
|
||||||
|
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<FinalizeDocumentJob>(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<APIEnvelopeJob>(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<Worker>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|||||||
74
EnvelopeGenerator.WorkerService/Services/TempFileManager.cs
Normal file
74
EnvelopeGenerator.WorkerService/Services/TempFileManager.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WorkerService.Services;
|
||||||
|
|
||||||
|
public sealed class TempFileManager
|
||||||
|
{
|
||||||
|
private readonly ILogger<TempFileManager> _logger;
|
||||||
|
|
||||||
|
public TempFileManager(ILogger<TempFileManager> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Worker> _logger;
|
||||||
|
private readonly WorkerSettings _settings;
|
||||||
|
private readonly TempFileManager _tempFiles;
|
||||||
|
|
||||||
|
public Worker(
|
||||||
|
ILogger<Worker> logger,
|
||||||
|
IOptions<WorkerSettings> settings,
|
||||||
|
TempFileManager tempFiles)
|
||||||
{
|
{
|
||||||
private readonly ILogger<Worker> _logger;
|
_logger = logger;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_tempFiles = tempFiles;
|
||||||
|
}
|
||||||
|
|
||||||
public Worker(ILogger<Worker> logger)
|
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)
|
||||||
|
{
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
await using var connection = new SqlConnection(_settings.ConnectionString);
|
||||||
{
|
await connection.OpenAsync(cancellationToken);
|
||||||
if (_logger.IsEnabled(LogLevel.Information))
|
_logger.LogInformation("Database connection established successfully.");
|
||||||
{
|
}
|
||||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
await Task.Delay(1000, stoppingToken);
|
_logger.LogError(ex, "Database connection could not be established.");
|
||||||
}
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Debug",
|
||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,18 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.Hosting.Lifetime": "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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user