0

I am consuming an asynchronous Web API that requires an AccessToken (an immutable struct) to be passed as an argument on every API call. This AccessToken is itself obtained by calling an asynchronous Authenticate method of the same Web API.

class WebApi
{
    public Task<AccessToken> Authenticate(string username, string password);
    public Task PurchaseItem(AccessToken token, int itemId, int quantity);
    // More methods having an AccessToken parameter
}

I don't want to call the Authenticate method before calling every other method of the API, for performance reasons. I want to call it once, and then reuse the same AccessToken for multiple API calls. My problem is that the AccessToken is expiring every 15 minutes, and calling any API method with an expired AccessToken results to an AccessTokenExpiredException. I could catch this exception and then retry the faulted call, after acquiring a new AccessToken, but I would prefer to preemptively refresh the AccessToken before it has expired, again for performance reasons. My application is multithreaded, so multiple threads might try to use/refresh the same AccessToken value concurrently, and things quickly start to become very messy.

The requirements are:

  1. The Authenticate method should not be called more frequently than once every 15 minutes, even if multiple threads attempt to invoke methods of the Web API concurrently.
  2. In case an Authenticate call fails, it should be repeated the next time an AccessToken is needed. This requirement takes precedence over the previous requirement. Caching and reusing a faulted Task<AccessToken> for 15 minutes is not acceptable.
  3. The Authenticate method should be called only when an AccessToken is actually needed. Invoking it every 15 minutes with a Timer is not acceptable.
  4. An AccessToken should only be used during the next 15 minutes after its creation.
  5. The expiration mechanism should not be dependent on the system clock. A system-wise clock adjustment should not affect (elongate or shorten) the expiration period.

My question is: how could I abstract the functionality of acquiring, monitoring the expiration, and refreshing the AccessToken, in a way that satisfies the requirements, while keeping the rest of my application clean from all this complexity? I am thinking of something similar to the AsyncLazy<T> type that I found in this question: Enforce an async method to be called once, but enhanced with expiration functionality. Here is a hypothetical example of using this type (enhanced with a TimeSpan parameter):

private readonly WebApi _webApi = new WebApi();
private readonly AsyncLazy<AccessToken> _accessToken = new AsyncLazy<AccessToken>(
    () => _webApi.Authenticate("xxx", "yyy"), TimeSpan.FromMinutes(15));

async Task Purchase(int itemId, int quantity)
{
    await _webApi.PurchaseItem(await _accessToken, itemId, quantity);
}

Btw this question was inspired by a recent question, where the OP was trying to solve a similar problem in a different way.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • "I would prefer to preemptively refresh the AccessToken before it has expired" - are you sure that's wise? It means your application has to have knowledge of specific policies belonging to the other system (what if it's reconfigured to limit validity to 10 minutes? What if it has rules that force early expiration?) – Damien_The_Unbeliever Jul 21 '21 at 10:46
  • @Damien_The_Unbeliever let's assume that I am consuming a Web API that is built and maintained by another department of the same company, and that the expiration policy is well known to both departments, and that changes in the expiration policy are happening rarely, and are communicated beforehand. – Theodor Zoulias Jul 21 '21 at 11:31
  • @Damien_The_Unbeliever actually you are right. In case the unthinkable happens and the `AccessToken` has expired on the server side before its expected expiration time on the client side, the client should have a fallback mechanism that discards the currently cached `AccessToken`, before retrying the failed request. I should probably ask for this functionality in the question as a 6th requirement, but the question is already complicated enough, so it might be better for now to leave it as is. – Theodor Zoulias Jul 22 '21 at 07:27

2 Answers2

1

A "resettable" AsyncLazy<T> is equivalent to a single-item asynchronous cache. In this case, with a time-based expiration, the similarity is even more striking.

I recommend using an actual AsyncCache<T>; I have one I'm working on and am currently using in a very low-load prod-like environment, but it hasn't been well tested in a real production environment.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Hmm, basing an expirable `AsyncLazy` on a full fledged [`IMemoryCache`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) engine seems like overkill to me. But it's certainly one way to do it. – Theodor Zoulias Jul 21 '21 at 14:14
0

Here is an implementation of an AsyncExpiringLazy<T> class, which is essentially an AsyncLazy<T> with added expiration functionality:

/// <summary>
/// Represents the result of an asynchronous operation that is invoked lazily
/// on demand, and is subject to an expiration policy. Errors are not cached.
/// Subsequent executions do not overlap. Concurrent observers receive
/// the result of the same operation.
/// </summary>
public class AsyncExpiringLazy<TResult>
{
    private readonly object _locker = new object();
    private readonly Func<Task<TResult>> _taskFactory;
    private readonly Func<TResult, TimeSpan> _expirationSelector;
    private State _state;

