using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using EmbedIO.Internal; using Swan.Threading; using Swan.Logging; namespace EmbedIO.Files { #pragma warning disable CA1001 // Type owns disposable field '_cleaner' but is not disposable - _cleaner has its own dispose semantics. /// /// A cache where one or more instances of can store hashes and file contents. /// public sealed partial class FileCache #pragma warning restore CA1001 { /// /// The default value for the property. /// public const int DefaultMaxSizeKb = 10240; /// /// The default value for the property. /// public const int DefaultMaxFileSizeKb = 200; private static readonly Stopwatch TimeBase = Stopwatch.StartNew(); private static readonly object DefaultSyncRoot = new object(); private static FileCache? _defaultInstance; private readonly ConcurrentDictionary _sections = new ConcurrentDictionary(StringComparer.Ordinal); private int _sectionCount; // Because ConcurrentDictionary<,>.Count is locking. private int _maxSizeKb = DefaultMaxSizeKb; private int _maxFileSizeKb = DefaultMaxFileSizeKb; private PeriodicTask? _cleaner; /// /// Gets the default instance used by . /// public static FileCache Default { get { if (_defaultInstance != null) return _defaultInstance; lock (DefaultSyncRoot) { if (_defaultInstance == null) _defaultInstance = new FileCache(); } return _defaultInstance; } } /// /// Gets or sets the maximum total size of cached data in kilobytes (1 kilobyte = 1024 bytes). /// The default value for this property is stored in the constant field. /// Setting this property to a value less lower han 1 has the same effect as setting it to 1. /// public int MaxSizeKb { get => _maxSizeKb; set => _maxSizeKb = Math.Max(value, 1); } /// /// Gets or sets the maximum size of a single cached file in kilobytes (1 kilobyte = 1024 bytes). /// A single file's contents may be present in a cache more than once, if the file /// is requested with different Accept-Encoding request headers. This property acts as a threshold /// for the uncompressed size of a file. /// The default value for this property is stored in the constant field. /// Setting this property to a value lower than 0 has the same effect as setting it to 0, in fact /// completely disabling the caching of file contents for this cache. /// This property cannot be set to a value higher than 2097151; in other words, it is not possible /// to cache files bigger than two Gigabytes (1 Gigabyte = 1048576 kilobytes) minus 1 kilobyte. /// public int MaxFileSizeKb { get => _maxFileSizeKb; set => _maxFileSizeKb = Math.Min(Math.Max(value, 0), 2097151); } // Cast as IDictionary because we WANT an exception to be thrown if the name exists. // It would mean that something is very, very wrong. internal Section AddSection(string name) { var section = new Section(); (_sections as IDictionary).Add(name, section); if (Interlocked.Increment(ref _sectionCount) == 1) _cleaner = new PeriodicTask(TimeSpan.FromMinutes(1), CheckMaxSize); return section; } internal void RemoveSection(string name) { _sections.TryRemove(name, out _); if (Interlocked.Decrement(ref _sectionCount) == 0) { _cleaner?.Dispose(); _cleaner = null; } } private async Task CheckMaxSize(CancellationToken cancellationToken) { var timeKeeper = new TimeKeeper(); var maxSizeKb = _maxSizeKb; var initialSizeKb = ComputeTotalSize() / 1024L; if (initialSizeKb <= maxSizeKb) { $"Total size = {initialSizeKb}/{_maxSizeKb}kb, not purging.".Debug(nameof(FileCache)); return; } $"Total size = {initialSizeKb}/{_maxSizeKb}kb, purging...".Debug(nameof(FileCache)); var removedCount = 0; var removedSize = 0L; var totalSizeKb = initialSizeKb; var threshold = 973L * maxSizeKb / 1024L; // About 95% of maximum allowed size while (totalSizeKb > threshold) { if (cancellationToken.IsCancellationRequested) return; var section = GetSectionWithLeastRecentItem(); if (section == null) return; removedSize += section.RemoveLeastRecentItem(); removedCount++; await Task.Yield(); totalSizeKb = ComputeTotalSize() / 1024L; } $"Purge completed in {timeKeeper.ElapsedTime}ms: removed {removedCount} items ({removedSize / 1024L}kb). Total size is now {totalSizeKb}kb." .Debug(nameof(FileCache)); } // Enumerate key / value pairs because the Keys and Values property // of ConcurrentDictionary<,> have snapshot semantics, // while GetEnumerator enumerates without locking. private long ComputeTotalSize() => _sections.Sum(pair => pair.Value.GetTotalSize()); private Section? GetSectionWithLeastRecentItem() { Section? result = null; var earliestTime = long.MaxValue; foreach (var pair in _sections) { var section = pair.Value; var time = section.GetLeastRecentUseTime(); if (time < earliestTime) { result = section; earliestTime = time; } } return result; } } }