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

Conversation

aaronburtle
Copy link
Contributor

@aaronburtle aaronburtle commented Jan 18, 2024

Why make this change?

In order to better facilitate Hot Reload, we need to split the responsibility for loading and accessing the RuntimeConfig between the two different scenarios that our service can run within, local and hosted. We already have a FileSystemRuntimeConfigLoader which handles the loading of the config from a local file system. This refactor naturally extends that approach so that we can Hot Reload both in the local and eventually, the hosted scenario.

We also make some necessary changes for hot reloading to work within the service.

What is this change?

As a part of the changes, we split the RuntimeConfigProvider into a LocalRuntimeConfigProvider and a HostedRuntimeConfigProvider. These classes then implement the IRuntimeConfigProvider interface in the way that makes sense for their scenario.

Rather than having these providers storing the internal copy of the RuntimeConfig, the natural approach is for the loaders to be responsible for maintaining this internal representation, and for the loaders to handle all of the loading, including any hot reloading that takes place for that RuntimeConfig.

This was previously not possible, because we could not store a copy of the RuntimeConfig with the loader due to circular dependency. This refactor removes that issue as part of its changes. We then move the internally stored RuntimeConfig to these loaders, as well as all of the logic for Hot Reloading. Since we are only handling Hot Reloading in the local scenario to being, we only have the loader for the local scenario currently setup to do this. When we extend Hot Reloading to the hosted scenario, that will mean adding a loader that can handle that specific logic in a hosted scenario.

We also change the way that the default data source names are generated so that this is deterministic. This is a requirement to pass validation during time of handling requests. In this case we are taking a hash of the connection string, though this means to hot reload the connection string we will need a new approach.

Within the tests, there was an attempt to use the LocalRuntimeConfigProvider when possible, since testing is naturally happening in a local scenario. However, some tests utilize functions that are only relevant for the HostedRuntimeConfigProvider and so in those cases, we used that class. This created some issues, and workarounds were found for these tests.

How was this tested?

Current test suite.

Sample Request(s)

Any valid request.

/// </summary>
public RuntimeConfigLoader ConfigLoader { get; set; }

public delegate Task<bool> RuntimeConfigLoadedHandler(IRuntimeConfigProvider sender, RuntimeConfig config);
Copy link
Contributor

Choose a reason for hiding this comment

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

add comment


public delegate Task<bool> RuntimeConfigLoadedHandler(IRuntimeConfigProvider sender, RuntimeConfig config);

public List<RuntimeConfigLoadedHandler> RuntimeConfigLoadedHandlers { get; }
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 what the handlers are for and why a list of handlers is needed

/// 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; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

as you were refactoring, did you identify whether there may be a more appropriate place for ManagedIdentityAccessToken and TrySetAccesstoken as they don't seem appropriate to be in this interface.

@@ -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.

@@ -54,7 +54,10 @@ public class MySqlQueryExecutorUnitTests
MockFileSystem fileSystem = new();
fileSystem.AddFile(FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME, new MockFileData(mockConfig.ToJson()));
FileSystemRuntimeConfigLoader loader = new(fileSystem);
RuntimeConfigProvider provider = new(loader);
HostedRuntimeConfigProvider provider = new(loader)
Copy link
Contributor

Choose a reason for hiding this comment

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

How are you determining whether to use Local/Hosted RuntimeConfigProvider?


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.

Comment on lines 18 to 19
/// 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?

Comment on lines +21 to +22
/// This class should be treated as the sole accessor of the config that is available within the service, and other classes
/// should not access the config directly, or maintain a reference to it, so that we can do hot-reloading by replacing
Copy link
Contributor

Choose a reason for hiding this comment

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

By 'this class' do you mean both Local/HostedRuntimeConfigProvider classes? if so, would need to clarify the comment

