// Copyright (C) 2026, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Data; using System.Threading; using COSXML.Log; #nullable enable namespace Duplicati.Library.Main.Database; /// /// Slow query monitor helper /// internal static class SlowQueryMonitor { /// /// The log tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(SlowQueryMonitor)); /// /// The current instance of the slow query monitor, based on call tree /// private static readonly AsyncLocal currentInstance = new(); /// /// Starts monitoring slow queries /// /// The threshold for a query to be considered slow /// A disposable that stops monitoring when disposed public static IDisposable? StartMonitoring(TimeSpan threshold) { if (threshold.TotalSeconds < 1) { Logging.Log.WriteInformationMessage(LOGTAG, "TooLowQueryThreshold", "Slow query threshold is lower than 1s, treating as a disable request and disabling slow query monitoring"); return null; } return new Disposer(currentInstance.Value = new SlowQueryMonitorInstance(threshold)); } /// /// Starts monitoring a query /// /// The command to monitor /// The operation being performed /// A disposable that stops monitoring the query when disposed public static IDisposable? StartQuery(IDbCommand command, string operation) => currentInstance.Value?.StartQuery(command, operation); /// /// Dispose helper to simplify stopping the monitoring /// private struct Disposer : IDisposable { /// /// The instance to dispose /// private readonly SlowQueryMonitorInstance _instance; /// /// Creates a new instance of the disposer /// /// The instance to dispose public Disposer(SlowQueryMonitorInstance instance) => _instance = instance; /// /// Stops monitoring slow queries /// public void Dispose() { _instance.Dispose(); currentInstance.Value = null!; } } /// /// Helper class to monitor slow queries by tracking active queries /// and logging a message if any runs for a long time. /// private sealed class SlowQueryMonitorInstance { /// /// Lock object to ensure thread safety. /// private readonly object _lock = new object(); /// /// The threshold for a query to be considered slow. /// private readonly TimeSpan _slowThreshold; /// /// The next ID to assign to a query. /// private long _nextId; /// /// The dictionary of active queries. /// private Dictionary? _activeQueries; /// /// The timer used to check for slow queries. /// private Timer? _timer; /// /// Represents an active query being tracked. /// /// The query id /// The command being executed /// The operation name /// The time the query started private sealed record ActiveQuery( long Id, IDbCommand Command, string Operation, DateTime StartTime ); /// /// Starts the slow query monitoring timer. /// public SlowQueryMonitorInstance(TimeSpan slowThreshold) { _nextId = 0; _activeQueries = new(); _slowThreshold = slowThreshold; _timer = new Timer(CheckSlowQueries, null, slowThreshold, slowThreshold); } /// /// Starts tracking a query and returns a disposable scope that ends tracking when disposed. /// /// The command being executed. /// The operation name. /// A disposable scope that ends tracking on dispose. public IDisposable StartQuery(IDbCommand command, string operation) { var id = Interlocked.Increment(ref _nextId); lock (_lock) _activeQueries?[id] = new ActiveQuery(id, command, operation, DateTime.UtcNow); return new QueryScope(this, id); } /// /// Ends tracking of a query. Called from the Scope.Dispose method. /// /// The query to end tracking for. private void EndQuery(long id) { lock (_lock) _activeQueries?.Remove(id); } /// /// Checks for slow queries and logs them. Called by the timer. /// /// Unused state object private void CheckSlowQueries(object? state) { try { // Get a snapshot of the active queries so we can check them without holding the lock List snapshot; lock (_lock) { if (_activeQueries == null || _activeQueries.Count == 0) return; snapshot = [.. _activeQueries.Values]; } var now = DateTime.UtcNow; foreach (var query in snapshot) { var elapsed = now - query.StartTime; if (elapsed < _slowThreshold) continue; // Try to get the command text, but the object could be disposed by now string? commandText; try { commandText = query.Command.GetPrintableCommandText(); } catch { commandText = "[command disposed or unavailable]"; } Logging.Log.WriteWarningMessage(LOGTAG, "SlowQueryDetected", null, "Query still running after {0:F1}s in {1}: {2}", elapsed.TotalSeconds, query.Operation, commandText); } } catch (Exception ex) { // Just in case the logging system is broken try { Logging.Log.WriteWarningMessage(LOGTAG, "SlowQueryMonitorError", ex, "Error checking for slow queries"); } catch { } } } /// /// Stops the slow query monitoring timer. /// public void Dispose() { lock (_lock) { _timer?.Dispose(); _timer = null; _activeQueries = null; } } /// /// A disposable scope that ends tracking of a query when disposed. /// private readonly struct QueryScope : IDisposable { /// /// The parent instance that owns the query. /// private readonly SlowQueryMonitorInstance _parent; /// /// The ID of the query to end tracking for. /// private readonly long _id; /// /// Creates a new scope that ends tracking of a query when disposed. /// /// The query to end tracking for. public QueryScope(SlowQueryMonitorInstance parent, long id) { _parent = parent; _id = id; } /// /// Ends tracking of the query. /// public void Dispose() => _parent.EndQuery(_id); } } }