/*
* 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