You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
9.6 KiB
433 lines
9.6 KiB
using System.Collections.Concurrent;
|
|
using System.Collections.Immutable;
|
|
using Connected.Interop;
|
|
|
|
namespace Connected.Caching;
|
|
|
|
internal abstract class Cache : ICache
|
|
{
|
|
private bool _disposedValue;
|
|
private readonly ConcurrentDictionary<string, Entries> _items;
|
|
private readonly Task _scavenger;
|
|
private readonly CancellationTokenSource _cancel = new();
|
|
|
|
public event CacheInvalidateHandler? Invalidating;
|
|
public event CacheInvalidateHandler? Invalidated;
|
|
|
|
public Cache()
|
|
{
|
|
_scavenger = new Task(OnScaveging, _cancel.Token, TaskCreationOptions.LongRunning);
|
|
_items = new ConcurrentDictionary<string, Entries>();
|
|
|
|
_scavenger.Start();
|
|
}
|
|
private ConcurrentDictionary<string, Entries> Items => _items;
|
|
private CancellationTokenSource Cancel => _cancel;
|
|
|
|
private void OnScaveging()
|
|
{
|
|
var token = Cancel.Token;
|
|
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
foreach (var i in Items)
|
|
i.Value.Scave();
|
|
|
|
var empties = Items.Where(f => f.Value.Count == 0).Select(f => f.Key);
|
|
|
|
foreach (var i in empties)
|
|
Items.TryRemove(i, out _);
|
|
|
|
token.WaitHandle.WaitOne(TimeSpan.FromMinutes(1));
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
public virtual bool IsEmpty(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.Any();
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool Exists(string key)
|
|
{
|
|
return Items.ContainsKey(key);
|
|
}
|
|
|
|
public void CreateKey(string key)
|
|
{
|
|
if (Exists(key))
|
|
return;
|
|
|
|
Items.TryAdd(key, new Entries());
|
|
}
|
|
|
|
public IEnumerator<T>? GetEnumerator<T>(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.GetEnumerator<T>();
|
|
|
|
return new List<T>().GetEnumerator();
|
|
}
|
|
public virtual ImmutableList<T>? All<T>(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.All<T>();
|
|
|
|
return default;
|
|
}
|
|
|
|
public int Count(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.Count;
|
|
|
|
return 0;
|
|
}
|
|
|
|
public virtual T? Get<T>(string key, Func<T, bool> predicate)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry)
|
|
return GetValue<T>(entry);
|
|
|
|
return default;
|
|
}
|
|
public virtual async Task<T?> Get<T>(string key, Func<T, bool> predicate, Func<EntryOptions, Task<T>> retrieve)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry)
|
|
return GetValue<T>(entry);
|
|
|
|
if (retrieve is null)
|
|
return default;
|
|
|
|
var options = new EntryOptions();
|
|
T instance = await retrieve(options);
|
|
|
|
if (EqualityComparer<T>.Default.Equals(instance, default))
|
|
{
|
|
if (!options.AllowNull)
|
|
return default;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(options.Key))
|
|
throw new SysException(this, SR.ErrCacheKeyNull);
|
|
|
|
Set(key, options.Key, instance, options.Duration, options.SlidingExpiration);
|
|
|
|
return instance;
|
|
}
|
|
|
|
public virtual async Task<T?> Get<T>(string key, object id, Func<EntryOptions, Task<T>>? retrieve)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value) && value.Get(id is null ? null : id.ToString()) is IEntry entry)
|
|
return GetValue<T>(entry);
|
|
|
|
if (retrieve is null)
|
|
return default;
|
|
|
|
var options = new EntryOptions
|
|
{
|
|
Key = id is null ? null : id.ToString()
|
|
};
|
|
|
|
T instance = await retrieve(options);
|
|
|
|
if (EqualityComparer<T>.Default.Equals(instance, default))
|
|
{
|
|
if (!options.AllowNull)
|
|
return default;
|
|
}
|
|
|
|
Set(key, options.Key, instance, options.Duration, options.SlidingExpiration);
|
|
|
|
return instance;
|
|
}
|
|
|
|
internal void ClearCore(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
value.Clear();
|
|
}
|
|
|
|
public virtual async Task Clear(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
value.Clear();
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public virtual T? Get<T>(string key, object id)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value) && value.Get(id is null ? null : id.ToString()) is IEntry entry)
|
|
return GetValue<T>(entry);
|
|
|
|
return default;
|
|
}
|
|
|
|
public IEntry? Get(string key, object id)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.Get(id is null ? null : id.ToString());
|
|
|
|
return default;
|
|
}
|
|
|
|
public virtual T? Get<T>(string key, Func<dynamic, bool> predicate)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value) && value.Get(predicate) is IEntry entry)
|
|
return GetValue<T>(entry);
|
|
|
|
return default;
|
|
}
|
|
|
|
public virtual T? First<T>(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value) && value.First() is IEntry entry)
|
|
return GetValue<T>(entry);
|
|
|
|
return default;
|
|
}
|
|
|
|
public virtual ImmutableList<T>? Where<T>(string key, Func<T, bool> predicate)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.Where(predicate);
|
|
|
|
return default;
|
|
}
|
|
|
|
public void CopyTo(string key, object id, IEntry instance)
|
|
{
|
|
if (!Items.TryGetValue(key, out Entries? value))
|
|
{
|
|
value = new Entries();
|
|
|
|
if (!Items.TryAdd(key, value))
|
|
return;
|
|
}
|
|
|
|
value.Set(id is null ? null : id.ToString(), instance.Instance, instance.Duration, instance.SlidingExpiration);
|
|
}
|
|
|
|
public virtual T? Set<T>(string key, object id, T? instance)
|
|
{
|
|
return Set(key, id, instance, TimeSpan.Zero);
|
|
}
|
|
|
|
public virtual T? Set<T>(string key, object id, T? instance, TimeSpan duration)
|
|
{
|
|
return Set(key, id, instance, duration, false);
|
|
}
|
|
|
|
public virtual T? Set<T>(string key, object id, T? instance, TimeSpan duration, bool slidingExpiration)
|
|
{
|
|
if (!Items.TryGetValue(key, out Entries? value))
|
|
{
|
|
value = new Entries();
|
|
|
|
if (!Items.TryAdd(key, value))
|
|
return default;
|
|
}
|
|
|
|
value.Set(id is null ? null : id.ToString(), instance, duration, slidingExpiration);
|
|
|
|
return instance;
|
|
}
|
|
|
|
internal void RemoveCore(string key, object id)
|
|
{
|
|
if (id is null)
|
|
return;
|
|
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
value.Remove(id.ToString());
|
|
}
|
|
public virtual async Task Remove(string key, object id)
|
|
{
|
|
await Remove(key, id, true);
|
|
}
|
|
|
|
private async Task Remove(string key, object id, bool removing)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
value.Remove(id is null ? null : id.ToString());
|
|
|
|
if (removing)
|
|
await OnRemove(key, id);
|
|
}
|
|
|
|
protected virtual async Task OnRemove(string key, object id)
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public async Task Invalidate(string key, object id)
|
|
{
|
|
await InvalidateCore(key, id, false);
|
|
}
|
|
|
|
internal async Task InvalidateCore(string key, object id, bool fromNotification)
|
|
{
|
|
/*
|
|
* we store existing instance but it is not
|
|
* removed from the cache yet. This is because other
|
|
* threads can access this instance while we are
|
|
* retrieving a new version from the source
|
|
*/
|
|
var existing = Get<object>(key, id);
|
|
var args = new CacheEventArgs(id is null ? null : id.ToString(), key);
|
|
/*
|
|
* this two events invalidate that cache reference.
|
|
* note that if no new version exists the existing one
|
|
* is still available to other threads.
|
|
*/
|
|
try
|
|
{
|
|
Invalidating?.Invoke(args);
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
if (!fromNotification)
|
|
await OnInvalidating(args);
|
|
|
|
await OnInvalidate(args);
|
|
}
|
|
catch { }
|
|
/*
|
|
* now find out if a new version has been set for the
|
|
* specified key
|
|
*/
|
|
var newInstance = Get<object>(key, id);
|
|
/*
|
|
* if no existing reference exists there is no need for
|
|
* removing anything
|
|
*/
|
|
if (existing is not null)
|
|
{
|
|
/*
|
|
* we have an existing instance. we are dealing with two possible scenarios:
|
|
* - newInstance is null because entity has been deleted
|
|
* - newInstance is actually the same instance as the existing which means a new
|
|
* version does not exist. In both cases we must remove existing reference because
|
|
* at this point it is not valid anymore.
|
|
* note that the third case exists: reference has been replaced. in that case there
|
|
* is nothing to do because Invalidating events has already replaced reference with a
|
|
* new version.
|
|
*/
|
|
if (newInstance is null)
|
|
await Remove(key, id, false);
|
|
else if (existing.Equals(newInstance) && args.Behavior == InvalidateBehavior.RemoveSameInstance)
|
|
await Remove(key, id, false);
|
|
}
|
|
|
|
try
|
|
{
|
|
Invalidated?.Invoke(args);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
protected internal virtual async Task OnInvalidating(CacheEventArgs e)
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
protected virtual async Task OnInvalidate(CacheEventArgs e)
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
private void Clear()
|
|
{
|
|
foreach (var i in Items)
|
|
i.Value.Clear();
|
|
|
|
Items.Clear();
|
|
}
|
|
|
|
public virtual async Task<ImmutableList<string>?> Remove<T>(string key, Func<T, bool> predicate)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
{
|
|
var result = value.Remove(predicate);
|
|
|
|
if (result is not null && result.Any())
|
|
await OnRemove(key, result);
|
|
}
|
|
|
|
return default;
|
|
}
|
|
|
|
protected virtual async Task OnRemove(string key, ImmutableList<string> ids)
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
public ImmutableList<string>? Keys(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.Keys;
|
|
|
|
return default;
|
|
}
|
|
|
|
public ImmutableList<string> Keys()
|
|
{
|
|
return Items.Keys.ToImmutableList();
|
|
}
|
|
|
|
public bool Any(string key)
|
|
{
|
|
if (Items.TryGetValue(key, out Entries? value))
|
|
return value.Any();
|
|
|
|
return false;
|
|
}
|
|
|
|
private static T? GetValue<T>(IEntry entry)
|
|
{
|
|
if (entry is null || entry.Instance is null)
|
|
return default;
|
|
|
|
if (TypeConversion.TryConvert(entry.Instance, out T? result))
|
|
return result;
|
|
|
|
return default;
|
|
}
|
|
|
|
protected virtual void OnDisposing(bool disposing)
|
|
{
|
|
if (!_disposedValue)
|
|
{
|
|
if (disposing)
|
|
{
|
|
Cancel.Cancel();
|
|
Clear();
|
|
|
|
if (_scavenger is not null)
|
|
{
|
|
_cancel.Cancel();
|
|
|
|
if (_scavenger.IsCompleted)
|
|
_scavenger.Dispose();
|
|
}
|
|
}
|
|
|
|
_disposedValue = true;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
OnDisposing(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|