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
base: main
Are you sure you want to change the base?
Conversation
/// </summary> | ||
public RuntimeConfigLoader ConfigLoader { get; set; } | ||
|
||
public delegate Task<bool> RuntimeConfigLoadedHandler(IRuntimeConfigProvider sender, RuntimeConfig config); |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
/// 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. |
There was a problem hiding this comment.
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?
/// 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 |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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?
if (ConfigLoader.RuntimeConfig is null) | ||
{ | ||
ConfigLoader.TryLoadKnownConfig(out _, replaceEnvVar: true); | ||
|
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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/RuntimeConfigLoader.cs
Outdated
@@ -22,6 +22,8 @@ public abstract class RuntimeConfigLoader | |||
{ | |||
protected readonly string? _connectionString; | |||
|
|||
public RuntimeConfig? RuntimeConfig; |
There was a problem hiding this comment.
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?
/// 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, |
There was a problem hiding this comment.
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
?
string? accessToken, | |
string? accessToken, |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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."); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commented code
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
bool configParsed = TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar); | ||
TrySetupConfigFileWatcher(); | ||
config = RuntimeConfig; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
//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); | ||
//} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commented code.
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 aFileSystemRuntimeConfigLoader
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 aHostedRuntimeConfigProvider
. These classes then implement theIRuntimeConfigProvider
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 thatRuntimeConfig
.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 storedRuntimeConfig
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 theHostedRuntimeConfigProvider
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.