Files
Mimante/Mimante/Services/HtmlCacheService.cs
Alberto Balbo 690f7e636a Ottimizzazione RAM, UI e sistema di timing aste
- Ridotto consumo RAM: limiti log, pulizia e compattazione dati aste, timer periodico di cleanup
- UI più fluida: cache locale aste, throttling aggiornamenti, refresh log solo se necessario
- Nuovo sistema Ticker Loop: timing configurabile, strategie solo vicino alla scadenza, feedback puntate tardive
- Migliorato layout e splitter, log visivo, gestione cache HTML
- Aggiornata UI impostazioni e fix vari per performance e thread-safety
2026-02-07 19:28:30 +01:00

327 lines
11 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace AutoBidder.Services
{
/// <summary>
/// Servizio centralizzato per gestione richieste HTTP con cache, rate limiting e queue
/// </summary>
public class HtmlCacheService
{
private static readonly HttpClient _httpClient = new HttpClient();
// Cache HTML con timestamp
private readonly ConcurrentDictionary<string, CachedHtml> _cache = new();
// Coda richieste con priorità
private readonly SemaphoreSlim _rateLimiter;
private readonly TimeSpan _minRequestDelay;
private DateTime _lastRequestTime = DateTime.MinValue;
private readonly object _requestLock = new object();
// Configurazione
private readonly int _maxConcurrentRequests;
private readonly TimeSpan _cacheExpiration;
private readonly int _maxRetries;
private readonly int _maxCacheEntries;
// Logging callback
public Action<string>? OnLog { get; set; }
public HtmlCacheService(
int maxConcurrentRequests = 3,
int requestsPerSecond = 5,
TimeSpan? cacheExpiration = null,
int maxRetries = 2,
int maxCacheEntries = 50)
{
_maxConcurrentRequests = maxConcurrentRequests;
_minRequestDelay = TimeSpan.FromMilliseconds(1000.0 / requestsPerSecond);
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(3); // Ridotto da 5 a 3 minuti
_maxRetries = maxRetries;
_maxCacheEntries = maxCacheEntries;
_rateLimiter = new SemaphoreSlim(maxConcurrentRequests, maxConcurrentRequests);
_httpClient.Timeout = TimeSpan.FromSeconds(15);
}
/// <summary>
/// Recupera HTML con cache automatica e rate limiting
/// </summary>
public async Task<HtmlResponse> GetHtmlAsync(
string url,
RequestPriority priority = RequestPriority.Normal,
bool bypassCache = false)
{
try
{
// 1. Controlla cache se non bypassata
if (!bypassCache && TryGetFromCache(url, out var cachedHtml))
{
OnLog?.Invoke($"[HTML CACHE] Hit per: {GetShortUrl(url)}");
return new HtmlResponse
{
Success = true,
Html = cachedHtml,
FromCache = true,
Url = url
};
}
// 2. Rate limiting - aspetta il tuo turno
await _rateLimiter.WaitAsync();
try
{
// 3. Applica min delay tra richieste
await ApplyRateLimitAsync();
// 4. Esegui richiesta HTTP con retry
var html = await ExecuteWithRetryAsync(url);
// 5. Salva in cache
SaveToCache(url, html);
OnLog?.Invoke($"[HTML FETCH] Success: {GetShortUrl(url)} ({html.Length} chars)");
return new HtmlResponse
{
Success = true,
Html = html,
FromCache = false,
Url = url
};
}
finally
{
_rateLimiter.Release();
}
}
catch (Exception ex)
{
OnLog?.Invoke($"[HTML ERROR] {GetShortUrl(url)}: {ex.Message}");
return new HtmlResponse
{
Success = false,
Error = ex.Message,
Url = url
};
}
}
/// <summary>
/// Richiesta HTTP con retry automatico
/// </summary>
private async Task<string> ExecuteWithRetryAsync(string url)
{
Exception? lastException = null;
for (int attempt = 1; attempt <= _maxRetries; attempt++)
{
try
{
var html = await _httpClient.GetStringAsync(url);
if (attempt > 1)
{
OnLog?.Invoke($"[HTML RETRY] Success al tentativo {attempt}: {GetShortUrl(url)}");
}
return html;
}
catch (TaskCanceledException) when (attempt < _maxRetries)
{
lastException = new TaskCanceledException($"Timeout (tentativo {attempt}/{_maxRetries})");
OnLog?.Invoke($"[HTML RETRY] Timeout tentativo {attempt}/{_maxRetries}: {GetShortUrl(url)}");
await Task.Delay(1000 * attempt); // Exponential backoff
}
catch (HttpRequestException ex) when (attempt < _maxRetries)
{
lastException = ex;
OnLog?.Invoke($"[HTML RETRY] Errore tentativo {attempt}/{_maxRetries}: {ex.Message}");
await Task.Delay(1000 * attempt);
}
}
throw lastException ?? new Exception("Tutti i tentativi falliti");
}
/// <summary>
/// Applica rate limiting tra richieste
/// </summary>
private async Task ApplyRateLimitAsync()
{
lock (_requestLock)
{
var timeSinceLastRequest = DateTime.UtcNow - _lastRequestTime;
if (timeSinceLastRequest < _minRequestDelay)
{
var delay = _minRequestDelay - timeSinceLastRequest;
OnLog?.Invoke($"[RATE LIMIT] Delay di {delay.TotalMilliseconds:F0}ms");
Thread.Sleep(delay); // Sync sleep dentro lock per garantire ordinamento
}
_lastRequestTime = DateTime.UtcNow;
}
}
/// <summary>
/// Controlla se HTML è in cache e ancora valido
/// </summary>
private bool TryGetFromCache(string url, out string html)
{
if (_cache.TryGetValue(url, out var cached))
{
if (DateTime.UtcNow - cached.Timestamp < _cacheExpiration)
{
html = cached.Html;
return true;
}
else
{
// Expired - rimuovi
_cache.TryRemove(url, out _);
OnLog?.Invoke($"[HTML CACHE] Expired: {GetShortUrl(url)}");
}
}
html = string.Empty;
return false;
}
/// <summary>
/// Salva HTML in cache con limite dimensione
/// </summary>
private void SaveToCache(string url, string html)
{
// Limita dimensione cache per evitare memory leak
if (_cache.Count >= _maxCacheEntries)
{
// Rimuovi le entry più vecchie
var oldestEntries = _cache
.OrderBy(kvp => kvp.Value.Timestamp)
.Take(_cache.Count - _maxCacheEntries + 10) // Rimuovi 10 extra per evitare chiamate frequenti
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in oldestEntries)
{
_cache.TryRemove(key, out _);
}
}
_cache[url] = new CachedHtml
{
Html = html,
Timestamp = DateTime.UtcNow
};
}
/// <summary>
/// Pulisce cache scaduta
/// </summary>
public void CleanExpiredCache()
{
var expired = _cache
.Where(kvp => DateTime.UtcNow - kvp.Value.Timestamp >= _cacheExpiration)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expired)
{
_cache.TryRemove(key, out _);
}
if (expired.Count > 0)
{
OnLog?.Invoke($"[HTML CACHE] Pulite {expired.Count} entry scadute");
}
}
/// <summary>
/// Pulisce tutta la cache
/// </summary>
public void ClearCache()
{
var count = _cache.Count;
_cache.Clear();
OnLog?.Invoke($"[HTML CACHE] Cache pulita ({count} entries)");
}
/// <summary>
/// Statistiche cache
/// </summary>
public CacheStats GetStats()
{
return new CacheStats
{
TotalEntries = _cache.Count,
AvailableSlots = _rateLimiter.CurrentCount,
MaxConcurrent = _maxConcurrentRequests
};
}
private string GetShortUrl(string url)
{
try
{
var uri = new Uri(url);
var path = uri.AbsolutePath;
if (path.Length > 40)
path = "..." + path.Substring(path.Length - 37);
return path;
}
catch
{
return url.Length > 40 ? url.Substring(0, 37) + "..." : url;
}
}
/// <summary>
/// Classe per cache con timestamp
/// </summary>
private class CachedHtml
{
public string Html { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
}
/// <summary>
/// Risposta richiesta HTML
/// </summary>
public class HtmlResponse
{
public bool Success { get; set; }
public string Html { get; set; } = string.Empty;
public string Error { get; set; } = string.Empty;
public bool FromCache { get; set; }
public string Url { get; set; } = string.Empty;
}
/// <summary>
/// Priorità richiesta (per future implementazioni)
/// </summary>
public enum RequestPriority
{
Low = 0,
Normal = 1,
High = 2,
Critical = 3
}
/// <summary>
/// Statistiche cache
/// </summary>
public class CacheStats
{
public int TotalEntries { get; set; }
public int AvailableSlots { get; set; }
public int MaxConcurrent { get; set; }
}
}