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

Refactor RuntimeConfig into Interface with Local and Hosted Providers #1981

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ public static bool IsConfigValid(ValidateOptions options, FileSystemRuntimeConfi
_logger.LogInformation("Loaded config file: {runtimeConfigFile}", runtimeConfigFile);
}

RuntimeConfigProvider runtimeConfigProvider = new(loader);
LocalRuntimeConfigProvider runtimeConfigProvider = new(loader);

ILogger<RuntimeConfigValidator> runtimeConfigValidatorLogger = LoggerFactoryForCli.CreateLogger<RuntimeConfigValidator>();
RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, runtimeConfigValidatorLogger, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Core.Configurations;
namespace Azure.DataApiBuilder.Config;

public class ConfigFileWatcher
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
{
private FileSystemWatcher? _fileWatcher;
private readonly RuntimeConfigProvider? _configProvider;
private FileSystemRuntimeConfigLoader _configLoader;

public ConfigFileWatcher(RuntimeConfigProvider configProvider, string directoryName, string configFileName)
public ConfigFileWatcher(FileSystemRuntimeConfigLoader configLoader, string directoryName, string configFileName)
{
_fileWatcher = new FileSystemWatcher(directoryName)
{
Filter = configFileName,
EnableRaisingEvents = true
};

_configProvider = configProvider;
_configLoader = configLoader;
_fileWatcher.Changed += OnConfigFileChange;
}

Expand All @@ -31,14 +31,19 @@ private void OnConfigFileChange(object sender, FileSystemEventArgs e)
{
try
{
if (_configProvider is null)
if (_configLoader is null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

summary comment needs completion for params.

also nit in the summary: this trigger function -> this event handler

{
throw new ArgumentNullException("_configProvider can not be null.");
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
}

if (!_configProvider.IsLateConfigured && _configProvider.GetConfig().IsDevelopmentMode())
if (_configLoader.RuntimeConfig is null)
{
_configProvider.HotReloadConfig();
throw new ArgumentNullException("RuntimeConfig can not be null.");
}

if (_configLoader.RuntimeConfig.IsDevelopmentMode())
{
_configLoader.HotReloadConfig();
}
}
catch (Exception ex)
Expand Down
42 changes: 41 additions & 1 deletion src/Config/FileSystemRuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class FileSystemRuntimeConfigLoader : RuntimeConfigLoader
// or user provided config file which could be a relative file path, absolute file path or simply the file name assumed to be in current directory.
private string _baseConfigFilePath;

private ConfigFileWatcher? _configFileWatcher;

private readonly IFileSystem _fileSystem;

public const string CONFIGFILE_NAME = "dab-config";
Expand Down Expand Up @@ -87,6 +89,32 @@ public string GetConfigFileName()
return configFileName;
}

/// <summary>
/// Checks if we have already attempted to configure the file watcher, if not
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
/// instantiate the file watcher if we are in the correct mode.
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
/// Returns true if we instantiate a file watcher.
/// </summary>
private bool TrySetupConfigFileWatcher()
{
if (RuntimeConfig is not null && RuntimeConfig.IsDevelopmentMode())
{
try
{
_configFileWatcher = new(this, GetConfigDirectoryName(), GetConfigFileName());
}
catch (Exception ex)
{
// Need to remove the dependencies in startup on the RuntimeConfigProvider
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
// before we can have an ILogger here.
Console.WriteLine($"Attempt to configure config file watcher for hot reload failed due to: {ex.Message}.");
}

return _configFileWatcher is not null;
}

return false;
}

/// <summary>
/// Load the runtime config from the specified path.
/// </summary>
Expand All @@ -104,7 +132,10 @@ public string GetConfigFileName()
{
Console.WriteLine($"Loading config file from {path}.");
string json = _fileSystem.File.ReadAllText(path);
return TryParseConfig(json, out config, connectionString: _connectionString, replaceEnvVar: replaceEnvVar);
bool res = TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar);
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@ayush3797 ayush3797 Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something changed in this PR: In the TryParseConfig() method, the order of parameters in the summary is messed up. Could you please fix that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function below? Seems correct to me.

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps Ayush meant that most other methods in our project have the out parameter as the last parameter in the method signature.

TrySetupConfigFileWatcher();
config = RuntimeConfig;
return res;
}
else
{
aaronburtle marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -129,6 +160,15 @@ public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? c
return TryLoadConfig(ConfigFilePath, out config, replaceEnvVar);
}

/// <summary>
/// Hot Reloads the runtime config when the file watcher
/// is active and detects a change to the underlying config file.
/// </summary>
public void HotReloadConfig()
{
TryLoadKnownConfig(out _, replaceEnvVar: true);
}

/// <summary>
/// Precedence of environments is
/// 1) Value of DAB_ENVIRONMENT.
Expand Down
2 changes: 1 addition & 1 deletion src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti
this.DataSource = DataSource;
this.Runtime = Runtime;
this.Entities = Entities;
_defaultDataSourceName = Guid.NewGuid().ToString();
_defaultDataSourceName = DataSource.ConnectionString.GetHashCode().ToString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have in your pr description or here as a comment why hash code? vs guid?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says This is a requirement to pass validation during time of handling requests.. What kind of validation are we talking here? Could you please elaborate?

Also, will this still be needed if we now have mutable records? Can we retain the originally generated Guid and modify only parts that changed in the hot reload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can pass the Guid along through the hot reload function and then overwrite the RuntimeConfig's default data source name with that passed along Guid if it exists. I updated this, so we can avoid using the connection string's hash.


// we will set them up with default values
_dataSourceNameToDataSource = new Dictionary<string, DataSource>
Expand Down
2 changes: 2 additions & 0 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public abstract class RuntimeConfigLoader
{
protected readonly string? _connectionString;

public RuntimeConfig? RuntimeConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: did you mean to add this as a property or a field?


public RuntimeConfigLoader(string? connectionString = null)
{
_connectionString = connectionString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class SimulatorAuthenticationHandler : AuthenticationHandler<Authenticati
/// <param name="encoder">URL encoder.</param>
/// <param name="clock">System clock.</param>
public SimulatorAuthenticationHandler(
RuntimeConfigProvider runtimeConfigProvider,
IRuntimeConfigProvider runtimeConfigProvider,
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class ClientRoleHeaderAuthenticationMiddleware

public ClientRoleHeaderAuthenticationMiddleware(RequestDelegate next,
ILogger<ClientRoleHeaderAuthenticationMiddleware> logger,
RuntimeConfigProvider runtimeConfigProvider)
IRuntimeConfigProvider runtimeConfigProvider)
{
_nextMiddleware = next;
_logger = logger;
Expand Down
6 changes: 3 additions & 3 deletions src/Core/Authorization/AuthorizationResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace Azure.DataApiBuilder.Core.Authorization;
/// </summary>
public class AuthorizationResolver : IAuthorizationResolver
{
private readonly RuntimeConfigProvider _runtimeConfigProvider;
private readonly IRuntimeConfigProvider _runtimeConfigProvider;
private readonly IMetadataProviderFactory _metadataProviderFactory;
public const string WILDCARD = "*";
public const string CLAIM_PREFIX = "@claims.";
Expand All @@ -36,7 +36,7 @@ public class AuthorizationResolver : IAuthorizationResolver
public Dictionary<string, EntityMetadata> EntityPermissionsMap { get; private set; } = new();

public AuthorizationResolver(
RuntimeConfigProvider runtimeConfigProvider,
IRuntimeConfigProvider runtimeConfigProvider,
IMetadataProviderFactory metadataProviderFactory
)
{
Expand All @@ -48,7 +48,7 @@ IMetadataProviderFactory metadataProviderFactory
}
else
{
runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((RuntimeConfigProvider sender, RuntimeConfig config) =>
runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((IRuntimeConfigProvider sender, RuntimeConfig config) =>
{
SetEntityPermissionMap(config);
return Task.FromResult(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,34 @@
using Azure.DataApiBuilder.Config.NamingPolicies;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Exceptions;
using static Azure.DataApiBuilder.Core.Configurations.IRuntimeConfigProvider;

namespace Azure.DataApiBuilder.Core.Configurations;

/// <summary>
/// This class is responsible for exposing the runtime config to the rest of the service.
/// The <c>RuntimeConfigProvider</c> won't directly load the config, but will instead rely on the <see cref="FileSystemRuntimeConfigLoader"/> to do so.
/// This class is responsible for exposing the runtime config to the rest of the service when in a hosted scenario.
/// The <c>HostedRuntimeConfigProvider</c> won't directly load the config, but will instead rely on the <see cref="FileSystemRuntimeConfigLoader"/> to do so.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description should explain what is unique about this runtimeconfigprovider. Why/how does it enable DAB to function appropriately in a hosted environment?

/// </summary>
/// <remarks>
/// The <c>RuntimeConfigProvider</c> will maintain internal state of the config, and will only load it once.
/// The <c>HostedRuntimeConfigProvider</c> will maintain internal state of the config, and will only load it once.
///
/// This class should be treated as the owner of the config that is available within the service, and other classes
/// should not load the config directly, or maintain a reference to it, so that we can do hot-reloading by replacing
/// the config that is available from this type.
/// </remarks>
public class RuntimeConfigProvider
public class HostedRuntimeConfigProvider : IRuntimeConfigProvider
{
public delegate Task<bool> RuntimeConfigLoadedHandler(RuntimeConfigProvider sender, RuntimeConfig config);

public List<RuntimeConfigLoadedHandler> RuntimeConfigLoadedHandlers { get; } = new List<RuntimeConfigLoadedHandler>();

/// <summary>
/// Indicates whether the config was loaded after the runtime was initialized.
/// </summary>
/// <remarks>This is most commonly used when DAB's config is provided via the <c>ConfigurationController</c>, such as when it's a hosted service.</remarks>
public bool IsLateConfigured { get; set; }

/// <summary>
/// The access tokens representing a Managed Identity to connect to the database.
/// The key is the unique datasource name and the value is the access token.
/// </summary>
public Dictionary<string, string?> ManagedIdentityAccessToken { get; private set; } = new Dictionary<string, string?>();

public RuntimeConfigLoader ConfigLoader { get; private set; }
public Dictionary<string, string?> ManagedIdentityAccessToken { get; set; } = new Dictionary<string, string?>();

private ConfigFileWatcher? _configFileWatcher;
public RuntimeConfigLoader ConfigLoader { get; set; }

private RuntimeConfig? _runtimeConfig;
public RuntimeConfig? _runtimeConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why make change this field from private to public? we already have accessor methods like TryGetConfig TryGetLoaded config etc. which provide access to the runtime config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were tests that in order to make work with the new refactor need these to be public for the mocking that was required. I could not find a straight forward way to get those tests to work without making this public, but perhaps we could sync on this.


public RuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader)
public HostedRuntimeConfigProvider(RuntimeConfigLoader runtimeConfigLoader)
{
ConfigLoader = runtimeConfigLoader;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the runtimeConfigLoader in case of HostedRuntimeConfigProvider?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used for testing because our tests are using the initialize function, which is for the hosted scenario. I am looking into how to refactor the tests to eliminate this, but the answer is not obvious.

}
Expand All @@ -66,93 +55,23 @@ public RuntimeConfig GetConfig()
{
return _runtimeConfig;
}

// While loading the config file, replace all the environment variables with their values.
if (ConfigLoader.TryLoadKnownConfig(out RuntimeConfig? config, replaceEnvVar: true))
{
_runtimeConfig = config;
TrySetupConfigFileWatcher();
}

if (_runtimeConfig is null)
else
{
throw new DataApiBuilderException(
message: "Runtime config isn't setup.",
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
}

return _runtimeConfig;
}

/// <summary>
/// Checks if we have already attempted to configure the file watcher, if not
/// instantiate the file watcher if we are in the correct scenario. If we
/// are not in the correct scenario, do not setup a file watcher but remember
/// that we have attempted to do so to avoid repeat checks in future calls.
/// Returns true if we instantiate a file watcher.
/// </summary>
private bool TrySetupConfigFileWatcher()
{
if (!IsLateConfigured && _runtimeConfig is not null && _runtimeConfig.IsDevelopmentMode())
{
try
{
FileSystemRuntimeConfigLoader loader = (FileSystemRuntimeConfigLoader)ConfigLoader;
_configFileWatcher = new(this, loader.GetConfigDirectoryName(), loader.GetConfigFileName());
}
catch (Exception ex)
{
// Need to remove the dependencies in startup on the RuntimeConfigProvider
// before we can have an ILogger here.
Console.WriteLine($"Attempt to configure config file watcher for hot reload failed due to: {ex.Message}.");
}

return _configFileWatcher is not null;
}

return false;
}

/// <summary>
/// Attempt to acquire runtime configuration metadata.
/// </summary>
/// <param name="runtimeConfig">Populated runtime configuration, if present.</param>
/// <returns>True when runtime config is provided, otherwise false.</returns>
public bool TryGetConfig([NotNullWhen(true)] out RuntimeConfig? runtimeConfig)
{
if (_runtimeConfig is null)
{
if (ConfigLoader.TryLoadKnownConfig(out RuntimeConfig? config, replaceEnvVar: true))
{
_runtimeConfig = config;
TrySetupConfigFileWatcher();
}
}

runtimeConfig = _runtimeConfig;
return _runtimeConfig is not null;
throw new NotImplementedException();
}

/// <summary>
/// Attempt to acquire runtime configuration metadata from a previously loaded one.
/// This method will not load the config if it hasn't been loaded yet.
/// </summary>
/// <param name="runtimeConfig">Populated runtime configuration, if present.</param>
/// <returns>True when runtime config is provided, otherwise false.</returns>
public bool TryGetLoadedConfig([NotNullWhen(true)] out RuntimeConfig? runtimeConfig)
{
runtimeConfig = _runtimeConfig;
return _runtimeConfig is not null;
}

/// <summary>
/// Hot Reloads the runtime config when the file watcher
/// is active and detects a change to the underlying config file.
/// </summary>
public void HotReloadConfig()
{
ConfigLoader.TryLoadKnownConfig(out _runtimeConfig, replaceEnvVar: true);
throw new NotImplementedException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need comment to explain why not implemented

}

/// <summary>
Expand Down Expand Up @@ -278,7 +197,7 @@ public void HotReloadConfig()
return false;
}

private async Task<bool> InvokeConfigLoadedHandlersAsync()
public async Task<bool> InvokeConfigLoadedHandlersAsync()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this function in HostedRuntimeConfigProvider?

{
List<Task<bool>> configLoadedTasks = new();
if (_runtimeConfig is not null)
Expand Down