fixed health checks and download service factory

This commit is contained in:
Flaminel
2025-06-16 20:17:06 +03:00
parent b4548573ee
commit f651663fd3
8 changed files with 352 additions and 198 deletions

View File

@@ -1,6 +1,5 @@
using Common.Configuration;
using Common.Exceptions;
using Data;
using Data.Models.Deluge.Response;
using Infrastructure.Events;
using Infrastructure.Interceptors;
@@ -15,7 +14,7 @@ namespace Infrastructure.Verticals.DownloadClient.Deluge;
public partial class DelugeService : DownloadService, IDelugeService
{
private DelugeClient? _client;
private readonly DelugeClient _client;
public DelugeService(
ILogger<DelugeService> logger,
@@ -26,48 +25,19 @@ public partial class DelugeService : DownloadService, IDelugeService
IHardLinkFileService hardLinkFileService,
IDynamicHttpClientProvider httpClientProvider,
EventPublisher eventPublisher,
BlocklistProvider blocklistProvider
BlocklistProvider blocklistProvider,
DownloadClientConfig downloadClientConfig
) : base(
logger, cache,
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
httpClientProvider, eventPublisher, blocklistProvider
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
)
{
// Client will be initialized when Initialize() is called with a specific client configuration
// TODO initialize client & httpclient here
}
/// <inheritdoc />
public override void Initialize(DownloadClientConfig downloadClientConfig)
{
// Initialize base service first
base.Initialize(downloadClientConfig);
// Ensure client type is correct
if (downloadClientConfig.TypeName != Common.Enums.DownloadClientTypeName.Deluge)
{
throw new InvalidOperationException($"Cannot initialize DelugeService with client type {downloadClientConfig.TypeName}");
}
if (_httpClient == null)
{
throw new InvalidOperationException("HTTP client is not initialized");
}
// Create Deluge client
_client = new DelugeClient(downloadClientConfig, _httpClient);
_logger.LogInformation("Initialized Deluge service for client {clientName} ({clientId})",
downloadClientConfig.Name, downloadClientConfig.Id);
}
public override async Task LoginAsync()
{
if (_client == null)
{
throw new InvalidOperationException("Deluge client is not initialized");
}
try
{
await _client.LoginAsync();
@@ -86,6 +56,68 @@ public partial class DelugeService : DownloadService, IDelugeService
}
}
public override async Task<HealthCheckResult> HealthCheckAsync()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) ||
!string.IsNullOrEmpty(_downloadClientConfig.Password);
if (hasCredentials)
{
// If credentials are provided, we must be able to login and connect for the service to be healthy
await _client.LoginAsync();
if (!await _client.IsConnected() && !await _client.Connect())
{
throw new Exception("Deluge WebUI is not connected to the daemon");
}
_logger.LogDebug("Health check: Successfully logged in to Deluge client {clientId}", _downloadClientConfig.Id);
}
else
{
// If no credentials, test basic connectivity to the web UI
// We'll try a simple HTTP request to verify the service is running
if (_httpClient == null)
{
throw new InvalidOperationException("HTTP client is not initialized");
}
var response = await _httpClient.GetAsync("/");
if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.Unauthorized)
{
throw new Exception($"Service returned status code: {response.StatusCode}");
}
_logger.LogDebug("Health check: Successfully connected to Deluge client {clientId}", _downloadClientConfig.Id);
}
stopwatch.Stop();
return new HealthCheckResult
{
IsHealthy = true,
ResponseTime = stopwatch.Elapsed
};
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogWarning(ex, "Health check failed for Deluge client {clientId}", _downloadClientConfig.Id);
return new HealthCheckResult
{
IsHealthy = false,
ErrorMessage = $"Connection failed: {ex.Message}",
ResponseTime = stopwatch.Elapsed
};
}
}
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile)
{
if (contents is null)
@@ -110,7 +142,5 @@ public partial class DelugeService : DownloadService, IDelugeService
public override void Dispose()
{
_client = null;
_httpClient?.Dispose();
}
}

View File

@@ -19,6 +19,13 @@ using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.DownloadClient;
public class HealthCheckResult
{
public bool IsHealthy { get; set; }
public string? ErrorMessage { get; set; }
public TimeSpan ResponseTime { get; set; }
}
public abstract class DownloadService : IDownloadService
{
protected readonly ILogger<DownloadService> _logger;
@@ -28,17 +35,11 @@ public abstract class DownloadService : IDownloadService
protected readonly MemoryCacheEntryOptions _cacheOptions;
protected readonly IDryRunInterceptor _dryRunInterceptor;
protected readonly IHardLinkFileService _hardLinkFileService;
protected readonly IDynamicHttpClientProvider _httpClientProvider;
protected readonly EventPublisher _eventPublisher;
protected readonly BlocklistProvider _blocklistProvider;
protected HttpClient? _httpClient;
protected readonly HttpClient _httpClient;
protected readonly DownloadClientConfig _downloadClientConfig;
// Client-specific configuration
protected DownloadClientConfig _downloadClientConfig;
// HTTP client for this service
protected DownloadService(
ILogger<DownloadService> logger,
IMemoryCache cache,
@@ -48,7 +49,8 @@ public abstract class DownloadService : IDownloadService
IHardLinkFileService hardLinkFileService,
IDynamicHttpClientProvider httpClientProvider,
EventPublisher eventPublisher,
BlocklistProvider blocklistProvider
BlocklistProvider blocklistProvider,
DownloadClientConfig downloadClientConfig
)
{
_logger = logger;
@@ -57,11 +59,12 @@ public abstract class DownloadService : IDownloadService
_striker = striker;
_dryRunInterceptor = dryRunInterceptor;
_hardLinkFileService = hardLinkFileService;
_httpClientProvider = httpClientProvider;
_eventPublisher = eventPublisher;
_blocklistProvider = blocklistProvider;
_cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
_downloadClientConfig = downloadClientConfig;
_httpClient = httpClientProvider.CreateClient(downloadClientConfig);
}
/// <inheritdoc />
@@ -70,22 +73,24 @@ public abstract class DownloadService : IDownloadService
return _downloadClientConfig.Id;
}
/// <inheritdoc />
public virtual void Initialize(DownloadClientConfig downloadClientConfig)
{
_downloadClientConfig = downloadClientConfig;
// Create HTTP client for this service
_httpClient = _httpClientProvider.CreateClient(downloadClientConfig);
_logger.LogDebug("Initialized download service for client {clientId} ({type})",
downloadClientConfig.Id, downloadClientConfig.TypeName);
}
// /// <inheritdoc />
// public virtual void Initialize(DownloadClientConfig downloadClientConfig)
// {
// _downloadClientConfig = downloadClientConfig;
//
// // Create HTTP client for this service
// _httpClient = _httpClientProvider.CreateClient(downloadClientConfig);
//
// _logger.LogDebug("Initialized download service for client {clientId} ({type})",
// downloadClientConfig.Id, downloadClientConfig.TypeName);
// }
public abstract void Dispose();
public abstract Task LoginAsync();
public abstract Task<HealthCheckResult> HealthCheckAsync();
public abstract Task<DownloadCheckResult> ShouldRemoveFromArrQueueAsync(string hash,
IReadOnlyList<string> ignoredDownloads);

View File

@@ -1,8 +1,15 @@
using Common.Configuration;
using Common.Enums;
using Infrastructure.Events;
using Infrastructure.Http;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadClient.QBittorrent;
using Infrastructure.Verticals.DownloadClient.Transmission;
using Infrastructure.Verticals.Files;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -63,23 +70,73 @@ public sealed class DownloadServiceFactory
return downloadClientConfig.TypeName switch
{
DownloadClientTypeName.QBittorrent => CreateClientService<QBitService>(downloadClientConfig),
DownloadClientTypeName.Deluge => CreateClientService<DelugeService>(downloadClientConfig),
DownloadClientTypeName.Transmission => CreateClientService<TransmissionService>(downloadClientConfig),
DownloadClientTypeName.QBittorrent => CreateQBitService(downloadClientConfig),
DownloadClientTypeName.Deluge => CreateDelugeService(downloadClientConfig),
DownloadClientTypeName.Transmission => CreateTransmissionService(downloadClientConfig),
_ => throw new NotSupportedException($"Download client type {downloadClientConfig.TypeName} is not supported")
};
}
/// <summary>
/// Creates a download client service for a specific client type
/// </summary>
/// <typeparam name="T">The type of download service to create</typeparam>
/// <param name="downloadClientConfig">The client configuration</param>
/// <returns>An implementation of IDownloadService</returns>
private T CreateClientService<T>(DownloadClientConfig downloadClientConfig) where T : IDownloadService
private QBitService CreateQBitService(DownloadClientConfig downloadClientConfig)
{
var service = _serviceProvider.GetRequiredService<T>();
service.Initialize(downloadClientConfig);
var logger = _serviceProvider.GetRequiredService<ILogger<QBitService>>();
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
var striker = _serviceProvider.GetRequiredService<IStriker>();
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
var httpClientProvider = _serviceProvider.GetRequiredService<IDynamicHttpClientProvider>();
var eventPublisher = _serviceProvider.GetRequiredService<EventPublisher>();
var blocklistProvider = _serviceProvider.GetRequiredService<BlocklistProvider>();
// Create the QBitService instance
QBitService service = new(
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
);
return service;
}
private DelugeService CreateDelugeService(DownloadClientConfig downloadClientConfig)
{
var logger = _serviceProvider.GetRequiredService<ILogger<DelugeService>>();
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
var striker = _serviceProvider.GetRequiredService<IStriker>();
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
var httpClientProvider = _serviceProvider.GetRequiredService<IDynamicHttpClientProvider>();
var eventPublisher = _serviceProvider.GetRequiredService<EventPublisher>();
var blocklistProvider = _serviceProvider.GetRequiredService<BlocklistProvider>();
// Create the DelugeService instance
DelugeService service = new(
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
);
return service;
}
private TransmissionService CreateTransmissionService(DownloadClientConfig downloadClientConfig)
{
var logger = _serviceProvider.GetRequiredService<ILogger<TransmissionService>>();
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
var striker = _serviceProvider.GetRequiredService<IStriker>();
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
var httpClientProvider = _serviceProvider.GetRequiredService<IDynamicHttpClientProvider>();
var eventPublisher = _serviceProvider.GetRequiredService<EventPublisher>();
var blocklistProvider = _serviceProvider.GetRequiredService<BlocklistProvider>();
// Create the TransmissionService instance
TransmissionService service = new(
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
);
return service;
}
}

View File

@@ -1,11 +1,4 @@
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Common.Configuration;
using Data.Models.Configuration.DownloadCleaner;
using Data.Models.Configuration.QueueCleaner;
using Data.Enums;
using Infrastructure.Interceptors;
using QBittorrent.Client;
namespace Infrastructure.Verticals.DownloadClient;
@@ -17,14 +10,14 @@ public interface IDownloadService : IDisposable
/// <returns>The client ID</returns>
Guid GetClientId();
/// <summary>
/// Initializes the download service with client-specific configuration
/// </summary>
/// <param name="downloadClientConfig">The client configuration</param>
public void Initialize(DownloadClientConfig downloadClientConfig);
public Task LoginAsync();
/// <summary>
/// Performs a health check on the download client
/// </summary>
/// <returns>The health check result</returns>
public Task<HealthCheckResult> HealthCheckAsync();
/// <summary>
/// Checks whether the download should be removed from the *arr queue.
/// </summary>

View File