/// </remarks>
public class LocalRuntimeConfigProvider : IRuntimeConfigProvider
{
public bool IsLateConfigured { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this just return false?

src/Config/FileSystemRuntimeConfigLoader.cs Outdated Show resolved Hide resolved
if (ConfigLoader.RuntimeConfig is null)
{
ConfigLoader.TryLoadKnownConfig(out _, replaceEnvVar: true);

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove extra line

@@ -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);
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.

Copy link
Contributor

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

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

Posting comments so far

@@ -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.

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?

src/Config/ConfigFileWatcher.cs Outdated Show resolved Hide resolved
@@ -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?

src/Config/FileSystemRuntimeConfigLoader.cs Show resolved Hide resolved
/// As opposed to using a json input and regenerating the runtimconfig, it sets the access token for the current runtimeConfig on the provider.
/// </summary>
public bool TrySetAccesstoken(
string? accessToken,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need all kinds of RuntimeConfigProvider to implement the method to TrySetAccessToken?

Suggested change
string? accessToken,
string? accessToken,

Copy link
Contributor

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

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

Good starter, but needs clarity on when would HostedRuntimeConfigProvider will be created so that previous scenarios are not broken.


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.

@@ -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?

@@ -79,7 +79,7 @@ public void ConfigureServices(IServiceCollection services)
null);
IFileSystem fileSystem = new FileSystem();
FileSystemRuntimeConfigLoader configLoader = new(fileSystem, configFileName, connectionString);
RuntimeConfigProvider configProvider = new(configLoader);
IRuntimeConfigProvider configProvider = new LocalRuntimeConfigProvider(configLoader);

services.AddSingleton(fileSystem);
services.AddSingleton(configProvider);
Copy link
Contributor

Choose a reason for hiding this comment

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

where is the hosted runtime config provider added to the set of services? If its not added, how will our late configured scenarios work?

@@ -403,7 +403,7 @@ public static ILoggerFactory CreateLoggerFactoryForHostedAndNonHostedScenario(IS
// attempt to get the runtime config to determine the loglevel based on host.mode.
// If runtime config is available, set the loglevel to Error if host.mode is Production,
// Debug if it is Development.
RuntimeConfigProvider configProvider = serviceProvider.GetRequiredService<RuntimeConfigProvider>();
IRuntimeConfigProvider configProvider = serviceProvider.GetRequiredService<IRuntimeConfigProvider>();
if (configProvider.TryGetConfig(out 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.

For hosted scenarios, the configProvider thats picked up here, should be HostedRuntimeConfigProvider. But the implementation of TryGetConfig is not provided for that provider - so this will throw an exception. We should implement TryGetConfig even for HostedRuntimeConfigProvider

@@ -917,7 +917,7 @@ public async Task TestSettingConfigurationCreatesCorrectClasses(string configura
Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode);

ValidateCosmosDbSetup(server);
RuntimeConfigProvider configProvider = server.Services.GetService<RuntimeConfigProvider>();
IRuntimeConfigProvider configProvider = server.Services.GetService<IRuntimeConfigProvider>();

Assert.IsNotNull(configProvider, "Configuration Provider shouldn't be null after setting the configuration at runtime.");
Assert.IsTrue(configProvider.TryGetConfig(out RuntimeConfig configuration), "TryGetConfig should return true when the config is set.");
Copy link
Contributor

Choose a reason for hiding this comment

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

This assert should fail since the configProvider in this test should result in hosted config provider that doesnt have the TryGetConfig method implemented

@@ -169,18 +169,18 @@ public RuntimeConfig(string? Schema, DataSource DataSource, RuntimeEntities Enti
this.DataSource = DataSource;
this.Runtime = Runtime;
this.Entities = Entities;
_defaultDataSourceName = Guid.NewGuid().ToString();
DefaultDataSourceName = Guid.NewGuid().ToString(); //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.

commented code

Copy link
Contributor

Choose a reason for hiding this comment

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

also seems inconsistent where all other members are set using this.Member = Parameter

@@ -130,7 +130,7 @@ public bool AllowIntrospection
}
}

private string _defaultDataSourceName;
public string DefaultDataSourceName;
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be only public member not following getter setter syntax. { get; set; } any reason not to?

{
_configProvider.HotReloadConfig();
_configLoader.HotReloadConfig(_configLoader.RuntimeConfig.DefaultDataSourceName);
Copy link
Contributor

Choose a reason for hiding this comment

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

may be helpful to print/log that hot reloading was skipped because config definded production mode.

@@ -31,14 +38,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

Comment on lines +144 to +146
bool configParsed = TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar);
TrySetupConfigFileWatcher();
config = RuntimeConfig;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify these few lines here?

Why would we still attempt to execute TrySetupConfigFileWatcher() if configParsed is false indicating some failure?

Also, the line config=Runtimeconfig wouldn't work or shouldn't occur if configParsed is false.

/// Hot Reloads the runtime config when the file watcher
/// is active and detects a change to the underlying config file.
/// </summary>
public void HotReloadConfig(string defaultDataSourceName)
Copy link
Contributor

Choose a reason for hiding this comment

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

why is the param named defaultDataSourceName? what happens when the value is not from the default data source and, instead, from a different data source?

@@ -22,6 +22,8 @@ public abstract class RuntimeConfigLoader
{
protected readonly string? _connectionString;

public RuntimeConfig? RuntimeConfig { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

should this class be renamed to RuntimeConfigLoaderBase?

/// </summary>
/// <param name="runtimeConfig">Populated runtime configuration, if present.</param>
/// <returns>True when runtime config is provided, otherwise false.</returns>
// Not currently used in hosted scenario, but needed for interface
Copy link
Contributor

Choose a reason for hiding this comment

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

comments need to explain why:
TryGetConfig([NotNullWhen(true)] out RuntimeConfig? runtimeConfig)
public bool TryGetLoadedConfig([NotNullWhen(true)] out RuntimeConfig? runtimeConfig)

are not used in hosted scenario. It doesn't make sense to me that these two methods are promoted to the interface if they aren't used in implementating classes.

Comment on lines +203 to +218
//public async Task<bool> InvokeConfigLoadedHandlersAsync()
//{
// List<Task<bool>> configLoadedTasks = new();
// if (_runtimeConfig is not null)
// {
// foreach (RuntimeConfigLoadedHandler configLoadedHandler in RuntimeConfigLoadedHandlers)
// {
// configLoadedTasks.Add(configLoadedHandler(this, _runtimeConfig));
// }
// }

// bool[] results = await Task.WhenAll(configLoadedTasks);

// // Verify that all tasks succeeded.
// return results.All(x => x);
//}
Copy link
Contributor

Choose a reason for hiding this comment

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

commented code.

@Aniruddh25 Aniruddh25 modified the milestones: 0.11rc, 0.12rc Mar 7, 2024
@seantleonard seantleonard modified the milestones: 0.12rc, 1.2rc Mar 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants