using System.Collections.Concurrent; using Microsoft.Extensions.Logging; namespace Managing.Infrastructure.Databases.PostgreSql; /// /// Service for detecting potential SQL query loops and performance issues /// Monitors query patterns and execution frequency to identify problematic operations /// public class SqlLoopDetectionService { private readonly ILogger _logger; private readonly ConcurrentDictionary _queryTrackers; private readonly Timer _cleanupTimer; private readonly TimeSpan _trackingWindow = TimeSpan.FromMinutes(5); private readonly int _maxExecutionsPerWindow = 10; private readonly TimeSpan _cleanupInterval = TimeSpan.FromMinutes(1); public SqlLoopDetectionService(ILogger logger) { _logger = logger; _queryTrackers = new ConcurrentDictionary(); // Setup cleanup timer to remove old tracking data _cleanupTimer = new Timer(CleanupOldTrackers, null, _cleanupInterval, _cleanupInterval); } /// /// Tracks a query execution and detects potential loops /// /// Name of the repository executing the query /// Name of the method executing the query /// Pattern or hash of the query being executed /// Time taken to execute the query /// True if a potential loop is detected public bool TrackQueryExecution(string repositoryName, string methodName, string queryPattern, TimeSpan executionTime) { var key = $"{repositoryName}.{methodName}.{queryPattern}"; var now = DateTime.UtcNow; var tracker = _queryTrackers.AddOrUpdate(key, new QueryExecutionTracker { RepositoryName = repositoryName, MethodName = methodName, QueryPattern = queryPattern, FirstExecution = now, LastExecution = now, ExecutionCount = 1, TotalExecutionTime = executionTime, MaxExecutionTime = executionTime, MinExecutionTime = executionTime }, (k, existing) => { existing.LastExecution = now; existing.ExecutionCount++; existing.TotalExecutionTime += executionTime; existing.MaxExecutionTime = existing.MaxExecutionTime > executionTime ? existing.MaxExecutionTime : executionTime; existing.MinExecutionTime = existing.MinExecutionTime < executionTime ? existing.MinExecutionTime : executionTime; return existing; }); // Check for potential loop conditions var timeSinceFirst = now - tracker.FirstExecution; var executionsPerMinute = tracker.ExecutionCount / Math.Max(timeSinceFirst.TotalMinutes, 0.1); var isLoopDetected = false; var reasons = new List(); // Check execution frequency (increased threshold to reduce false positives) if (executionsPerMinute > 100) { isLoopDetected = true; reasons.Add($"High frequency: {executionsPerMinute:F1} executions/minute"); } // Check total execution count in window if (tracker.ExecutionCount > _maxExecutionsPerWindow) { isLoopDetected = true; reasons.Add($"High count: {tracker.ExecutionCount} executions in {timeSinceFirst.TotalMinutes:F1} minutes"); } // Check for rapid successive executions (increased threshold to reduce false positives) if (tracker.ExecutionCount > 20 && timeSinceFirst.TotalSeconds < 10) { isLoopDetected = true; reasons.Add( $"Rapid execution: {tracker.ExecutionCount} executions in {timeSinceFirst.TotalSeconds:F1} seconds"); } // Check for consistently slow queries (increased threshold to reduce false positives) if (tracker.ExecutionCount > 10 && tracker.AverageExecutionTime.TotalMilliseconds > 1000) { isLoopDetected = true; reasons.Add($"Consistently slow: {tracker.AverageExecutionTime.TotalMilliseconds:F0}ms average"); } if (isLoopDetected) { _logger.LogWarning( "[SQL-LOOP-DETECTED] {Repository}.{Method} | Pattern: {Pattern} | Count: {Count} | Reasons: {Reasons} | Avg Time: {AvgTime}ms", repositoryName, methodName, queryPattern, tracker.ExecutionCount, string.Join(", ", reasons), tracker.AverageExecutionTime.TotalMilliseconds); // Log detailed execution history _logger.LogWarning( "[SQL-LOOP-DETAILS] {Repository}.{Method} | First: {First} | Last: {Last} | Min: {Min}ms | Max: {Max}ms | Total: {Total}ms", repositoryName, methodName, tracker.FirstExecution.ToString("HH:mm:ss.fff"), tracker.LastExecution.ToString("HH:mm:ss.fff"), tracker.MinExecutionTime.TotalMilliseconds, tracker.MaxExecutionTime.TotalMilliseconds, tracker.TotalExecutionTime.TotalMilliseconds); } return isLoopDetected; } /// /// Gets current statistics for all tracked queries /// public Dictionary GetQueryStatistics() { var stats = new Dictionary(); var now = DateTime.UtcNow; foreach (var kvp in _queryTrackers) { var tracker = kvp.Value; var timeSinceFirst = now - tracker.FirstExecution; stats[kvp.Key] = new QueryExecutionStats { RepositoryName = tracker.RepositoryName, MethodName = tracker.MethodName, QueryPattern = tracker.QueryPattern, ExecutionCount = tracker.ExecutionCount, FirstExecution = tracker.FirstExecution, LastExecution = tracker.LastExecution, AverageExecutionTime = tracker.AverageExecutionTime, MinExecutionTime = tracker.MinExecutionTime, MaxExecutionTime = tracker.MaxExecutionTime, ExecutionsPerMinute = tracker.ExecutionCount / Math.Max(timeSinceFirst.TotalMinutes, 0.1), IsActive = timeSinceFirst < _trackingWindow }; } return stats; } /// /// Clears all tracking data /// public void ClearAllTracking() { _queryTrackers.Clear(); _logger.LogInformation("[SQL-LOOP-DETECTION] All tracking data cleared"); } private void CleanupOldTrackers(object? state) { var now = DateTime.UtcNow; var keysToRemove = new List(); foreach (var kvp in _queryTrackers) { var timeSinceLastExecution = now - kvp.Value.LastExecution; if (timeSinceLastExecution > _trackingWindow) { keysToRemove.Add(kvp.Key); } } foreach (var key in keysToRemove) { _queryTrackers.TryRemove(key, out _); } if (keysToRemove.Count > 0) { _logger.LogDebug("[SQL-LOOP-DETECTION] Cleaned up {Count} old trackers", keysToRemove.Count); } } public void Dispose() { _cleanupTimer?.Dispose(); } private class QueryExecutionTracker { public string RepositoryName { get; set; } = string.Empty; public string MethodName { get; set; } = string.Empty; public string QueryPattern { get; set; } = string.Empty; public DateTime FirstExecution { get; set; } public DateTime LastExecution { get; set; } public int ExecutionCount { get; set; } public TimeSpan TotalExecutionTime { get; set; } public TimeSpan MaxExecutionTime { get; set; } public TimeSpan MinExecutionTime { get; set; } public TimeSpan AverageExecutionTime => ExecutionCount > 0 ? TimeSpan.FromTicks(TotalExecutionTime.Ticks / ExecutionCount) : TimeSpan.Zero; } } /// /// Statistics for query execution tracking /// public class QueryExecutionStats { public string RepositoryName { get; set; } = string.Empty; public string MethodName { get; set; } = string.Empty; public string QueryPattern { get; set; } = string.Empty; public int ExecutionCount { get; set; } public DateTime FirstExecution { get; set; } public DateTime LastExecution { get; set; } public TimeSpan AverageExecutionTime { get; set; } public TimeSpan MinExecutionTime { get; set; } public TimeSpan MaxExecutionTime { get; set; } public double ExecutionsPerMinute { get; set; } public bool IsActive { get; set; } }