0

I have a rest api calls via I get a config. I cached it by semaphore right now but problem can arrive in case the api is not available.

My code is following:

    public async Task<Config> Config(CancellationToken ct = default) { 
        using (var releaser = await _asyncSemaphore.EnterAsync(ct))
        {
            if (_serverConfig != null) return _serverConfig;
            _serverConfig = await _restApi.GetConfig(ct).ConfigureAwait(false);
            return _serverConfig;
        }
    }

But it can happen, this function can be called from several points within short time period and in a case the _restApi would be down, it would make several calls that timed out synchronously because of the lock. (E.g. 4 calls at the same time, call timeout is 15 seconds so a function can await up to 60 seconds which is not wanted)

I came up with a solution that uses MemoryCache.

    public async Task<Config> Config(CancellationToken ct = default)
    {
        if(_memoryCache.TryGetValue("configTask", out Task<Config> configTask))
        {
            return await configTask.ConfigureAwait(false);
        }

        using (var releaser = await _asyncSemaphore.EnterAsync())
        {
            if (_config != null) return _config;
            var task = _memoryCache.Set("configTask", _restApi.Config(ct), TimeSpan.FromSeconds(5));
            _serverConfig = await task.ConfigureAwait(false);
            return _serverConfig;
        }
    }

Is anything bad on this approach? Looks to me ok. If the function is called within 5 seconds from the first call, I will always return the first call, otherwise I will wait and do the call again. Not sure, if it's fine to use a MemoryCache for tasks.

  • Have you looked at Polly to do this for you? https://github.com/App-vNext/Polly.Caching.MemoryCache – Rich Tebb Apr 26 '23 at 12:51
  • Could be related: [Enforce an async method to be called once](https://stackoverflow.com/questions/28340177/enforce-an-async-method-to-be-called-once). – Theodor Zoulias Apr 26 '23 at 18:42
  • @RichTebb, Thanks, didn't know about this implementation. But looks in the end that the lazy async will be more suitable to my case. TheodorZoulias, thanks I know about LazyAsync but it didn't come to my mind the correct solution. – Jerrod Wynne Apr 27 '23 at 12:23

1 Answers1

0

I ended with a bit adjusted LazyAsync by Stephen Cleary as I need it also reset the cache on request.

public class LazyAsyncResetable<T>
{
    private readonly Func<Nito.AsyncEx.AsyncLazy<T>> _valueFactory;        
    
    private readonly object _lock = new object();

    private Nito.AsyncEx.AsyncLazy<T> _lazy;
    public Nito.AsyncEx.AsyncLazy<T> ValueAsync
    {
        get
        {
            lock (_lock)
            {
                return _lazy;
            }
        }
    }

    public LazyAsyncResetable(Func<Task<T>> valueFactory, Nito.AsyncEx.AsyncLazyFlags asyncLazyFlags = Nito.AsyncEx.AsyncLazyFlags.None)
    {
        _valueFactory = new Func<Nito.AsyncEx.AsyncLazy<T>>(() =>
        {
            return new Nito.AsyncEx.AsyncLazy<T>(valueFactory, asyncLazyFlags);
        });

        _lazy = _valueFactory();
    }

    public void Reset()
    {
        lock (_lock)
        {
            _lazy = _valueFactory();
        }
    }

    /// <summary>
    /// Asynchronous infrastructure support. This method permits instances of <see cref="AsyncLazy&lt;T&gt;"/> to be await'ed.
    /// </summary>
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    public TaskAwaiter<T> GetAwaiter()
    {
        return ValueAsync.Task.GetAwaiter();
    }

    /// <summary>
    /// Asynchronous infrastructure support. This method permits instances of <see cref="AsyncLazy&lt;T&gt;"/> to be await'ed.
    /// </summary>
    public ConfiguredTaskAwaitable<T> ConfigureAwait(bool continueOnCapturedContext)
    {
        return ValueAsync.Task.ConfigureAwait(continueOnCapturedContext);
    }
}

And then use it like this:

public class Configuration
{
    public LazyAsyncResetable<Config> Config { get; }

    public Configuration(RestApi restApi)
    {
        Config = new LazyAsyncResetable<Config>(() => { return restApi.Config(CancellationToken.None); }, Nito.AsyncEx.AsyncLazyFlags.RetryOnFailure);            
    }

    private void OnSettingsChanged(object sender, EventArgs e)
    {
        Config.Reset();
    }
}
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 29 '23 at 19:58
  • I also have an [async cache](https://gist.github.com/StephenCleary/39a2cd0aa3c705a984a4dbbea8275fe9/c071c0bd79d39cafa20a7f544d1fad065371b333) you may find useful. – Stephen Cleary May 04 '23 at 02:27