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

In-Memory Cache #1881

Merged
merged 19 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
84d9138
caching setup
seantleonard Oct 9, 2023
9450efb
class folder structure cchanges
seantleonard Oct 18, 2023
d624689
Merge remote-tracking branch 'origin/main' into dev/seantleonard/inme…
seantleonard Oct 30, 2023
bdc776c
Adding caching interfaces and initial implementations.
seantleonard Oct 31, 2023
6b61514
Merge branch 'main' of https://github.com/Azure/data-api-builder into…
seantleonard Oct 31, 2023
4cd8f8e
Merge branch 'dev/seantleonard/inmemorycache' of https://github.com/A…
seantleonard Oct 31, 2023
e8470ae
progress
seantleonard Nov 11, 2023
2eea6bd
tests and caching updates
seantleonard Nov 14, 2023
8c845d5
updated tests.
seantleonard Nov 14, 2023
abd72a8
more comments and test revisions. Feature flagging the main execution…
seantleonard Nov 14, 2023
d89aad3
Address feedback and added comments. Plus added additional asserts in…
seantleonard Dec 6, 2023
b8aace6
Merge branch 'main' into dev/seantleonard/inmemorycache
seantleonard Dec 6, 2023
86d4854
Merge branch 'dev/seantleonard/inmemorycache' of https://github.com/A…
seantleonard Dec 6, 2023
18eb138
updating cache gate to consider config.cacheEnabled too.
seantleonard Dec 6, 2023
bee1759
Merge branch 'main' into dev/seantleonard/inmemorycache
seantleonard Dec 6, 2023
d257e4b
Update caching code to utilize configuration values for ttl (global c…
seantleonard Dec 6, 2023
909ce74
Merge branch 'main' into dev/seantleonard/inmemorycache
seantleonard Dec 6, 2023
229d912
reverting change to verified file that was needed locally.
seantleonard Dec 7, 2023
fd02027
Remove FeatureFlag in favor of runtime config.
seantleonard Dec 7, 2023
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
9 changes: 9 additions & 0 deletions src/Config/FeatureFlagConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Config;

public static class FeatureFlagConstants
{
public const string INMEMORYCACHE = "InMemoryCachingPreview";
}
2 changes: 2 additions & 0 deletions src/Config/ObjectModel/Entity.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;
Expand Down Expand Up @@ -56,6 +57,7 @@ public Entity(
/// </summary>
/// <returns>Whether caching is enabled for the entity.</returns>
[JsonIgnore]
[MemberNotNullWhen(true, nameof(Cache))]
public bool IsCachingEnabled =>
Cache is not null &&
Cache.Enabled is not null &&
Expand Down
2 changes: 2 additions & 0 deletions src/Config/ObjectModel/EntityCacheOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;
Expand Down Expand Up @@ -56,5 +57,6 @@ public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null)
/// property/value specified would be interpreted by DAB as "user explicitly set ttl."
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
[MemberNotNullWhen(true, nameof(TtlSeconds))]
public bool UserProvidedTtlOptions { get; init; } = false;
}
60 changes: 60 additions & 0 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,66 @@ public bool IsDevelopmentMode() =>
Runtime is not null && Runtime.Host is not null
&& Runtime.Host.Mode is HostMode.Development;

/// <summary>
/// Returns the ttl-seconds value for a given entity.
/// If the property is not set, returns the global default value set in the runtime config.
/// If the global default value is not set, the default value is used (5 seconds).
/// </summary>
/// <param name="entityName">Name of the entity to check cache configuration.</param>
/// <returns>Number of seconds (ttl) that a cache entry should be valid before cache eviction.</returns>
/// <exception cref="DataApiBuilderException">Raised when an invalid entity name is provided or if the entity has caching disabled.</exception>
public int GetEntityCacheEntryTtl(string entityName)
{
if (!Entities.TryGetValue(entityName, out Entity? entityConfig))
{
throw new DataApiBuilderException(
message: $"{entityName} is not a valid entity.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

if (!entityConfig.IsCachingEnabled)
{
throw new DataApiBuilderException(
message: $"{entityName} does not have caching enabled.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
}

if (entityConfig.Cache.UserProvidedTtlOptions)
{
return entityConfig.Cache.TtlSeconds.Value;
}
else
{
return GlobalCacheEntryTtl();
}
}

/// <summary>
/// Whether the caching service should be used for a given operation. This is determined by
/// - whether caching is enabled globally
/// - whether the datasource is SQL and session context is disabled.
/// </summary>
/// <returns>Whether cache operations should proceed.</returns>
public bool CanUseCache()
{
bool setSessionContextEnabled = DataSource.GetTypedOptions<MsSqlOptions>()?.SetSessionContext ?? true;
return IsCachingEnabled && SqlDataSourceUsed && !setSessionContextEnabled;
}

/// <summary>
/// Returns the ttl-seconds value for the global cache entry.
/// If no value is explicitly set, returns the global default value.
/// </summary>
/// <returns>Number of seconds a cache entry should be valid before cache eviction.</returns>
public int GlobalCacheEntryTtl()
{
return Runtime is not null && Runtime.IsCachingEnabled && Runtime.Cache.UserProvidedTtlOptions
? Runtime.Cache.TtlSeconds.Value
: EntityCacheOptions.DEFAULT_TTL_SECONDS;
}

private void CheckDataSourceNamePresent(string dataSourceName)
{
if (!_dataSourceNameToDataSource.ContainsKey(dataSourceName))
Expand Down
2 changes: 2 additions & 0 deletions src/Config/ObjectModel/RuntimeOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;
Expand Down Expand Up @@ -37,6 +38,7 @@ public RuntimeOptions(
/// </summary>
/// <returns>Whether caching is enabled globally.</returns>
[JsonIgnore]
[MemberNotNullWhen(true, nameof(Cache))]
public bool IsCachingEnabled =>
Cache is not null &&
Cache.Enabled is not null &&
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Azure.DataApiBuilder.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageReference Include="HotChocolate.AspNetCore.Authorization" />
<PackageReference Include="HotChocolate.Types.NodaTime" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="NJsonSchema"/>
<PackageReference Include="NJsonSchema" />
<PackageReference Include="Microsoft.Azure.Cosmos" />
<PackageReference Include="Microsoft.Data.SqlClient" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
Expand All @@ -34,6 +34,7 @@
<PackageReference Include="Polly" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="ZiggyCreatures.FusionCache" />
</ItemGroup>

<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
Expand Down
28 changes: 28 additions & 0 deletions src/Core/Models/DatabaseQueryMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Core.Models;

/// <summary>
/// Represents the database query built from a query structure.
/// Contains all query metadata need to create a cache key.
/// </summary>
public class DatabaseQueryMetadata
{
public string QueryText { get; }
public string DataSource { get; }
public Dictionary<string, DbConnectionParam> QueryParameters { get; }

/// <summary>
/// Creates a "Data Transfer Object" (DTO) used for provided query metadata to dependent services.
/// </summary>
/// <param name="queryText">Raw query text built from a query structure object.</param>
/// <param name="dataSource">Name of the data source where the query will execute.</param>
/// <param name="queryParameters">Dictonary of query parameter names and values.</param>
public DatabaseQueryMetadata(string queryText, string dataSource, Dictionary<string, DbConnectionParam> queryParameters)
{
QueryText = queryText;
DataSource = dataSource;
QueryParameters = queryParameters;
}
}
6 changes: 4 additions & 2 deletions src/Core/Resolvers/Factories/QueryEngineFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Services.Cache;
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.AspNetCore.Http;
Expand All @@ -29,7 +30,8 @@ public QueryEngineFactory(RuntimeConfigProvider runtimeConfigProvider,
IHttpContextAccessor contextAccessor,
IAuthorizationResolver authorizationResolver,
GQLFilterParser gQLFilterParser,
ILogger<IQueryEngine> logger)
ILogger<IQueryEngine> logger,
DabCacheService cache)
{
_queryEngines = new Dictionary<DatabaseType, IQueryEngine>();

Expand All @@ -38,7 +40,7 @@ public QueryEngineFactory(RuntimeConfigProvider runtimeConfigProvider,
if (config.SqlDataSourceUsed)
{
IQueryEngine queryEngine = new SqlQueryEngine(
queryManagerFactory, metadataProviderFactory, contextAccessor, authorizationResolver, gQLFilterParser, logger, runtimeConfigProvider);
queryManagerFactory, metadataProviderFactory, contextAccessor, authorizationResolver, gQLFilterParser, logger, runtimeConfigProvider, cache);
_queryEngines.Add(DatabaseType.MSSQL, queryEngine);
_queryEngines.Add(DatabaseType.MySQL, queryEngine);
_queryEngines.Add(DatabaseType.PostgreSQL, queryEngine);
Expand Down
41 changes: 30 additions & 11 deletions src/Core/Resolvers/SqlQueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Resolvers.Factories;
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Core.Services.Cache;
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
Expand All @@ -30,6 +31,7 @@ public class SqlQueryEngine : IQueryEngine
private readonly ILogger<IQueryEngine> _logger;
private readonly RuntimeConfigProvider _runtimeConfigProvider;
private readonly GQLFilterParser _gQLFilterParser;
private readonly DabCacheService _cache;

// <summary>
// Constructor.
Expand All @@ -41,7 +43,8 @@ public SqlQueryEngine(
IAuthorizationResolver authorizationResolver,
GQLFilterParser gQLFilterParser,
ILogger<IQueryEngine> logger,
RuntimeConfigProvider runtimeConfigProvider)
RuntimeConfigProvider runtimeConfigProvider,
DabCacheService cache)
{
_queryFactory = queryFactory;
_sqlMetadataProviderFactory = sqlMetadataProviderFactory;
Expand All @@ -50,6 +53,7 @@ public SqlQueryEngine(
_gQLFilterParser = gQLFilterParser;
_logger = logger;
_runtimeConfigProvider = runtimeConfigProvider;
_cache = cache;
}

/// <summary>
Expand Down Expand Up @@ -199,21 +203,36 @@ public async Task<IActionResult> ExecuteAsync(StoredProcedureRequestContext cont
// </summary>
private async Task<JsonDocument?> ExecuteAsync(SqlQueryStructure structure, string dataSourceName)
{
DatabaseType databaseType = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName).DatabaseType;
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
DatabaseType databaseType = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType;
IQueryBuilder queryBuilder = _queryFactory.GetQueryBuilder(databaseType);
IQueryExecutor queryExecutor = _queryFactory.GetQueryExecutor(databaseType);

// Open connection and execute query using _queryExecutor
string queryString = queryBuilder.Build(structure);
JsonDocument? jsonDocument =
await queryExecutor.ExecuteQueryAsync(
sqltext: queryString,
parameters: structure.Parameters,
dataReaderHandler: queryExecutor.GetJsonResultAsync<JsonDocument>,
httpContext: _httpContextAccessor.HttpContext!,
args: null,
dataSourceName: dataSourceName);
return jsonDocument;

if (runtimeConfig.CanUseCache())
{
seantleonard marked this conversation as resolved.
Show resolved Hide resolved
bool dbPolicyConfigured = !string.IsNullOrEmpty(structure.DbPolicyPredicatesForOperations[EntityActionOperation.Read]);

if (dbPolicyConfigured)
{
DatabaseQueryMetadata queryMetadata = new(queryText: queryString, dataSource: dataSourceName, queryParameters: structure.Parameters);
JsonElement result = await _cache.GetOrSetAsync<JsonElement>(queryExecutor, queryMetadata, cacheEntryTtl: runtimeConfig.GetEntityCacheEntryTtl(entityName: structure.EntityName));
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(result);
JsonDocument cacheServiceResponse = JsonDocument.Parse(jsonBytes);
return cacheServiceResponse;
}
}

JsonDocument? response = await queryExecutor.ExecuteQueryAsync(
sqltext: queryString,
parameters: structure.Parameters,
dataReaderHandler: queryExecutor.GetJsonResultAsync<JsonDocument>,
httpContext: _httpContextAccessor.HttpContext!,
args: null,
dataSourceName: dataSourceName);
return response;
}

// <summary>
Expand Down
Loading