/* * 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 { /// /// Represents a cache that can store entities by their unique key /// using a user-provided backing store and custom request state. /// /// /// The cache backing store /// Specifies how background cache tasks are handled /// The result expiration policy public class EntityResultCache( IEntityCache cache, ICacheTaskPolicy taskPolicy, ICacheExpirationPolicy expirationPolicy ) where TEntity : class { /// /// The backing entity cache store /// public IEntityCache Cache => cache; /// /// The task policy for which this result cache will /// respect /// public ICacheTaskPolicy TaskPolicy => taskPolicy; /// /// The expiration policy for which this result cache will /// respect for entity expiration and refreshing /// public ICacheExpirationPolicy ExpirationPolicy => expirationPolicy; /// /// Fetchs a result by it's request entity /// /// The fetch request state object /// A token to canel the operation /// A callback generator function /// A task the returns the result of the requested entity, or null if it was not found or provided by the backing store public Task FetchAsync( TRequest request, Func> resultFactory, CancellationToken cancellation = default ) where TRequest : IEntityCacheKey { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(resultFactory); return FetchAsync( key: request.GetKey(), state: (resultFactory, request), resultFactory: static (rf, c) => rf.resultFactory(rf.request, c), cancellation ); } /// /// Fetchs a result by it's request entity /// /// The fetch request state object /// A token to canel the operation /// A callback generator function /// A task the returns the result of the requested entity, or null if it was not found or provided by the backing store public Task FetchAsync( string key, Func> resultFactory, CancellationToken cancellation = default ) { ArgumentNullException.ThrowIfNull(resultFactory); return FetchAsync( key, state: resultFactory, resultFactory: static (rf, c) => rf(c), cancellation ); } private async Task FetchAsync( string key, TState state, Func> resultFactory, CancellationToken cancellation = default ) { ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentNullException.ThrowIfNull(resultFactory); cancellation.ThrowIfCancellationRequested(); //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(state, 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; } /// /// Removes an entity from the cache by it's request entity /// /// The request entity to retrieve the entity key from /// A token to cancel the async operation /// A task that completes when the key is removed, based on the task policy public Task RemoveAsync(TRequest request, CancellationToken cancellation = default) where TRequest : IEntityCacheKey { ArgumentNullException.ThrowIfNull(request); string key = request.GetKey(); return cache.RemoveAsync(key, cancellation); } /// /// Removes an entity from the cache by it's request entity /// /// The entities unique key /// A token to cancel the async operation /// A task that completes when the key is removed, based on the task policy public Task RemoveAsync(string key, CancellationToken cancellation = default) { ArgumentException.ThrowIfNullOrWhiteSpace(key); Task remove = cache.RemoveAsync(key, cancellation); return taskPolicy.ObserveOperationAsync(remove); } /// /// Performs a cache replacement operation. That is substitutes an exiting /// value with a new one, or inserts a new value if the key does not exist. /// /// The operation request state object /// The entity object to store /// A generic callback function to invoke in parallel with the upsert operation /// A token to cancel the async operation /// /// A task that completes when the upsert operation has completed according to the /// /// /// /// public Task UpsertAsync( TRequest request, TEntity entity, Func action, CancellationToken cancellation = default ) where TRequest : IEntityCacheKey { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(action); return UpsertAsync( key: request.GetKey(), entity: entity, state: (action, request), callback: static (cb, e, c) => cb.action.Invoke(cb.request, e, c), cancellation ); } /// /// Performs a cache replacement operation. That is substitutes an exiting /// value with a new one, or inserts a new value if the key does not exist. /// /// The entity's unique id within the cache store /// The entity object to store /// A generic callback function to invoke in parallel with the upsert operation /// A token to cancel the async operation /// /// A task that completes when the upsert operation has completed according to the /// /// /// /// public Task UpsertAsync( string key, TEntity entity, Func action, CancellationToken cancellation = default ) { ArgumentNullException.ThrowIfNull(action); return UpsertAsync( key, entity, state: action, callback: static (cb, e, c) => cb.Invoke(e, c), cancellation ); } /// /// Performs a cache replacement operation. That is substitutes an exiting /// value with a new one, or inserts a new value if the key does not exist. /// /// The entity's unique id within the cache store /// The entity object to store /// A token to cancel the async operation /// /// A task that completes when the upsert operation has completed according to the /// /// /// public Task UpsertAsync(string key, TEntity entity, CancellationToken cancellation = default) { return UpsertAsync( key, entity, state: null, callback: static (_, _, _) => Task.CompletedTask, cancellation ); } private Task UpsertAsync( string key, TEntity entity, TState state, Func callback, CancellationToken cancellation = default ) { ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentNullException.ThrowIfNull(callback); cancellation.ThrowIfCancellationRequested(); //Call refresh before storing the entity incase any setup needs to be performed expirationPolicy.OnRefreshed(entity); //Cache task must be observed by the task policy Task upsert = taskPolicy.ObserveOperationAsync( operation: cache.UpsertAsync(key, entity, cancellation) ); Task cbResult = callback(state, entity, cancellation); //Combine the observed task and the callback function return Task.WhenAll(cbResult, upsert); } } }