    // The mutable state is stored in a record struct for convenience.
    private record struct State(Task<TResult> Task, long ExpirationTimestamp);

    public AsyncExpiringLazy(Func<Task<TResult>> taskFactory,
        Func<TResult, TimeSpan> expirationSelector)
    {
        ArgumentNullException.ThrowIfNull(taskFactory);
        ArgumentNullException.ThrowIfNull(expirationSelector);
        _taskFactory = taskFactory;
        _expirationSelector = expirationSelector;
    }

    public AsyncExpiringLazy(Func<TResult> valueFactory,
        Func<TResult, TimeSpan> expirationSelector)
    {
        ArgumentNullException.ThrowIfNull(valueFactory);
        ArgumentNullException.ThrowIfNull(expirationSelector);
        _taskFactory = () => System.Threading.Tasks.Task.FromResult(valueFactory());
        _expirationSelector = expirationSelector;
    }

    private Task<TResult> GetTask()
    {
        Task<Task<TResult>> newTaskTask;
        Task<TResult> newTask;
        lock (_locker)
        {
            if (_state.Task is not null
                && _state.ExpirationTimestamp > Environment.TickCount64)
                    return _state.Task; // The task has not expired.

            // Either this is the first call, or the task expired or failed.
            newTaskTask = new(_taskFactory);
            newTask = newTaskTask.Unwrap().ContinueWith(task =>
            {
                State newState = default;
                try
                {
                    if (task.IsCompletedSuccessfully)
                    {
                        TimeSpan expiration = _expirationSelector(task.Result);
                        if (expiration > TimeSpan.Zero)
                            newState = new State(task, Environment.TickCount64
                                + (long)expiration.TotalMilliseconds);
                    }
                }
                finally
                {
                    // In case the task or the selector failed,
                    // or the expiration is not positive, the _state is
                    // updated to default, to trigger a retry later.
                    lock (_locker) _state = newState;
                }
                return task;
            }, default, TaskContinuationOptions.DenyChildAttach |
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default).Unwrap();

            // While the task is running, the expiration is set to never.
            _state = new State(newTask, Int64.MaxValue);
        }
        newTaskTask.RunSynchronously(TaskScheduler.Default);
        return newTask;
    }

    public Task<TResult> Task => GetTask();

    public TResult Result => GetTask().GetAwaiter().GetResult();

    public TaskAwaiter<TResult> GetAwaiter() => GetTask().GetAwaiter();

    public ConfiguredTaskAwaitable<TResult> ConfigureAwait(
        bool continueOnCapturedContext)
            => GetTask().ConfigureAwait(continueOnCapturedContext);

    public bool ExpireImmediately()
    {
        lock (_locker)
        {
            if (_state.Task is null) return false;
            if (!_state.Task.IsCompleted) return false;
            _state = default;
            return true;
        }
    }
}

Usage example:

_webApi = new WebApi();
_accessToken = new AsyncExpiringLazy<AccessToken>(
    async () => await _webApi.Authenticate("xxx", "yyy"), _ => TimeSpan.FromMinutes(15));
try
{
    await _webApi.PurchaseItem(await _accessToken, itemId, quantity);
}
catch (AccessTokenExpiredException)
{
    _accessToken.ExpireImmediately(); throw;
}

This implementation is a modified version of the AsyncLazy<T> class that can be found in this answer. The AsyncExpiringLazy<T> constructor accepts two delegates. The taskFactory is the asynchronous method that produces the result, and it is invoked on the calling thread (the thread that calls the await _accessToken in the example above). The expirationSelector is a selector of the expiration period, which is a TimeSpan, and takes the produced result as argument. This delegate is invoked on an unknown thread (usually on the ThreadPool), immediately after a result has been asynchronously produced.

A constructor that accepts a synchronous valueFactory is also available.

The ExpireImmediately method causes the immediate expiration of the previously completed task. In case a task is currently running, this method has no effect.

This implementation propagates all the exceptions that might be thrown by the taskFactory delegate, not just the first one.

An online demonstration of the AsyncExpiringLazy<T> class can be found here. It demonstrates the behavior of the class when used by multiple concurrent workers, and when the taskFactory fails.

The AsyncExpiringLazy<T> class is thread-safe.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I should note that an `AsyncExpiringLazy` class with similar functionality already exists in [this](https://github.com/filipw/async-expiring-lazy "Async Expiring Lazy") GitHub repository (by [filipw](https://github.com/filipw)). – Theodor Zoulias Jun 13 '22 at 17:42