TransWikia.com

Reload configs in memory basis on a trigger efficiently

Code Review Asked by dragons on October 27, 2021

(following up from here)

I am working on a little project where I need to scan all the config files present in a folder on the disk and load it in memory. Below are the steps:

  • On the disk there is already a default Records folder which has all the default config files present. This is to fallback if loadDefaultFlag is enabled. We will never overwrite this config or delete at all.
  • There are also new config files present as a tar.gz file (max 100 MB size) in a remote url location which I need to download and store it on disk in secondary location only if loadDefaultFlag is disabled.

During Server startup

During server startup I need to either load local files from default Records folder or remote files by downloading it from remote server (and storing it in new secondary location) and then using it in memory.

{"loadDefaultFlag":"false", "remoteFileName":"abc-123.tgz", "reload":"false"}

For example: server started up and loaded abc-123.tgz config in memory.

After server startup

Case 1:

After server started up with some configs (abc-123.tgz) then someone from outside can tell us to download new configs from remote location again or go back and use default local configs from Records folder.

{"loadDefaultFlag":"true", "remoteFileName":"abc-123.tgz", "reload":"false"}

If loadDefaultFlag is true, then it means someone is telling from outside to load configs from default Records folder in memory so once this is changed, all machines will switch to use local configs in memory.

Case 2:

Second case can be someone telling to download new remote configs as we have new configs available that we should use now.

{"loadDefaultFlag":"false", "remoteFileName":"abc-124.tgz", "reload":"false"}

so now all machines will download abc-124.tgz onto the disk but they won’t switch to these new configs yet in memory unless someone is instructing them from outside to start using new configs in memory. Save method actually switches config in memory from old to new. And that flag to switch to new config is reload – once that is true then all machines will switch to use new abc-124.tgz configs in memory.

Records folder which has default configs is just a backup and not meant to be used in regular cases.

Below is my code:

public class RecordManager
{
    private const string _remoteUrl = "remote-url-from-where-to-download-new-configs";
    private static string _remoteFileName; 

    private const string SecondaryLocation = "SecondaryConfigs";
    private readonly IConfiguration _configuration;
    private readonly string _localPath; 
    private IEnumerable<RecordHolder> _records; 
    
    public enum ConfigLocation { System, Local, Remote }
    
    public RecordManager(IConfiguration configuration, string localPath)
    {
        if(configuration == null) { throw new ArgumentNullException(nameof(configuration)); }
        
        if(localPath?.Length == 0) { throw new ArgumentNullException(nameof(localPath)); }
        
        _localPath = localPath;
        _configuration = configuration;
        ChangeToken.OnChange(configuration.GetReloadToken, _ => ConfigChanged(), new object());
    }
    
    public RecordManager(IConfiguration configuration) : this(configuration, "Records") { } 
    
    public RecordManager LoadConfigurationsFrom(ConfigLocation location)
    {
        switch(location)
        {
            case ConfigLocation.Remote:
                _records = GetConfigFromServer();
                break; 
            case ConfigLocation.Local:
                _records = GetConfigFromLocalFiles();
                break; 
            case ConfigLocation.System:
                _records = IsConfigFromServer() ?  GetConfigFromServer() : GetConfigFromLocalFiles();
                break;  
        }
        
        return this; 
    }
    
    public void Save()
    {
        // now load `_records` configs in memory here
        // only called once you are ready to switch
    }

    private bool IsConfigFromServer()
    {
        string configValue = configuration["configKey"];

        if (string.IsNullOrWhiteSpace(configValue)){ return false; }
        
        var dcc = JsonConvert.DeserializeObject<RecordPojo>(configValue);
        if(!bool.TryParse(dcc.loadDefaultFlag?.ToString(), out bool loadDefaultFlag)) { return false; }
        
        _remoteFileName = dcc.remoteFileName;
        
        return !loadDefaultFlag && !string.IsNullOrWhiteSpace(dcc.remoteFileName);
    }
    
    // download tar.gz file from remote server, store it on disk in secondary location
    // uncompress tar.gz file, read it and return RecordHolder list back.
    private IEnumerable<RecordHolder> GetConfigFromServer()
    {       
       var isDownloaded = _fileHelper.Download($"{_remoteUrl}{_remoteFileName}", _secondaryLocation);
        if(!isDownloaded) { yield return default; }
        
        var isExtracted = _fileHelper.ExtractTarGz(_remoteFileName, _directory);
        if(!isExtracted) { yield return default; }
        
        foreach(var configPath in _fileHelper.GetFiles(directory))
        {
            if(!File.Exists(configPath)) { continue; }
            
            var fileDate = File.GetLastWriteTimeUtc(configPath);
            var fileContent = File.ReadAllText(configPath);
            var pathPieces = configPath.Split(System.IO.Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
            var fileName = pathPieces[pathPieces.Length - 1];
            yield return new RecordHolder
            {
                Name = fileName,
                Date = fileDate,
                JDoc = fileContent
            };
        }
    }


    private IEnumerable<RecordHolder> GetConfigFromLocalFiles()
    {
        // read config files already present in default "Records" folder
        // and return RecordHolder list back.
    }

    // this can be improved a lot to achieve below cases in proper way
    private void ConfigChanged()
    {
        string configValue = _configuration["configKey"];
        if (string.IsNullOrWhiteSpace(configValue)) { return; }

        var dcc = JsonConvert.DeserializeObject<ConsulConfig>(configValue);
        bool.TryParse(dcc.loadDefaultFlag?.ToString(), out bool loadDefaultFlag);
        bool.TryParse(dcc.reloadConfig?.ToString(), out bool reloadConfig);
        _remoteFileName = dcc.remoteFileName;

        if (switchConfig) { Save(); }
        if (loadDefaultFlag) { _records = GetConfigFromLocalFiles(); }
        else { _records = GetConfigFromServer(); }
    }
}

This is how I use it as a fluent api and during server startup this will be called as it is:

new RecordManager(configuration)
    .LoadConfigurationsFrom(RecordManager.ConfigLocation.Remote)
    .Save();

Question:

Now as you can see I have a ChangeToken.OnChange notification enabled in my constructor where I need to do something whenever my configuration (configKey) is changed and it will invoke my ConfigChanged method. Basically after server startup is done and configs is loaded up in memory with above code then someone can tell us to download new configs again and then load it in memory and thats what I do in ConfigChanged method.

Opting for a code review here specifically for the case when I need to reload configs again and load it in memory. I am specifically interested in the way I have designed and implemented my code for ConfigChanged method. I am sure there must be a better way to rewrite the ConfigChanged method code in a better way that can handle all those above cases efficiently.

One Answer

The Records is a backup configuration that would be used if there is any issues on the configuration.

What I think you need is the following workflow:

