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;
}
}
}