Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement in-memory cache for resource providers #2026

Merged
merged 7 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The following new App Configuration settings are required:
|`FoundationaLLM:UserPortal:Configuration:ShowLastConversationOnStartup` | `false` | If `true`, the last conversation will be displayed when the user logs in. Otherwise, a new conversation placeholder appears on page load. |
|`FoundationaLLM:UserPortal:Configuration:ShowMessageTokens` | `true` | If `true`, the number of consumed tokens on agent and user messages will appear. |
|`FoundationaLLM:UserPortal:Configuration:ShowViewPrompt` | `true` | If `true`, the "View Prompt" button on agent messages will appear. |
|`FoundationaLLM:Instance:EnableResourceProvidersCache` | `false` | If `true`, the caching of resource providers will be enabled. |

#### Agent Tool configuration changes

Expand Down
8 changes: 8 additions & 0 deletions src/dotnet/Common/Constants/Data/AppConfiguration.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
"value": "",
"content_type": "",
"first_version": "0.8.0"
},
{
"name": "EnableResourceProvidersCache",
"description": "Enable caching for resource providers.",
"secret": "",
"value": "false",
"content_type": "",
"first_version": "0.9.1"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using FoundationaLLM.Common.Models.ResourceProviders;

namespace FoundationaLLM.Common.Interfaces
{
/// <summary>
/// Provides the resource caching services used by FoundationaLLM resource providers.
/// </summary>
public interface IResourceProviderResourceCacheService
{
/// <summary>
/// Tries to get a resource value identified by a resource reference from the cache.
/// </summary>
/// <typeparam name="T">The type of resource value to be retrieved.</typeparam>
/// <param name="resourceReference">The <see cref="ResourceReference"/> used as a key in the cache.</param>
/// <param name="resourceValue">The resource value to be retrieved.</param>
/// <returns><see langword="true"/> is the resource value was found in the cache, <see langword="false"/> otherwise.</returns>
bool TryGetValue<T>(ResourceReference resourceReference, out T? resourceValue) where T: ResourceBase;

/// <summary>
/// Sets a resource value identified by a resource reference in the cache.
/// </summary>
/// <typeparam name="T">The type of resource value to be set.</typeparam>
/// <param name="resourceReference">The <see cref="ResourceReference"/> used as a key in the cache.</param>
/// <param name="resourceValue">The resource value to be set.</param>
void SetValue<T>(ResourceReference resourceReference, T resourceValue) where T : ResourceBase;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ public class InstanceSettings
/// The Regex pattern used to validate the values allowed as User Principal Name (UPN) substitutes in the X-USER-IDENTITY header.
/// </summary>
public string? IdentitySubstitutionUserPrincipalNamePattern { get; set; }

/// <summary>
/// Enabling caching for resource providers.
/// </summary>
public bool EnableResourceProvidersCache { get; set; } = false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using FoundationaLLM.Common.Interfaces;
using FoundationaLLM.Common.Models.ResourceProviders;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;

namespace FoundationaLLM.Common.Services.ResourceProviders
{
/// <summary>
/// Provides the resource caching services used by FoundationaLLM resource providers.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> used to log information.</param>
public class ResourceProviderResourceCacheService(
ILogger logger) : IResourceProviderResourceCacheService
{
private readonly ILogger _logger = logger;

private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 10000, // Limit cache size to 5000 resources.
ExpirationScanFrequency = TimeSpan.FromMinutes(5) // Scan for expired items every five minutes.
});
private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(60)) // Cache entries are valid for 60 minutes.
.SetSlidingExpiration(TimeSpan.FromMinutes(30)) // Reset expiration time if accessed within 5 minutes.
.SetSize(1); // Each cache entry is a single resource.

/// <inheritdoc/>
public void SetValue<T>(ResourceReference resourceReference, T resourceValue) where T : ResourceBase
{
try
{
_cache.Set<T>(GetCacheKey(resourceReference), resourceValue, _cacheEntryOptions);
_logger.LogInformation("The resource {ResourceName} of type {ResourceType} has been set in the cache.",
resourceReference.Name,
resourceReference.Type);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an error setting the resource {ResourceName} of type {ResourceType} in the cache.",
resourceReference.Name,
resourceReference.Type);
}
}

/// <inheritdoc/>
public bool TryGetValue<T>(ResourceReference resourceReference, out T? resourceValue) where T : ResourceBase
{
resourceValue = default;

try
{
if (_cache.TryGetValue<T>(GetCacheKey(resourceReference), out T? cachedValue)
&& cachedValue != null)
{
resourceValue = cachedValue;
_logger.LogInformation("The resource {ResourceName} of type {ResourceType} has been retrieved from the cache.",
resourceReference.Name,
resourceReference.Type);
return true;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an error getting the resource {ResourceName} of type {ResourceType} from the cache.",
resourceReference.Name,
resourceReference.Type);
}

return false;
}

private string GetCacheKey(ResourceReference resourceReference) =>
$"{resourceReference.Type}|{resourceReference.Name}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using FoundationaLLM.Common.Models.ResourceProviders;
using FoundationaLLM.Common.Services.Events;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
Expand All @@ -37,6 +38,8 @@ public class ResourceProviderServiceBase<TResourceReference> : IResourceProvider
private readonly bool _useInternalReferencesStore;
private readonly SemaphoreSlim _lock = new(1, 1);

private readonly IResourceProviderResourceCacheService? _resourceCache;

/// <summary>
/// The resource reference store used by the resource provider.
/// </summary>
Expand Down Expand Up @@ -143,6 +146,9 @@ public ResourceProviderServiceBase(
_eventNamespacesToSubscribe = eventNamespacesToSubscribe;
_useInternalReferencesStore = useInternalReferencesStore;

if (_instanceSettings.EnableResourceProvidersCache)
_resourceCache = new ResourceProviderResourceCacheService(_logger);

_allowedResourceProviders = [_name];
_allowedResourceTypes = GetResourceTypes();

Expand Down Expand Up @@ -996,6 +1002,12 @@ protected async Task<List<ResourceProviderGetResult<T>>> LoadResources<T>(
$"The resource reference {resourceReference.Name} is not of the expected type {typeof(T).Name}.",
StatusCodes.Status400BadRequest);


if (_resourceCache != null
&& _resourceCache.TryGetValue<T>(resourceReference, out T? cachedResource)
&& cachedResource != null)
return cachedResource;

if (await _storageService.FileExistsAsync(_storageContainerName, resourceReference.Filename, default))
{
var fileContent =
Expand All @@ -1009,6 +1021,8 @@ protected async Task<List<ResourceProviderGetResult<T>>> LoadResources<T>(
?? throw new ResourceProviderException($"Failed to load the resource {resourceReference.Name}. Its content file might be corrupt.",
StatusCodes.Status500InternalServerError);

_resourceCache?.SetValue<T>(resourceReference, resourceObject);

return resourceObject;
}
catch (Exception ex)
Expand Down Expand Up @@ -1038,6 +1052,11 @@ protected async Task<List<ResourceProviderGetResult<T>>> LoadResources<T>(
?? throw new ResourceProviderException($"Could not locate the {resourceName} resource.",
StatusCodes.Status404NotFound);

if (_resourceCache != null
&& _resourceCache.TryGetValue<T>(resourceReference, out T? cachedResource)
&& cachedResource != null)
return cachedResource;

if (await _storageService.FileExistsAsync(_storageContainerName, resourceReference.Filename, default))
{
var fileContent =
Expand All @@ -1048,6 +1067,8 @@ protected async Task<List<ResourceProviderGetResult<T>>> LoadResources<T>(
?? throw new ResourceProviderException($"Failed to load the resource {resourceReference.Name}. Its content file might be corrupt.",
StatusCodes.Status400BadRequest);

_resourceCache?.SetValue<T>(resourceReference, resourceObject);

return resourceObject;
}

Expand Down Expand Up @@ -1086,6 +1107,9 @@ await _storageService.WriteFileAsync(
default);

await _resourceReferenceStore!.AddResourceReference(resourceReference);

// Add resource to cache if caching is enabled.
_resourceCache?.SetValue<T>(resourceReference, resource);
}
finally
{
Expand Down Expand Up @@ -1200,6 +1224,9 @@ await _storageService.WriteFileAsync(
JsonSerializer.Serialize<T>(resource, _serializerSettings),
default,
default);

// Update resource to cache if caching is enabled.
_resourceCache?.SetValue<T>(resourceReference, resource);
}
finally
{
Expand Down
7 changes: 7 additions & 0 deletions src/dotnet/Common/Templates/AppConfigurationKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public static class AppConfigurationKeys
/// </summary>
public const string FoundationaLLM_Instance_IdentitySubstitutionUserPrincipalNamePattern =
"FoundationaLLM:Instance:IdentitySubstitutionUserPrincipalNamePattern";

/// <summary>
/// The app configuration key for the FoundationaLLM:Instance:EnableResourceProvidersCache setting.
/// <para>Value description:<br/>Enable caching for resource providers.</para>
/// </summary>
public const string FoundationaLLM_Instance_EnableResourceProvidersCache =
"FoundationaLLM:Instance:EnableResourceProvidersCache";

#endregion

Expand Down
7 changes: 7 additions & 0 deletions src/dotnet/Common/Templates/appconfig.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
"content_type": "",
"tags": {}
},
{
"key": "FoundationaLLM:Instance:EnableResourceProvidersCache",
"value": "false",
"label": null,
"content_type": "",
"tags": {}
},
{
"key": "FoundationaLLM:Configuration:KeyVaultURI",
"value": "${env:AZURE_KEY_VAULT_ENDPOINT}",
Expand Down
Loading