using System.Collections.Concurrent; using System.Globalization; using System.Text; namespace Sonex.Worker.WebSync; internal sealed class WebSyncRunReport { private const int MaxDictionaryValuesPerColumn = 1000; private readonly ConcurrentDictionary _errorTypes = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentQueue _exceptions = new(); private readonly ConcurrentDictionary _dictionaryOverflowColumns = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _dictionaryObservedColumns = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary> _dictionaryValues = new(StringComparer.OrdinalIgnoreCase); private long _sitemapLinksCount; private long _pagesDownloaded; private long _pagesUpdated; private long _imagesFound; private long _imagesDownloaded; private long _imagesUpdated; private long _errorsCount; public void SetSitemapLinksCount(int count) { Interlocked.Exchange(ref _sitemapLinksCount, Math.Max(0, count)); } public void IncrementPageDownloaded() { Interlocked.Increment(ref _pagesDownloaded); } public void IncrementPageUpdated() { Interlocked.Increment(ref _pagesUpdated); } public void IncrementImageDownloaded() { Interlocked.Increment(ref _imagesDownloaded); } public void IncrementImageFound(int count = 1) { if (count <= 0) return; Interlocked.Add(ref _imagesFound, count); } public void IncrementImageUpdated() { Interlocked.Increment(ref _imagesUpdated); } public void RegisterDictionaryValue(string tableName, string columnName, string? value) { if (string.IsNullOrWhiteSpace(tableName) || string.IsNullOrWhiteSpace(columnName)) return; string normalizedTableName = tableName.Trim().ToLowerInvariant(); string normalizedColumnName = columnName.Trim().ToLowerInvariant(); string dictionaryKey = BuildDictionaryKey(normalizedTableName, normalizedColumnName); _dictionaryObservedColumns.TryAdd( dictionaryKey, new DictionaryColumnKey { TableName = normalizedTableName, ColumnName = normalizedColumnName }); if (_dictionaryOverflowColumns.ContainsKey(dictionaryKey)) return; string normalizedValue = NormalizeValue(value); if (normalizedValue.Length == 0) return; ConcurrentDictionary values = _dictionaryValues.GetOrAdd( dictionaryKey, _ => new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase)); values.TryAdd(normalizedValue, 0); if (values.Count > MaxDictionaryValuesPerColumn) { _dictionaryOverflowColumns.TryAdd(dictionaryKey, 0); _dictionaryValues.TryRemove(dictionaryKey, out _); } } public void RegisterException(string operation, Exception exception, string? contextMessage = null) { string normalizedOperation = string.IsNullOrWhiteSpace(operation) ? "UnknownOperation" : operation.Trim(); string exceptionType = exception.GetType().Name; string key = $"{normalizedOperation}|{exceptionType}"; _errorTypes.AddOrUpdate(key, 1, (_, current) => current + 1); Interlocked.Increment(ref _errorsCount); _exceptions.Enqueue(new RunExceptionInfo { Operation = normalizedOperation, ExceptionType = exceptionType, ContextMessage = string.IsNullOrWhiteSpace(contextMessage) ? exception.Message : contextMessage.Trim(), ExceptionText = exception.ToString() }); } public IReadOnlyList BuildDictionaryValueSnapshot() { var snapshot = new List(_dictionaryObservedColumns.Count); foreach (var pair in _dictionaryObservedColumns .OrderBy(static item => item.Value.TableName, StringComparer.OrdinalIgnoreCase) .ThenBy(static item => item.Value.ColumnName, StringComparer.OrdinalIgnoreCase)) { IReadOnlyCollection values; if (_dictionaryOverflowColumns.ContainsKey(pair.Key)) { values = Array.Empty(); } else if (!_dictionaryValues.TryGetValue(pair.Key, out ConcurrentDictionary? rawValues)) { values = Array.Empty(); } else { values = rawValues.Keys .OrderBy(static item => item, StringComparer.OrdinalIgnoreCase) .ToArray(); } snapshot.Add(new DictionaryColumnValues { TableName = pair.Value.TableName, ColumnName = pair.Value.ColumnName, Values = values }); } return snapshot; } public string BuildSummaryReport(TimeSpan duration, int processedTasks) { double tasksPerMinute = duration.TotalMinutes <= 0 ? 0 : processedTasks / duration.TotalMinutes; return string.Format( CultureInfo.GetCultureInfo("pl-PL"), "Worker finished. Duration: {0:hh\\:mm\\:ss}. Speed: {1:F2} tasks/min.", duration, tasksPerMinute); } public int GetProcessedTasksForSpeed() { long value = Interlocked.Read(ref _pagesDownloaded); return value >= int.MaxValue ? int.MaxValue : (int)Math.Max(0, value); } public string BuildFullReport(TimeSpan duration, int processedTasks) { var sb = new StringBuilder(); sb.AppendLine(BuildSummaryReport(duration, processedTasks)); sb.AppendLine($"Sitemap links: {_sitemapLinksCount}"); sb.AppendLine($"Pages downloaded: {_pagesDownloaded}"); sb.AppendLine($"Pages updated: {_pagesUpdated}"); sb.AppendLine($"Images found: {_imagesFound}"); sb.AppendLine($"Images downloaded: {_imagesDownloaded}"); sb.AppendLine($"Images updated: {_imagesUpdated}"); sb.AppendLine($"Errors: {_errorsCount}"); if (_errorTypes.Count > 0) { sb.AppendLine("Error types:"); foreach (var pair in _errorTypes.OrderByDescending(static item => item.Value).ThenBy(static item => item.Key, StringComparer.OrdinalIgnoreCase)) { sb.AppendLine($"- {pair.Key}: {pair.Value}"); } } if (!_exceptions.IsEmpty) { sb.AppendLine("Exceptions:"); int index = 0; while (_exceptions.TryDequeue(out RunExceptionInfo? item)) { index++; sb.AppendLine($"[{index}] Operation={item.Operation}; Type={item.ExceptionType}; Message={item.ContextMessage}"); sb.AppendLine(item.ExceptionText); } } return sb.ToString().TrimEnd(); } private static string NormalizeValue(string? value) { return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); } private static string BuildDictionaryKey(string tableName, string columnName) { return $"{tableName}|{columnName}"; } private sealed class DictionaryColumnKey { public string TableName { get; init; } = string.Empty; public string ColumnName { get; init; } = string.Empty; } private sealed class RunExceptionInfo { public string Operation { get; init; } = string.Empty; public string ExceptionType { get; init; } = string.Empty; public string ContextMessage { get; init; } = string.Empty; public string ExceptionText { get; init; } = string.Empty; } public sealed class DictionaryColumnValues { public string TableName { get; init; } = string.Empty; public string ColumnName { get; init; } = string.Empty; public IReadOnlyCollection Values { get; init; } = Array.Empty(); } }