diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.VNCache')
12 files changed, 649 insertions, 103 deletions
diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheBuilder.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheBuilder.cs new file mode 100644 index 0000000..ea1566d --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheBuilder.cs @@ -0,0 +1,94 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: EntityCacheBuilder.cs +* +* EntityCacheBuilder.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading.Tasks; + +using VNLib.Utils.Logging; + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// Represents a cache that can store entities by their unique key + /// </summary> + /// <typeparam name="TEntity"></typeparam> + /// <param name="cache"></param> + public sealed class EntityCacheBuilder<TEntity>(IEntityCache<TEntity> cache) + where TEntity : class + { + ICacheExpirationPolicy<TEntity>? _expPolicy; + ICacheTaskPolicy? _taskPolicy; + + public EntityCacheBuilder<TEntity> WithExpirationPoicy(ICacheExpirationPolicy<TEntity> expiration) + { + _expPolicy = expiration; + return this; + } + + public EntityCacheBuilder<TEntity> WithInfiniteExpiration() + { + _expPolicy = new InfiniteCacheExpirationPolicy<TEntity>(); + return this; + } + + public EntityCacheBuilder<TEntity> WithTaskPolicy(ICacheTaskPolicy taskPolicy) + { + _taskPolicy = taskPolicy; + return this; + } + + public EntityCacheBuilder<TEntity> WithWriteBackPolicy(Action<Task> onFaulted) + { + _taskPolicy = new WriteBackCachePolicy(onFaulted); + return this; + } + + public EntityCacheBuilder<TEntity> WithWriteBackPolicy(ILogProvider logger, LogLevel logLevel = LogLevel.Warning) + { + _taskPolicy = new WriteBackCachePolicy((t) => + { + logger.Write(logLevel, t.Exception!, "Background task operation failed with exception: "); + }); + + return this; + } + + public EntityCacheBuilder<TEntity> WithWriteThoughPolicy() + { + _taskPolicy = WriteThroughCachePolicy.Instance; + return this; + } + + + public EntityResultCache<TEntity> Build() + { + ArgumentNullException.ThrowIfNull(_expPolicy, "Expiration"); + ArgumentNullException.ThrowIfNull(cache); + ArgumentNullException.ThrowIfNull(_taskPolicy, "TaskPolicy"); + + return new EntityResultCache<TEntity>(cache, _taskPolicy, _expPolicy); + } + } + +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheExtensions.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheExtensions.cs index 6b39580..a26da27 100644 --- a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityCacheExtensions.cs @@ -57,8 +57,9 @@ namespace VNLib.Plugins.Extensions.VNCache.DataModel /// <exception cref="ArgumentNullException"></exception> public static Task<bool> RemoveAsync<T>(this IEntityCache<T> cache, T entity, CancellationToken cancellation) where T: class, ICacheEntity { - _ = entity ?? throw new ArgumentNullException(nameof(entity)); - _ = cache ?? throw new ArgumentNullException(nameof(entity)); + ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(cache); + //Delete by its id return cache.RemoveAsync(entity.Id, cancellation); } @@ -94,9 +95,9 @@ namespace VNLib.Plugins.Extensions.VNCache.DataModel /// <exception cref="ArgumentNullException"></exception> public static IEntityCache<T> CreateEntityCache<T>(this IGlobalCacheProvider cache, ICacheObjectSerializer serialier, ICacheObjectDeserializer deserializer) where T: class { - _ = cache ?? throw new ArgumentNullException(nameof(cache)); - _ = serialier ?? throw new ArgumentNullException(nameof(serialier)); - _ = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); + ArgumentNullException.ThrowIfNull(cache); + ArgumentNullException.ThrowIfNull(serialier); + ArgumentNullException.ThrowIfNull(deserializer); return new EntityCacheImpl<T>(cache, deserializer, serialier); } @@ -112,96 +113,52 @@ namespace VNLib.Plugins.Extensions.VNCache.DataModel /// <exception cref="ArgumentNullException"></exception> public static IEntityCache<T> CreateJsonEntityCache<T>(this IGlobalCacheProvider cache, int bufferSize) where T: class { - _ = cache ?? throw new ArgumentNullException(nameof(cache)); + ArgumentNullException.ThrowIfNull(cache); + JsonCacheObjectSerializer json = new(bufferSize); return CreateEntityCache<T>(cache, json, json); } /// <summary> - /// Attemts to recover an entity from cache if possible, if a miss occurs, the - /// factory function is called to produce a value from a backing store. If the store - /// returns a result it is writen back to the cache before this method returns + /// Creates a new <see cref="EntityCacheBuilder{TEntity}"/> instance for building a + /// new <see cref="EntityResultCache{TEntity}"/> /// </summary> - /// <typeparam name="T"></typeparam> + /// <typeparam name="TEntity">The result entity type</typeparam> /// <param name="cache"></param> - /// <param name="id">The id of the entity to get or laod</param> - /// <param name="factory">The factory callback function to produce a value when a cache miss occurs</param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>A task that completes by returning the entity</returns> + /// <returns></returns> /// <exception cref="ArgumentNullException"></exception> - public static async Task<T?> GetOrLoadAsync<T>(this IEntityCache<T> cache, string id, Func<string, Task<T?>> factory, CancellationToken cancellation = default) where T : class + public static EntityCacheBuilder<TEntity> CreateResultCache<TEntity>(this IEntityCache<TEntity> cache) + where TEntity : class { - _ = cache ?? throw new ArgumentNullException(nameof(cache)); - _ = id ?? throw new ArgumentNullException(nameof(id)); - _ = factory ?? throw new ArgumentNullException(nameof(factory)); - - //try to load the value from cache - T? record = await cache.GetAsync(id, cancellation); - - //If record was not found in cache, load it from the factory - if (record is null) - { - record = await factory(id); - - //If new record found, write to cache - if (record is not null) - { - await cache.UpsertAsync(id, record, cancellation); - } - } - - return record; + ArgumentNullException.ThrowIfNull(cache); + return new EntityCacheBuilder<TEntity>(cache); } /// <summary> - /// Attemts to recover an entity from cache if possible, if a miss occurs, the - /// factory function is called to produce a value from a backing store. If the store - /// returns a result it is writen back to the cache before this method returns + /// Builds a transparent entity result cache for a backing store. /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="cache"></param> - /// <param name="id">The id of the entity to get or laod</param> - /// <param name="factory">The factory callback function to produce a value when a cache miss occurs</param> - /// <param name="cancellation">A token to cancel the operation</param> - /// <returns>A task that completes by returning the entity</returns> - /// <exception cref="ArgumentException"></exception> - /// <exception cref="ArgumentNullException"></exception> - public static async Task<T?> GetOrLoadAsync<T>(this IEntityCache<T> cache, string id, Func<string, CancellationToken, Task<T?>> factory, CancellationToken cancellation = default) where T : class + /// <typeparam name="TEntity"></typeparam> + /// <param name="builder"></param> + /// <param name="store">The backing data store used to fetch results from</param> + /// <returns>The new <see cref="TransparentEntityCache{TEntity}"/> that wraps the entity store</returns> + public static TransparentEntityCache<TEntity> BuildTransparent<TEntity>(this EntityCacheBuilder<TEntity> builder, IEntityStore<TEntity> store) + where TEntity : class { - ArgumentNullException.ThrowIfNull(cache); - ArgumentNullException.ThrowIfNull(factory); - ArgumentException.ThrowIfNullOrWhiteSpace(id); + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(store); - //try to load the value from cache - T? record = await cache.GetAsync(id, cancellation); + EntityResultCache<TEntity> resultCache = builder.Build(); - //If record was not found in cache, load it from the factory - if (record is null) - { - record = await factory(id, cancellation); - - //If new record found, write to cache - if(record is not null) - { - await cache.UpsertAsync(id, record, cancellation); - } - } + return new TransparentEntityCache<TEntity>(store, resultCache); + } + - return record; - } - - private sealed class EntityCacheImpl<T> : IEntityCache<T> where T : class + private sealed class EntityCacheImpl<T>(IGlobalCacheProvider cache, ICacheObjectDeserializer deserializer, ICacheObjectSerializer serializer) + : IEntityCache<T> where T : class { - private readonly IGlobalCacheProvider _cacheProvider; - private readonly ICacheObjectDeserializer _cacheObjectDeserialzer; - private readonly ICacheObjectSerializer _cacheObjectSerialzer; - - public EntityCacheImpl(IGlobalCacheProvider cache, ICacheObjectDeserializer deserializer, ICacheObjectSerializer serializer) - { - _cacheProvider = cache; - _cacheObjectDeserialzer = deserializer; - _cacheObjectSerialzer = serializer; - } + private readonly IGlobalCacheProvider _cacheProvider = cache; + private readonly ICacheObjectDeserializer _cacheObjectDeserialzer = deserializer; + private readonly ICacheObjectSerializer _cacheObjectSerialzer = serializer; ///<inheritdoc/> public Task<T?> GetAsync(string id, CancellationToken token = default) => _cacheProvider.GetAsync<T>(id, _cacheObjectDeserialzer, token); @@ -213,9 +170,9 @@ namespace VNLib.Plugins.Extensions.VNCache.DataModel public Task UpsertAsync(string id, T entity, CancellationToken token = default) => _cacheProvider.AddOrUpdateAsync(id, null, entity, _cacheObjectSerialzer, token); } - private sealed class ScopedCacheImpl: ScopedCache + private sealed class ScopedCacheImpl(IGlobalCacheProvider cache, ICacheKeyGenerator keyGen) : ScopedCache { - private readonly IGlobalCacheProvider Cache; + private readonly IGlobalCacheProvider Cache = cache; ///<inheritdoc/> public override bool IsConnected @@ -225,7 +182,7 @@ namespace VNLib.Plugins.Extensions.VNCache.DataModel } ///<inheritdoc/> - protected override ICacheKeyGenerator KeyGen { get; } + protected override ICacheKeyGenerator KeyGen { get; } = keyGen; ///<inheritdoc/> public override ICacheObjectDeserializer DefaultDeserializer => Cache.DefaultDeserializer; @@ -233,12 +190,6 @@ namespace VNLib.Plugins.Extensions.VNCache.DataModel ///<inheritdoc/> public override ICacheObjectSerializer DefaultSerializer => Cache.DefaultSerializer; - public ScopedCacheImpl(IGlobalCacheProvider cache, ICacheKeyGenerator keyGen) - { - this.Cache = cache; - KeyGen = keyGen; - } - ///<inheritdoc/> public override Task<bool> DeleteAsync(string key, CancellationToken cancellation) { diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityResultCache.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityResultCache.cs new file mode 100644 index 0000000..fb75fc6 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/EntityResultCache.cs @@ -0,0 +1,131 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: EntityResultCache.cs +* +* EntityResultCache.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// Represents a cache that can store entities by their unique key + /// using a user-provided backing store and custom request state. + /// </summary> + /// <typeparam name="TEntity"></typeparam> + /// <param name="cache">The cache backing store</param> + /// <param name="taskPolicy">Specifies how background cache tasks are handled</param> + /// <param name="expirationPolicy">The result expiration policy</param> + public class EntityResultCache<TEntity>( + IEntityCache<TEntity> cache, + ICacheTaskPolicy taskPolicy, + ICacheExpirationPolicy<TEntity> expirationPolicy + ) + where TEntity : class + { + + /// <summary> + /// Fetchs a result by it's request entity + /// </summary> + /// <param name="request">The fetch request state object</param> + /// <param name="cancellation">A token to canel the operation</param> + /// <param name="resultFactory">A callback generator function</param> + /// <returns>A task the returns the result of the requested entity, or null if it was not found or provided by the backing store</returns> + public async Task<TEntity?> FetchAsync<TRequest>( + TRequest request, + Func<TRequest, CancellationToken, Task<TEntity?>> resultFactory, + CancellationToken cancellation = default + ) where TRequest : IEntityCacheKey + { + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(resultFactory); + cancellation.ThrowIfCancellationRequested(); + + string key = request.GetKey(); + + //try to fetch from cache + TEntity? entity = await cache.GetAsync(key, cancellation); + + if (entity is not null) + { + //Check if the entity is expired + if (expirationPolicy.IsExpired(entity)) + { + //Setting to null will force a cache miss + entity = null; + } + } + + if (entity is null) + { + //Cache miss, load from factory + entity = await resultFactory(request, cancellation); + + if (entity is not null) + { + //Notify the expiration policy that the entity was refreshed before writing back to cache + expirationPolicy.OnRefreshed(entity); + + //Fresh entity was fetched from the factory so write to cache + Task upsert = cache.UpsertAsync(key, entity, cancellation); + + //Allow task policy to determine how completions are observed + await taskPolicy.ObserveOperationAsync(upsert); + } + } + + return entity; + } + + /// <summary> + /// Removes an entity from the cache by it's request entity + /// </summary> + /// <param name="request">The request entity to retrieve the entity key from</param> + /// <param name="cancellation">A token to cancel the async operation</param> + /// <returns>A task that completes when the key is removed, based on the task policy</returns> + public Task RemoveAsync<TRequest>(TRequest request, CancellationToken cancellation = default) + where TRequest : IEntityCacheKey + { + ArgumentNullException.ThrowIfNull(request); + + string key = request.GetKey(); + + return cache.RemoveAsync(key, cancellation); + } + + /// <summary> + /// Removes an entity from the cache by it's request entity + /// </summary> + /// <param name="key">The entities unique key</param> + /// <param name="cancellation">A token to cancel the async operation</param> + /// <returns>A task that completes when the key is removed, based on the task policy</returns> + public Task RemoveAsync(string key, CancellationToken cancellation = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + + Task remove = cache.RemoveAsync(key, cancellation); + + return taskPolicy.ObserveOperationAsync(remove); + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheExpirationPolicy.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheExpirationPolicy.cs new file mode 100644 index 0000000..60f1f22 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheExpirationPolicy.cs @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: ICacheExpirationPolicy.cs +* +* ICacheExpirationPolicy.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// Provides an expiration policy for cache entities + /// </summary> + /// <typeparam name="TEntity"></typeparam> + public interface ICacheExpirationPolicy<TEntity> + { + /// <summary> + /// Determines if the entity is expired and should be + /// reloaded from the backing store. + /// </summary> + /// <param name="result">The entity to check status of</param> + /// <returns>True of the entity has expired and should be reloaded, false if it is still a valid result</returns> + bool IsExpired(TEntity result); + + /// <summary> + /// Fired directly after the entity has been refreshed + /// </summary> + /// <param name="entity">The entity freshly fetched from the backing store</param> + virtual void OnRefreshed(TEntity entity) + { } + } + +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheExpirationStrategy.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheTaskPolicy.cs index 299834b..4824937 100644 --- a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheExpirationStrategy.cs +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/ICacheTaskPolicy.cs @@ -1,11 +1,11 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.VNCache -* File: ICacheExpirationStrategy.cs +* File: ICacheTaskPolicy.cs * -* ICacheExpirationStrategy.cs is part of VNLib.Plugins.Extensions.VNCache +* ICacheTaskPolicy.cs is part of VNLib.Plugins.Extensions.VNCache * which is part of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify @@ -22,27 +22,21 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; +using System.Threading.Tasks; namespace VNLib.Plugins.Extensions.VNCache.DataModel { /// <summary> - /// An interface that provides an object caching expiration - /// instructions + /// Provides a policy for observing the completion of cache operations /// </summary> - public interface ICacheExpirationStrategy + public interface ICacheTaskPolicy { /// <summary> - /// The maxium age of a given entity + /// Observes the completion of a cache operation /// </summary> - TimeSpan CacheMaxAge { get; } - - /// <summary> - /// Invoked when a record is retrieved and determined to be expired - /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="expired"></param> - void OnExpired<T>(T expired); + /// <param name="operation">The task representing the operation to observe</param> + /// <returns>A task that the caller will await on the current control flow</returns> + Task ObserveOperationAsync(Task operation); } } diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/IEntityCacheKey.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/IEntityCacheKey.cs new file mode 100644 index 0000000..8416a40 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/IEntityCacheKey.cs @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: IEntityCacheKey.cs +* +* IEntityCacheKey.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// When implemnted on an entity object, gets a deterministic cache key for the entity + /// </summary> + public interface IEntityCacheKey + { + /// <summary> + /// Gets the key for the current entity request + /// </summary> + /// <returns>The cache key string unique to the stored record</returns> + string GetKey(); + } + +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/IEntityStore.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/IEntityStore.cs new file mode 100644 index 0000000..29282cf --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/IEntityStore.cs @@ -0,0 +1,61 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: IEntityStore.cs +* +* IEntityStore.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// An instance that stores entities which can be fetched, updated, or removed + /// </summary> + /// <typeparam name="TEntity">The entity result type</typeparam> + public interface IEntityStore<TEntity> + { + /// <summary> + /// Fetches an entity from the store by it's request entity state object + /// </summary> + /// <param name="request">The request state object</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that yields the entity object if it exists</returns> + Task<TEntity?> GetAsync<TRequest>(TRequest request, CancellationToken cancellation = default) where TRequest : IEntityCacheKey; + + /// <summary> + /// Updates or inserts an entity into the store + /// </summary> + /// <param name="entity">The entity instance to store in the database</param> + /// <param name="request">The request state object</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that completes when the upsert operation has completed</returns> + Task UpsertAsync<TRequest>(TRequest request, TEntity entity, CancellationToken cancellation = default) where TRequest : IEntityCacheKey; + + /// <summary> + /// Removes an entity from the store + /// </summary> + /// <param name="request">The request state object</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A task that completes with the result of the delete operation</returns> + Task<bool> RemoveAsync<TRequest>(TRequest request, CancellationToken cancellation = default) where TRequest : IEntityCacheKey; + } +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/InfiniteCacheExpirationPolicy.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/InfiniteCacheExpirationPolicy.cs new file mode 100644 index 0000000..b28ee84 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/InfiniteCacheExpirationPolicy.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: InfiniteCacheExpirationPolicy.cs +* +* InfiniteCacheExpirationPolicy.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// A cache exiration policy that never expires entities + /// from the cache + /// </summary> + /// <typeparam name="TEntity">The entity type</typeparam> + public class InfiniteCacheExpirationPolicy<TEntity> : ICacheExpirationPolicy<TEntity> + { + ///<inheritdoc/> + public bool IsExpired(TEntity result) => false; + } + +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/TransparentEntityCache.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/TransparentEntityCache.cs new file mode 100644 index 0000000..90aeb74 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/TransparentEntityCache.cs @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: EntityCacheExtensions.cs +* +* EntityCacheExtensions.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// A cache proxy that sits transparently between a backing store and the caller + /// to cache unique entities + /// </summary> + /// <typeparam name="TEntity"></typeparam> + /// <param name="store">The backing entity data store</param> + /// <param name="cache">The entity cache used to fetch</param> + public class TransparentEntityCache<TEntity>(IEntityStore<TEntity> store, EntityResultCache<TEntity> cache) : IEntityStore<TEntity> + where TEntity : class + { + + ///<inheritdoc/> + public Task<TEntity?> GetAsync<TRequest>(TRequest request, CancellationToken cancellation = default) + where TRequest : IEntityCacheKey + { + return cache.FetchAsync(request, store.GetAsync, cancellation); + } + + ///<inheritdoc/> + public Task<bool> RemoveAsync<TRequest>(TRequest request, CancellationToken cancellation = default) + where TRequest : IEntityCacheKey + { + Task<bool> _fromCache = cache.RemoveAsync(request, cancellation) + .ContinueWith(static (_) => true, TaskScheduler.Default); + + Task<bool> _fromStore = store.RemoveAsync(request, cancellation); + + return Task.WhenAll(_fromCache, _fromStore) + .ContinueWith(static (t) => t.Result.All(static r => r), TaskScheduler.Default); + } + + ///<inheritdoc/> + public Task UpsertAsync<TRequest>(TRequest request, TEntity entity, CancellationToken cancellation = default) + where TRequest : IEntityCacheKey + { + //Remove key from cache but push update to store + Task _fromCache = cache.RemoveAsync(request, cancellation); + + Task _fromStore = store.UpsertAsync(request, entity, cancellation); + + return Task.WhenAll(_fromCache, _fromStore); + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/WriteBackCachePolicy.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/WriteBackCachePolicy.cs new file mode 100644 index 0000000..93434de --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/WriteBackCachePolicy.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: WriteBackCachePolicy.cs +* +* WriteBackCachePolicy.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// Provides a policy for observing the completion of cache operations + /// </summary> + /// <param name="onFaulted"> + /// A callback function that is executed when a cache operation has fauled due to an exception + /// </param> + public class WriteBackCachePolicy(Action<Task> onFaulted) : ICacheTaskPolicy + { + ///<inheritdoc/> + public Task ObserveOperationAsync(Task operation) + { + ArgumentNullException.ThrowIfNull(operation); + + if (!operation.IsCompleted) + { + //Defer the observation to the callback function to watch for errors + _ = operation.ContinueWith( + ObserveOperation, + cancellationToken: default, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default + ); + } + + return Task.CompletedTask; + } + + private void ObserveOperation(Task operation) + { + //Should only be called on a completed task + Debug.Assert(operation.IsCompleted); + + if (operation.IsFaulted) + { + onFaulted(operation); + } + } + } + +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/WriteThroughCachePolicy.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/WriteThroughCachePolicy.cs new file mode 100644 index 0000000..9a3e0fc --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/DataModel/WriteThroughCachePolicy.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: WriteThroughCachePolicy.cs +* +* WriteThroughCachePolicy.cs is part of VNLib.Plugins.Extensions.VNCache +* which is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.VNCache.DataModel +{ + /// <summary> + /// Provides a very simple Write-Through cache policy that + /// will cause fetch functions to block until the cache is + /// consistent with the backing store + /// </summary> + public class WriteThroughCachePolicy : ICacheTaskPolicy + { + /// <summary> + /// The singleton instance of the write-through cache policy + /// </summary> + public static readonly WriteThroughCachePolicy Instance = new(); + + ///<inheritdoc/> + public Task ObserveOperationAsync(Task operation) => operation; //Write through + } + +} diff --git a/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs b/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs index f28ea74..a7f3835 100644 --- a/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.VNCache/src/VNCacheExtensions.cs @@ -55,7 +55,7 @@ namespace VNLib.Plugins.Extensions.VNCache /// <param name="search">The directory search option</param> /// <returns>The loaded <see cref="IGlobalCacheProvider"/> instance</returns> public static IGlobalCacheProvider LoadCacheLibrary(this PluginBase plugin, string asmDllPath, SearchOption search = SearchOption.AllDirectories) - => plugin.CreateServiceExternal<IGlobalCacheProvider>(asmDllPath, search, null); + => plugin.CreateServiceExternal<IGlobalCacheProvider>(asmDllPath, search, defaultCtx: null); /// <summary> /// Gets the configuration assigned global cache provider, if defined. If the configuration does not |