Init
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
using FinlyticAssets.Models;
|
||||
using FinlyticAssets.Models.DataToObject.TradeRepublic;
|
||||
using FinlyticCore.Models.Assets;
|
||||
|
||||
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 AssetsFullScanService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<AssetsFullScanService> _logger;
|
||||
|
||||
private AssetsCount? _assetsCount;
|
||||
private AssetsCount? _currAssetsCount;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssetsFullScanService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serviceScopeFactory">Factory used to create service scopes for database and API requests.</param>
|
||||
/// <param name="logger">Logger for service lifecycle and scanning progress messages.</param>
|
||||
public AssetsFullScanService(IServiceScopeFactory serviceScopeFactory, ILogger<AssetsFullScanService> logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the background scanning task, handling initial startup, recovery, and periodic full-scan cycles.
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken">Triggered when the host is shutting down.</param>
|
||||
/// <returns>A task that represents the background operation.</returns>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("AssetsFullScanService has started.");
|
||||
|
||||
try
|
||||
{
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
||||
_logger.LogInformation("Building initial asset index on service startup...");
|
||||
await indexService.ReCreateIndexFileAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to build initial asset index on startup. Continuing service execution.");
|
||||
}
|
||||
|
||||
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("Requesting total asset counts from Trade Republic...");
|
||||
_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("Recovery: {AssetType} was already processed. Skipping.", type);
|
||||
continue;
|
||||
}
|
||||
isRecoveryMode = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var settings = await settingsService.GetSettings();
|
||||
settings.CurrentScanningType = type;
|
||||
settings.CurrentScanningPage = 0;
|
||||
await settingsService.SaveSettings(settings);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Processing asset type: {AssetType}...", type);
|
||||
await HandleAssetType(type, tradeRepublicService, settingsService, assetsDbService, indexService, stoppingToken);
|
||||
|
||||
var currentSettings = await settingsService.GetSettings();
|
||||
var delaySeconds = currentSettings.FinishedInitialScan
|
||||
? currentSettings.AssetUpdateTypeDelay
|
||||
: currentSettings.InitAssetUpdateTypeDelay;
|
||||
|
||||
var jitter = Random.Shared.Next(0, 480);
|
||||
_logger.LogDebug("Waiting {Delay} seconds before the next asset type.", delaySeconds + jitter);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken);
|
||||
}
|
||||
|
||||
var finalSettings = await settingsService.GetSettings();
|
||||
finalSettings.CurrentScanningPage = 0;
|
||||
|
||||
if (!finalSettings.FinishedInitialScan && !stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Initial scan successfully completed. Switching FinishedInitialScan to true.");
|
||||
finalSettings.FinishedInitialScan = true;
|
||||
}
|
||||
|
||||
await settingsService.SaveSettings(finalSettings);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Full scan cycle completed. Waiting 1 minute before starting the next cycle.");
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An unhandled exception occurred in AssetsFullScanService. Retrying in 10 seconds.");
|
||||
try { await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } catch { /* Ignore */ }
|
||||
}
|
||||
} while (!stoppingToken.IsCancellationRequested);
|
||||
|
||||
_logger.LogInformation("AssetsFullScanService is stopping.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the scanning process for a specific asset type, iterating through its paginated results.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of assets to process (e.g., Stock, Crypto).</param>
|
||||
/// <param name="tradeRepublicService">Service to fetch asset data from Trade Republic.</param>
|
||||
/// <param name="settingsDbService">Service to read and write application state/settings.</param>
|
||||
/// <param name="assetsDbService">Service to insert or update assets in the database.</param>
|
||||
/// <param name="indexService">Service to recreate the local index file if needed.</param>
|
||||
/// <param name="stoppingToken">Cancellation token monitored for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
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("No assets found for type {AssetType}.", type);
|
||||
return;
|
||||
}
|
||||
|
||||
_currAssetsCount ??= new AssetsCount();
|
||||
var currentItemOffset = 0;
|
||||
|
||||
var settings = await settingsDbService.GetSettings();
|
||||
if (settings.CurrentScanningType == type && settings.CurrentScanningPage > 0)
|
||||
{
|
||||
var pageSize = settings.TradeRepublicMaxRequestPageSize <= 0 ? 50 : settings.TradeRepublicMaxRequestPageSize;
|
||||
if (pageSize > 100) pageSize = 100;
|
||||
|
||||
currentItemOffset = (settings.CurrentScanningPage - 1) * pageSize;
|
||||
_logger.LogInformation("Resuming full scan for {AssetType} from Page {Page} (Offset: {Offset}).",
|
||||
type, settings.CurrentScanningPage, currentItemOffset);
|
||||
}
|
||||
|
||||
while (currentItemOffset < totalCount && !stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var currentSettings = await settingsDbService.GetSettings();
|
||||
var pageSize = currentSettings.TradeRepublicMaxRequestPageSize;
|
||||
if (pageSize <= 0 || pageSize > 100) pageSize = 100;
|
||||
|
||||
var currentPage = (currentItemOffset / pageSize) + 1;
|
||||
|
||||
currentSettings.CurrentScanningType = type;
|
||||
currentSettings.CurrentScanningPage = currentPage;
|
||||
await settingsDbService.SaveSettings(currentSettings);
|
||||
|
||||
_logger.LogDebug("Fetching {AssetType} - Page {Page} (Size: {PageSize}). Offset: {Offset}/{Total}",
|
||||
type, currentPage, pageSize, currentItemOffset, totalCount);
|
||||
|
||||
var assets = await tradeRepublicService.GetAssets(type, currentPage, pageSize, stoppingToken);
|
||||
|
||||
if (assets?.Results == null || assets.Results.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("Fetch for {AssetType} (Page {Page}) returned no results. Retrying in 5 seconds...", type, currentPage);
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
await ProcessAssets(assets.Results, assetsDbService, indexService,stoppingToken);
|
||||
_currAssetsCount.SetCountOfType(type, _currAssetsCount.GetCountFromType(type) + assets.Results.Count);
|
||||
|
||||
currentItemOffset = currentPage * pageSize;
|
||||
|
||||
if (assets.Results.Count < pageSize)
|
||||
{
|
||||
_logger.LogInformation("Reached the last page for {AssetType}.", type);
|
||||
break;
|
||||
}
|
||||
|
||||
var delaySeconds = currentSettings.FinishedInitialScan
|
||||
? currentSettings.BatchAssetUpdateDelay
|
||||
: currentSettings.InitBatchAssetUpdateDelay;
|
||||
|
||||
var jitter = Random.Shared.Next(0, 360);
|
||||
_logger.LogDebug("Waiting {Delay} seconds before the next batch.", delaySeconds + jitter);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds + jitter), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a list of fetched Trade Republic assets, updates them in the database,
|
||||
/// and triggers an index recreation if changes were detected.
|
||||
/// </summary>
|
||||
/// <param name="assets">The list of Trade Republic assets to process.</param>
|
||||
/// <param name="assetsDbService">Service to insert or update assets in the database.</param>
|
||||
/// <param name="indexService">Service to recreate the local index file.</param>
|
||||
/// <param name="stoppingToken">Cancellation token monitored for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
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("[Scan] {Count} assets passed to the DB service. {Changed} modifications/inserts executed.", assets.Count, changedRows);
|
||||
|
||||
if (changedRows > 0)
|
||||
{
|
||||
_logger.LogInformation("Database modifications detected. Recreating the asset index file...");
|
||||
await indexService.ReCreateIndexFileAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user