feat(Assets): update asset services and background workers

This commit is contained in:
2026-08-09 21:01:39 +02:00
parent b57cc9894c
commit e0778b88ea
32 changed files with 1020 additions and 2062 deletions
@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FinlyticAssets.Models;
using FinlyticCore.Models.Assets;
using FinlyticCore.Models.TradeRepublic;
using FinlyticCore.Services.TradeRepublic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FinlyticAssets.Services;
/// <summary>
/// A background service that runs a continuous asset synchronization loop,
/// scanning Trade Republic to retrieve, update, and index all supported asset types.
/// </summary>
public class AssetScannerBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<AssetScannerBackgroundService> _logger;
private AssetsCount? _assetsCount;
private AssetsCount? _currAssetsCount;
public AssetScannerBackgroundService(IServiceScopeFactory serviceScopeFactory, ILogger<AssetScannerBackgroundService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("[{Channel}] AssetScannerBackgroundService has started.", "AssetsChannel");
try
{
using var scope = _serviceScopeFactory.CreateScope();
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
_logger.LogInformation("[{Channel}] Building initial asset index on service startup...", "AssetsChannel");
await indexService.ReCreateIndexFileAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to build initial asset index on startup. Continuing service execution.", "AssetsChannel");
}
do
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var tradeRepublicService = scope.ServiceProvider.GetRequiredService<ITradeRepublicService>();
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
var assetsDbService = scope.ServiceProvider.GetRequiredService<IAssetsDbService>();
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
_logger.LogInformation("[{Channel}] Requesting total asset counts from Trade Republic...", "AssetsChannel");
_assetsCount = await tradeRepublicService.GetAssetsCount(stoppingToken);
_currAssetsCount = new AssetsCount();
var initSettings = await settingsService.GetSettings();
var isRecoveryMode = initSettings.CurrentScanningPage > 0;
foreach (var type in Enum.GetValues<AssetType>())
{
if (stoppingToken.IsCancellationRequested) break;
if (isRecoveryMode)
{
if (type != initSettings.CurrentScanningType)
{
_logger.LogInformation("[{Channel}] Recovery: {AssetType} was already processed. Skipping.", "AssetsChannel", type);
continue;
}
isRecoveryMode = false;
}
else
{
var settings = await settingsService.GetSettings();
settings.CurrentScanningType = type;
settings.CurrentScanningPage = 0;
await settingsService.SaveSettings(settings);
}
_logger.LogInformation("[{Channel}] Processing asset type: {AssetType}...", "AssetsChannel", type);
await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken);
var currentSettings = await settingsService.GetSettings();
var delaySeconds = currentSettings.FinishedInitialScan
? currentSettings.AssetUpdateTypeDelay
: currentSettings.InitAssetUpdateTypeDelay;
if (delaySeconds > 0)
{
var jitter = Random.Shared.Next(0, Math.Min(15, delaySeconds));
_logger.LogInformation("[{Channel}] Waiting {Delay}s before next asset type ({Type}).", "AssetsChannel", delaySeconds + jitter, type);
await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken);
}
}
var finalSettings = await settingsService.GetSettings();
finalSettings.CurrentScanningPage = 0;
if (!finalSettings.FinishedInitialScan && !stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("[{Channel}] Initial scan successfully completed. Switching FinishedInitialScan to true.", "AssetsChannel");
finalSettings.FinishedInitialScan = true;
}
await settingsService.SaveSettings(finalSettings);
_logger.LogInformation("[{Channel}] Full scan cycle completed. Waiting 1 minute before starting the next cycle.", "AssetsChannel");
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
catch (Exception e) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(e, "[{Channel}] An unhandled exception occurred in AssetScannerBackgroundService. Retrying in 10 seconds.", "AssetsChannel");
try { await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } catch { /* Ignore */ }
}
} while (!stoppingToken.IsCancellationRequested);
_logger.LogInformation("[{Channel}] AssetScannerBackgroundService is stopping.", "AssetsChannel");
}
private async Task HandleAssetType(
AssetType type,
ITradeRepublicService tradeRepublicService,
ISettingsDbService settingsDbService,
IAssetsDbService assetsDbService,
IAssetsIndexService indexService,
CancellationToken stoppingToken)
{
var totalCount = _assetsCount?.GetCountFromType(type) ?? 0;
if (totalCount == 0)
{
_logger.LogWarning("[{Channel}] No assets found for type {AssetType}.", "AssetsChannel", type);
return;
}
_currAssetsCount ??= new AssetsCount();
var currentItemOffset = 0;
var settings = await settingsDbService.GetSettings();
var pageSize = Math.Clamp(settings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : settings.TradeRepublicMaxRequestPageSize, 1, 100);
if (settings.CurrentScanningType == type && settings.CurrentScanningPage > 0)
{
currentItemOffset = (settings.CurrentScanningPage - 1) * pageSize;
_logger.LogInformation("[{Channel}] Resuming full scan for {AssetType} from Page {Page} (Calculated Offset: {Offset}).",
"AssetsChannel", type, settings.CurrentScanningPage, currentItemOffset);
}
while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested)
{
var currentSettings = await settingsDbService.GetSettings();
pageSize = Math.Clamp(currentSettings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : currentSettings.TradeRepublicMaxRequestPageSize, 1, 100);
var currentPage = (currentItemOffset / pageSize) + 1;
currentSettings.CurrentScanningType = type;
currentSettings.CurrentScanningPage = currentPage;
await settingsDbService.SaveSettings(currentSettings);
_logger.LogDebug("Fetching {AssetType} - Page {Page}. Numerical Offset: {Offset}/{Total}",
type, currentPage, currentItemOffset, totalCount);
var assets = await tradeRepublicService.GetAssets(type, currentPage, pageSize, stoppingToken);
// Keine Ergebnisse geliefert -> Katalogende erreicht
if (assets?.Results == null || assets.Results.Count == 0)
{
_logger.LogInformation("[{Channel}] Fetch for {AssetType} (Page {Page}) returned no results. Reached end of available assets.", "AssetsChannel", type, currentPage);
currentSettings.CurrentScanningPage = 0;
await settingsDbService.SaveSettings(currentSettings);
break;
}
await ProcessAssets(assets.Results, assetsDbService, indexService, stoppingToken);
_currAssetsCount.SetCountOfType(type, _currAssetsCount.GetCountFromType(type) + assets.Results.Count);
currentItemOffset += assets.Results.Count;
// Unvollständige Seite -> Letzte Seite abgearbeitet
if (assets.Results.Count < pageSize)
{
_logger.LogInformation("[{Channel}] Reached the last page for {AssetType}.", "AssetsChannel", type);
currentSettings.CurrentScanningPage = 0;
await settingsDbService.SaveSettings(currentSettings);
break;
}
var delaySeconds = currentSettings.FinishedInitialScan
? currentSettings.BatchAssetUpdateDelay
: currentSettings.InitBatchAssetUpdateDelay;
if (delaySeconds > 0)
{
// Angemessener Jitter (0 bis max. 5 Sek. bzw. kleiner als delaySeconds)
var maxJitter = Math.Min(5, delaySeconds);
var jitter = Random.Shared.Next(0, maxJitter + 1);
_logger.LogDebug("Waiting {Delay} seconds before the next batch.", delaySeconds + jitter);
await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken);
}
}
_logger.LogInformation("[{Channel}] Finished scanning {AssetType}. Total scanned in this cycle: {Count}/{Total}",
"AssetsChannel", type, _currAssetsCount.GetCountFromType(type), totalCount);
}
private async Task ProcessAssets(
IList<TradeRepublicAsset> assets,
IAssetsDbService assetsDbService,
IAssetsIndexService indexService,
CancellationToken stoppingToken)
{
if (assets == null || assets.Count == 0) return;
var changedRows = await assetsDbService.AddOrUpdateAssetsAsync(assets);
_logger.LogInformation("[{Channel}] [Scan] {Count} assets passed to the DB service. {Changed} modifications/inserts executed.", "AssetsChannel", assets.Count, changedRows);
if (changedRows > 0)
{
_logger.LogInformation("[{Channel}] Database modifications detected. Recreating the asset index file...", "AssetsChannel");
await indexService.ReCreateIndexFileAsync(stoppingToken);
}
}
}