Skip to content

Add Level 2 Caching #2619

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
@@ -739,7 +739,7 @@ private static bool TryUpdateConfiguredRuntimeOptions(
if (options.RuntimeCacheEnabled != null ||
options.RuntimeCacheTTL != null)
{
EntityCacheOptions? updatedCacheOptions = runtimeConfig?.Runtime?.Cache ?? new();
RuntimeCacheOptions? updatedCacheOptions = runtimeConfig?.Runtime?.Cache ?? new();
bool status = TryUpdateConfiguredCacheValues(options, ref updatedCacheOptions);
if (status)
{
@@ -906,7 +906,7 @@ private static bool TryUpdateConfiguredGraphQLValues(
/// <returns>True if the value needs to be udpated in the runtime config, else false</returns>
private static bool TryUpdateConfiguredCacheValues(
ConfigureOptions options,
ref EntityCacheOptions? updatedCacheOptions)
ref RuntimeCacheOptions? updatedCacheOptions)
{
object? updatedValue;
try
49 changes: 44 additions & 5 deletions src/Config/Converters/EntityCacheOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
@@ -12,6 +12,10 @@ namespace Azure.DataApiBuilder.Config.Converters;
/// </summary>
internal class EntityCacheOptionsConverterFactory : JsonConverterFactory
{
// Determines whether to replace environment variable with its
// value or not while deserializing.
private bool _replaceEnvVar;

/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
@@ -21,11 +25,29 @@ public override bool CanConvert(Type typeToConvert)
/// <inheritdoc/>
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new EntityCacheOptionsConverter();
return new EntityCacheOptionsConverter(_replaceEnvVar);
}

/// <param name="replaceEnvVar">Whether to replace environment variable with its
/// value or not while deserializing.</param>
internal EntityCacheOptionsConverterFactory(bool replaceEnvVar)
{
_replaceEnvVar = replaceEnvVar;
}

private class EntityCacheOptionsConverter : JsonConverter<EntityCacheOptions>
{
// Determines whether to replace environment variable with its
// value or not while deserializing.
private bool _replaceEnvVar;

/// <param name="replaceEnvVar">Whether to replace environment variable with its
/// value or not while deserializing.</param>
public EntityCacheOptionsConverter(bool replaceEnvVar)
{
_replaceEnvVar = replaceEnvVar;
}

/// <summary>
/// Defines how DAB reads an entity's cache options and defines which values are
/// used to instantiate EntityCacheOptions.
@@ -40,11 +62,13 @@ private class EntityCacheOptionsConverter : JsonConverter<EntityCacheOptions>
// Defer to EntityCacheOptions record definition to define default ttl value.
int? ttlSeconds = null;

EntityCacheLevel? level = null;

while (reader.Read())
{
if (reader.TokenType is JsonTokenType.EndObject)
{
return new EntityCacheOptions(enabled, ttlSeconds);
return new EntityCacheOptions(enabled, ttlSeconds, level);
}

string? property = reader.GetString();
@@ -79,6 +103,15 @@ private class EntityCacheOptionsConverter : JsonConverter<EntityCacheOptions>
ttlSeconds = parseTtlSeconds;
}

break;
case "level":
if (reader.TokenType is JsonTokenType.Null)
{
throw new JsonException("level property cannot be null.");
}

level = EnumExtensions.Deserialize<EntityCacheLevel>(reader.DeserializeString(_replaceEnvVar)!);

break;
}
}
@@ -89,9 +122,9 @@ private class EntityCacheOptionsConverter : JsonConverter<EntityCacheOptions>

/// <summary>
/// When writing the EntityCacheOptions back to a JSON file, only write the ttl-seconds
/// property and value when EntityCacheOptions.Enabled is true. This avoids polluting
/// the written JSON file with a property the user most likely omitted when writing the
/// original DAB runtime config file.
/// and level properties and values when EntityCacheOptions.Enabled is true.
/// This avoids polluting the written JSON file with a property the user most likely
/// omitted when writing the original DAB runtime config file.
/// This Write operation is only used when a RuntimeConfig object is serialized to JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, EntityCacheOptions value, JsonSerializerOptions options)
@@ -105,6 +138,12 @@ public override void Write(Utf8JsonWriter writer, EntityCacheOptions value, Json
JsonSerializer.Serialize(writer, value.TtlSeconds, options);
}

if (value?.UserProvidedLevelOptions is true)
{
writer.WritePropertyName("level");
JsonSerializer.Serialize(writer, value.Level, options);
}

writer.WriteEndObject();
}
}
2 changes: 1 addition & 1 deletion src/Config/Converters/HostOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ private class HostOptionsConverter : JsonConverter<HostOptions>
/// <summary>
/// When writing the HostOptions back to a JSON file, only write the MaxResponseSizeMB property
/// if the property is user provided. This avoids polluting the written JSON file with a property
/// the user most likely ommitted when writing the original DAB runtime config file.
/// the user most likely omitted when writing the original DAB runtime config file.
/// This Write operation is only used when a RuntimeConfig object is serialized to JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, HostOptions value, JsonSerializerOptions options)
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.ObjectModel;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// Defines how DAB reads and writes a runtime cache options (JSON).
/// </summary>
internal class RuntimeCacheLevel2OptionsConverterFactory : JsonConverterFactory
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsAssignableTo(typeof(RuntimeCacheLevel2Options));
}

