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. The example presented above is contrived. My intention is to self-answer this question, but any contribution as an answer is welcome.

I would like to ask to avoid posting answers in the comments. Please use the comments to ask for clarifications about this question, in case clarifications are needed.

Theodor Zoulias
  • 24,585
  • 5
  • 40
  • 69
  • "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
  • 406,130
  • 70
  • 637
  • 767
  • Hmm, basing an expirable `AsyncLazy` on a full fledged [`IMemoryCache`](https://docs.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 an asynchronous operation that is started on first demand,
/// and is subject to an expiration policy. In case of failure the error
/// is propagated, but it's not cached.
/// </summary>
public class AsyncExpiringLazy<TResult>
{
    private readonly object _locker = new object();
    private readonly Func<Task<TResult>> _factory;
    private readonly Func<TResult, TimeSpan> _expirationSelector;
    private Task<(TResult Value, TimeSpan, long)> _task;

    public AsyncExpiringLazy(Func<Task<TResult>> factory,
        Func<TResult, TimeSpan> expirationSelector)
    {
        // Arguments validation omitted
        _factory = factory;
        _expirationSelector = expirationSelector;
    }

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

    public void Reset()
    {
        lock (_locker) if (_task != null && _task.IsCompleted) _task = null;
    }

    private async Task<TResult> GetTask()
    {
        var capturedTask = Volatile.Read(ref _task); // Capture the current task
        if (capturedTask != null)
        {
            // Propagate non-completed tasks without post-completion expiration checks
            if (!capturedTask.IsCompleted)
                return (await capturedTask.ConfigureAwait(false)).Value;

            // At this point the task is completed, so getting its .Result is OK
            var (value, expiration, timestamp) = capturedTask.GetAwaiter().GetResult();
            // Check if the value has expired, using static Stopwatch methods
            TimeSpan elapsed = TimeSpan.FromSeconds(
                (double)(Stopwatch.GetTimestamp() - timestamp) / Stopwatch.Frequency);
            if (elapsed < expiration) return value; // Return the non-expired value
        }

        // First call, or previous value expired, or previous operation failed
        Task<Task<(TResult, TimeSpan, long)>> newTaskTask = null;
        lock (_locker)
        {
            if (_task == capturedTask || _task == null)
            {
                // The current thread is eligible for creating the new task
                Task<(TResult, TimeSpan, long)> newTask = null;
                newTaskTask = new Task<Task<(TResult, TimeSpan, long)>>(async () =>
                {
                    try
                    {
                        var value = await _factory().ConfigureAwait(false);
                        var expiration = _expirationSelector(value);
                        return (value, expiration, Stopwatch.GetTimestamp());
                    }
                    catch
                    {
                        // Discard the failed task before throwing
                        lock (_locker) if (_task == newTask) _task = null;
                        throw;
                    }
                });
                _task = newTask = newTaskTask.Unwrap();
            }
            capturedTask = _task; // Capture the current task again
        }
        if (newTaskTask != null) newTaskTask.RunSynchronously(TaskScheduler.Default);
        return (await capturedTask.ConfigureAwait(false)).Value;
    }
}

This implementation is a modified version of the AsyncLazy<T> class found in this answer. Here I used the lock statement for synchronization, because using Interlocked operations would require doing loops to cover corner cases, which would make the code even more obscure than it already is.

The AsyncExpiringLazy<T> constructor accepts two delegates. The first (the factory) is the asynchronous factory that produces the result, and it is invoked on the context of the caller. The second (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 context (usually the ThreadPool context), immediately after a result has been asynchronously produced.

Usage example:

_webApi = new WebApi();
_accessToken = new AsyncExpiringLazy<AccessToken>(
    async () => await _webApi.Authenticate("xxx", "yyy"), _ => TimeSpan.FromMinutes(15));
await _webApi.PurchaseItem(await _accessToken, itemId, quantity);
Theodor Zoulias
  • 24,585
  • 5
  • 40
  • 69
  • 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)). filipw's implementation differs at having a more verbose API, it doesn't work with the `T` being a value type, it is dependent on the system clock, it makes no guarantee about the context that invokes the `factory`, and it makes post-completion expiration checks for initially non-completed tasks, but otherwise it's a good implementation and gets the job done. – Theodor Zoulias Jul 23 '21 at 12:45