@@ -14,11 +14,10 @@ namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
public partial class QBitService : DownloadService, IQBitService
{
protected QBittorrentClient? _client;
protected readonly QBittorrentClient _client;
public QBitService(
ILogger<QBitService> logger,
IHttpClientFactory httpClientFactory,
IMemoryCache cache,
IFilenameEvaluator filenameEvaluator,
IStriker striker,
@@ -26,35 +25,18 @@ public partial class QBitService : DownloadService, IQBitService
IHardLinkFileService hardLinkFileService,
IDynamicHttpClientProvider httpClientProvider,
EventPublisher eventPublisher,
BlocklistProvider blocklistProvider
BlocklistProvider blocklistProvider,
DownloadClientConfig downloadClientConfig
) : base(
logger, cache, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
httpClientProvider, eventPublisher, blocklistProvider
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
)
{
// Client will be initialized when Initialize() is called with a specific client configuration
_client = new QBittorrentClient(_httpClient, downloadClientConfig.Url);
}
/// <inheritdoc />
public override void Initialize(DownloadClientConfig downloadClientConfig)
{
// Initialize base service first
base.Initialize(downloadClientConfig);
// Create QBittorrent client
_client = new QBittorrentClient(_httpClient, downloadClientConfig.Url);
_logger.LogInformation("Initialized QBittorrent service for client {clientName} ({clientId})",
downloadClientConfig.Name, downloadClientConfig.Id);
}
public override async Task LoginAsync()
{
if (_client == null)
{
throw new InvalidOperationException("QBittorrent client is not initialized");
}
if (string.IsNullOrEmpty(_downloadClientConfig.Username) && string.IsNullOrEmpty(_downloadClientConfig.Password))
{
_logger.LogDebug("No credentials configured for client {clientId}, skipping login", _downloadClientConfig.Id);
@@ -72,14 +54,54 @@ public partial class QBitService : DownloadService, IQBitService
throw;
}
}
public override async Task<HealthCheckResult> HealthCheckAsync()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) ||
!string.IsNullOrEmpty(_downloadClientConfig.Password);
if (hasCredentials)
{
// If credentials are provided, we must be able to login for the service to be healthy
await _client.LoginAsync(_downloadClientConfig.Username, _downloadClientConfig.Password);
_logger.LogDebug("Health check: Successfully logged in to QBittorrent client {clientId}", _downloadClientConfig.Id);
}
else
{
// If no credentials, test connectivity using version endpoint
await _client.GetApiVersionAsync();
_logger.LogDebug("Health check: Successfully connected to QBittorrent client {clientId}", _downloadClientConfig.Id);
}
stopwatch.Stop();
return new HealthCheckResult
{
IsHealthy = true,
ResponseTime = stopwatch.Elapsed
};
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogWarning(ex, "Health check failed for QBittorrent client {clientId}", _downloadClientConfig.Id);
return new HealthCheckResult
{
IsHealthy = false,
ErrorMessage = $"Connection failed: {ex.Message}",
ResponseTime = stopwatch.Elapsed
};
}
}
private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
{
if (_client == null)
{
throw new InvalidOperationException("QBittorrent client is not initialized");
}
return (await _client.GetTorrentTrackersAsync(hash))
.Where(x => x.Url.Contains("**"))
.ToList();
@@ -87,7 +109,6 @@ public partial class QBitService : DownloadService, IQBitService
public override void Dispose()
{
_client?.Dispose();
_httpClient?.Dispose();
_client.Dispose();
}
}

View File