/// <inheritdoc/>
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new RuntimeCacheLevel2OptionsConverter();
}

private class RuntimeCacheLevel2OptionsConverter : JsonConverter<RuntimeCacheLevel2Options>
{
/// <summary>
/// Defines how DAB reads a runtime cache level2 options and defines which values are
/// used to instantiate RuntimeCacheLevel2Options.
/// </summary>
/// <exception cref="JsonException">Thrown when improperly formatted cache options are provided.</exception>
public override RuntimeCacheLevel2Options? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Remove the converter so we don't recurse.
JsonSerializerOptions jsonSerializerOptions = new(options);
jsonSerializerOptions.Converters.Remove(jsonSerializerOptions.Converters.First(c => c is RuntimeCacheLevel2OptionsConverterFactory));

RuntimeCacheLevel2Options? res = JsonSerializer.Deserialize<RuntimeCacheLevel2Options>(ref reader, jsonSerializerOptions);

// TODO: maybe add a check to ensure that the provider is valid?

return res;
}

/// <summary>
/// This Write operation is only used when a RuntimeConfig object is serialized to JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, RuntimeCacheLevel2Options value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteBoolean("enabled", value?.Enabled ?? false);

if (value is not null)
{
if (value.Provider is not null)
{
writer.WritePropertyName("provider");
JsonSerializer.Serialize(writer, value.Provider, options);
}

if (value.Partition is not null)
{
writer.WritePropertyName("partition");
JsonSerializer.Serialize(writer, value.Partition, options);
}

if (value.ConnectionString is not null)
{
writer.WritePropertyName("connection-string");
JsonSerializer.Serialize(writer, value.ConnectionString, options);
}
}

writer.WriteEndObject();
}
}
}
83 changes: 83 additions & 0 deletions src/Config/Converters/RuntimeCacheOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.ObjectModel;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// Defines how DAB reads and writes a runtime cache options (JSON).
/// </summary>
internal class RuntimeCacheOptionsConverterFactory : JsonConverterFactory
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsAssignableTo(typeof(RuntimeCacheOptions));
}

/// <inheritdoc/>
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new RuntimeCacheOptionsConverter();
}

private class RuntimeCacheOptionsConverter : JsonConverter<RuntimeCacheOptions>
{
/// <summary>
/// Defines how DAB reads a runtime cache options and defines which values are
/// used to instantiate RuntimeCacheOptions.
/// </summary>
/// <exception cref="JsonException">Thrown when improperly formatted cache options are provided.</exception>
public override RuntimeCacheOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Remove the converter so we don't recurse.
JsonSerializerOptions jsonSerializerOptions = new(options);
jsonSerializerOptions.Converters.Remove(jsonSerializerOptions.Converters.First(c => c is RuntimeCacheOptionsConverterFactory));

RuntimeCacheOptions? res = JsonSerializer.Deserialize<RuntimeCacheOptions>(ref reader, jsonSerializerOptions);

if (res is not null)
{
if (res.TtlSeconds <= 0)
{
throw new JsonException($"Invalid value for ttl-seconds: {res.TtlSeconds}. Value must be greater than 0.");
}
}

return res;
}

/// <summary>
/// When writing the RuntimeCacheOptions back to a JSON file, only write the ttl-seconds
/// property and value when RuntimeCacheOptions.Enabled is true. This avoids polluting
/// the written JSON file with a property the user most likely omitted when writing the
/// original DAB runtime config file.
/// This Write operation is only used when a RuntimeConfig object is serialized to JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, RuntimeCacheOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteBoolean("enabled", value?.Enabled ?? false);

if (value is not null)
{
if (value.UserProvidedTtlOptions is true)
{
writer.WritePropertyName("ttl-seconds");
JsonSerializer.Serialize(writer, value.TtlSeconds, options);
}

if (value.Level2 is not null)
{
writer.WritePropertyName("level-2");
JsonSerializer.Serialize(writer, value.Level2, options);
}
}

writer.WriteEndObject();
}
}
}
5 changes: 1 addition & 4 deletions src/Config/ObjectModel/Entity.cs
Original file line number Diff line number Diff line change
@@ -70,10 +70,7 @@ public Entity(
/// <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 &&
Cache.Enabled is true;
public bool IsCachingEnabled => Cache?.Enabled is true;

[JsonIgnore]
public bool IsEntityHealthEnabled =>
10 changes: 10 additions & 0 deletions src/Config/ObjectModel/EntityCacheLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Config.ObjectModel;

public enum EntityCacheLevel
{
L1,
L1L2
}
40 changes: 38 additions & 2 deletions src/Config/ObjectModel/EntityCacheOptions.cs
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
namespace Azure.DataApiBuilder.Config.ObjectModel;

/// <summary>
/// Entity specific in-memory cache configuration.
/// Entity specific cache configuration.
/// Properties are nullable to support DAB CLI merge config
/// expected behavior.
/// </summary>
@@ -18,6 +18,11 @@ public record EntityCacheOptions
/// </summary>
public const int DEFAULT_TTL_SECONDS = 5;

/// <summary>
/// Default ttl value for an entity.
/// </summary>
public const EntityCacheLevel DEFAULT_LEVEL = EntityCacheLevel.L1L2;

/// <summary>
/// Whether the cache should be used for the entity.
/// </summary>
@@ -30,9 +35,16 @@ public record EntityCacheOptions
[JsonPropertyName("ttl-seconds")]
public int? TtlSeconds { get; init; } = null;

/// <summary>
/// The cache levels to use for a cache entry.
/// </summary>
[JsonPropertyName("level")]
public EntityCacheLevel? Level { get; init; } = null;

[JsonConstructor]
public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null)
public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null, EntityCacheLevel? Level = null)
{
// TODO: shouldn't we apply the same "UserProvidedXyz" logic to Enabled, too?
this.Enabled = Enabled;

if (TtlSeconds is not null)
@@ -44,6 +56,16 @@ public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null)
{
this.TtlSeconds = DEFAULT_TTL_SECONDS;
}

if (Level is not null)
{
this.Level = Level;
UserProvidedLevelOptions = true;
}
else
{
this.Level = DEFAULT_LEVEL;
}
}

/// <summary>
@@ -59,4 +81,18 @@ public EntityCacheOptions(bool? Enabled = null, int? TtlSeconds = null)
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
[MemberNotNullWhen(true, nameof(TtlSeconds))]
public bool UserProvidedTtlOptions { get; init; } = false;

/// <summary>
/// Flag which informs CLI and JSON serializer whether to write ttl-seconds
/// property and value to the runtime config file.
/// When user doesn't provide the ttl-seconds property/value, which signals DAB to use the default,
/// the DAB CLI should not write the default value to a serialized config.
/// This is because the user's intent is to use DAB's default value which could change
/// and DAB CLI writing the property and value would lose the user's intent.
/// This is because if the user were to use the CLI created config, a ttl-seconds
/// property/value specified would be interpreted by DAB as "user explicitly set ttl."
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
[MemberNotNullWhen(true, nameof(Level))]
public bool UserProvidedLevelOptions { get; init; } = false;
}
Loading