  1. On server startup, read the JSON configuration, download the file, and store JSON values statically.
  2. After server started up, if the JSON values have been changed, get the new values, compared them with the stored values, and execute the logic based on this comparison.

So, to prepare that we need to add a child private class which would store the json config values. Next, add static instance of this new private class which would store the current settings and On the ConfigChanged just compare between the new file name and the current one. Then, just load the settings from local or server or return the default values.

You need a separate method for loading the Default settings (which is the backup). So, at the end you'll have three methods for loading the configurations.

here is the changes you need (I have optout the rest of the code only included the changes).

public class RecordManager
{
    private static JsonConfiguation _jsonConfig; 

    private class JsonConfiguation
    {
        public string RemoteFileName { get; set; }

        public bool LoadDefault { get; set; }

        public bool Reload { get; set; }

        public bool HasNewerFile(JsonConfiguation jsonConfiguation)
        {
            return !RemoteFileName.Equals(jsonConfiguation.RemoteFileName, StringComparison.InvariantCultureIgnoreCase);
        }

        public bool IsConfigFromServer => !LoadDefault && !string.IsNullOrWhiteSpace(RemoteFileName);
    }


    
    public RecordManager(IConfiguration configuration, string localPath)
    {
        if(configuration == null) { throw new ArgumentNullException(nameof(configuration)); }
        
        if(localPath?.Length == 0) { throw new ArgumentNullException(nameof(localPath)); }
        
        _localPath = localPath;
        
        _configuration = configuration;
        
        if(_jsonConfig == null)
            _jsonConfig = GetConfigValuesFromJson();

        ChangeToken.OnChange(configuration.GetReloadToken, _ => ConfigChanged(), new object());
    }   

    private JsonConfiguation GetConfigValuesFromJson()
    {
        string configValue = _configuration["configKey"];
        
        if (string.IsNullOrWhiteSpace(configValue)) { throw new ArgumentNullException(nameof(configValue)); }

        var dcc = JsonConvert.DeserializeObject<ConsulConfig>(configValue);
        
        return new JsonConfiguation
        {
            RemoteFileName = dcc.remoteFileName, 
            LoadDefault = bool.TryParse(dcc.loadDefaultFlag?.ToString(), out bool loadDefaultFlag) ? loadDefaultFlag : false, 
            Reload = bool.TryParse(dcc.reloadConfig?.ToString(), out bool reloadConfig) ? reloadConfig : false
        };
    }

    
    private void ConfigChanged()
    {
        var configNew = GetConfigValuesFromJson();

        // fallback in case if something happened unexpectedly. 
        if (_jsonConfig == null)
        {
            _jsonConfig = configNew;
        }
       
        if(configNew.IsConfigFromServer)
        {
            // if both (the current downloaded and on the remote) are different, 
            // Redownload the file before going to the next step.
            // else just load the local config 

            _records = _jsonConfig.HasNewerFile(configNew) ? GetConfigFromServer() : GetConfigFromLocalFiles();
            _jsonConfig = configNew;
        }
        else
        {
            // here it will cover if the loadDefaultFlag is true or any other issue with the configuration (like missing values)
            // it will reload the default configuration (as a reset switch). 
            _records = GetDefaultConfiguration();
            _jsonConfig = configNew;
        }


        // if it requires to reload the configuration immediately
        // if not, it'll now reload the configuration, and it would be stored in this instance.
        if (configNew.Reload)
        {
            Save();
        }

    }

    private IEnumerable<RecordHolder> GetDefaultConfiguration()
    {
        // get the default config files already present in default "Records" folder
        // and return RecordHolder list back.
    }   

    private IEnumerable<RecordHolder> GetConfigFromServer()
    {
        // get the config files from the server 
        // and return RecordHolder list back.       
    }
    
    
    private IEnumerable<RecordHolder> GetConfigFromLocalFiles()
    {
        // get the config files from the secondary location 
        // and return RecordHolder list back.
    }               
}

Answered by iSR5 on October 27, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP