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.
Connected.Framework/Connected.Caching/Cache.cs

433 lines
9.6 KiB

2 years ago
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);
}
}