@@ -14,7 +14,7 @@ namespace Infrastructure.Verticals.DownloadClient.Transmission;
public partial class TransmissionService : DownloadService, ITransmissionService
{
private Client? _client;
private readonly Client _client;
private static readonly string[] Fields =
[
@@ -44,55 +44,61 @@ public partial class TransmissionService : DownloadService, ITransmissionService
IHardLinkFileService hardLinkFileService,
IDynamicHttpClientProvider httpClientProvider,
EventPublisher eventPublisher,
BlocklistProvider blocklistProvider
BlocklistProvider blocklistProvider,
DownloadClientConfig downloadClientConfig
) : base(
logger, cache,
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
httpClientProvider, eventPublisher, blocklistProvider
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
)
{
// Client will be initialized when Initialize() is called with a specific client configuration
UriBuilder uriBuilder = new(_downloadClientConfig.Url);
uriBuilder.Path = string.IsNullOrEmpty(_downloadClientConfig.UrlBase)
? $"{uriBuilder.Path.TrimEnd('/')}/rpc"
: $"{uriBuilder.Path.TrimEnd('/')}/{_downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc";
// TODO check if httpClientProvider creates a client as expected
_client = new Client(
_httpClient,
uriBuilder.Uri.ToString(),
login: _downloadClientConfig.Username,
password: _downloadClientConfig.Password
);
}
/// <inheritdoc />
public override void Initialize(DownloadClientConfig downloadClientConfig)
{
// Initialize base service first
base.Initialize(downloadClientConfig);
// Ensure client type is correct
if (downloadClientConfig.TypeName != Common.Enums.DownloadClientTypeName.Transmission)
{
throw new InvalidOperationException($"Cannot initialize TransmissionService with client type {downloadClientConfig.TypeName}");
}
if (_httpClient == null)
{
throw new InvalidOperationException("HTTP client is not initialized");
}
// Create the RPC path
string rpcPath = string.IsNullOrEmpty(downloadClientConfig.UrlBase)
? "/rpc"
: $"/{downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc";
// Create full RPC URL
string rpcUrl = new UriBuilder(downloadClientConfig.Url) { Path = rpcPath }.Uri.ToString();
// Create Transmission client
_client = new Client(_httpClient, rpcUrl, login: downloadClientConfig.Username, password: downloadClientConfig.Password);
_logger.LogInformation("Initialized Transmission service for client {clientName} ({clientId})",
downloadClientConfig.Name, downloadClientConfig.Id);
}
// /// <inheritdoc />
// public override void Initialize(DownloadClientConfig downloadClientConfig)
// {
// // Initialize base service first
// base.Initialize(downloadClientConfig);
//
// // Ensure client type is correct
// if (downloadClientConfig.TypeName != Common.Enums.DownloadClientTypeName.Transmission)
// {
// throw new InvalidOperationException($"Cannot initialize TransmissionService with client type {downloadClientConfig.TypeName}");
// }
//
// if (_httpClient == null)
// {
// throw new InvalidOperationException("HTTP client is not initialized");
// }
//
// // Create the RPC path
// string rpcPath = string.IsNullOrEmpty(downloadClientConfig.UrlBase)
// ? "/rpc"
// : $"/{downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc";
//
// // Create full RPC URL
// string rpcUrl = new UriBuilder(downloadClientConfig.Url) { Path = rpcPath }.Uri.ToString();
//
// // Create Transmission client
// _client = new Client(_httpClient, rpcUrl, login: downloadClientConfig.Username, password: downloadClientConfig.Password);
//
// _logger.LogInformation("Initialized Transmission service for client {clientName} ({clientId})",
// downloadClientConfig.Name, downloadClientConfig.Id);
// }
public override async Task LoginAsync()
{
if (_client == null)
{
throw new InvalidOperationException("Transmission client is not initialized");
}
try
{
await _client.GetSessionInformationAsync();
@@ -104,11 +110,78 @@ public partial class TransmissionService : DownloadService, ITransmissionService
throw;
}
}
public override async Task<HealthCheckResult> HealthCheckAsync()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) ||
!string.IsNullOrEmpty(_downloadClientConfig.Password);
if (hasCredentials)
{
// If credentials are provided, we must be able to authenticate for the service to be healthy
await _client.GetSessionInformationAsync();
_logger.LogDebug("Health check: Successfully authenticated with Transmission client {clientId}", _downloadClientConfig.Id);
}
else
{
// If no credentials, test basic connectivity with a simple RPC call
// This will likely fail with authentication error, but that tells us the service is running
try
{
await _client.GetSessionInformationAsync();
_logger.LogDebug("Health check: Successfully connected to Transmission client {clientId}", _downloadClientConfig.Id);
}
catch (Exception ex) when (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized"))
{
// Authentication error means the service is running but requires credentials
_logger.LogDebug("Health check: Transmission client {clientId} is running but requires authentication", _downloadClientConfig.Id);
}
}
stopwatch.Stop();
return new HealthCheckResult
{
IsHealthy = true,
ResponseTime = stopwatch.Elapsed
};
}
catch (Exception ex)
{
stopwatch.Stop();
// Check if this is an authentication error when no credentials are provided
bool isAuthError = ex.Message.Contains("401") || ex.Message.Contains("Unauthorized");
bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) ||
!string.IsNullOrEmpty(_downloadClientConfig.Password);
if (isAuthError && !hasCredentials)
{
// Authentication error without credentials means service is running
return new HealthCheckResult
{
IsHealthy = true,
ResponseTime = stopwatch.Elapsed
};
}
_logger.LogWarning(ex, "Health check failed for Transmission client {clientId}", _downloadClientConfig.Id);
return new HealthCheckResult
{
IsHealthy = false,
ErrorMessage = $"Connection failed: {ex.Message}",
ResponseTime = stopwatch.Elapsed
};
}
}
public override void Dispose()
{
_client = null;
_httpClient?.Dispose();
}
private async Task<TorrentInfo?> GetTorrentAsync(string hash)