Justin Horst
Justin Horst

Reputation: 201

kick off background task and return immediately

I have a method called DownloadFileAsync(url, Func<RemoteFileResponse, Task> onDownloadFinished) which performs the following:

This feels dirty to me as I need to perform the download on a background thread but I can't await it because I want to be able to return the cached file right away. The problem is that I lose any exception context if I do this.

Some options I can think of:

Was wondering if anyone had any other suggestions of how I could do this?

Pseudo code for my method:

Task<IFile> async DownloadFileAsync(url, Func<RemoteFileResponse, Task> onDownloadFinished)
{
   var cache = await CheckCacheAsync(url);

   // don't await this so the callee can use the cached file right away 
   // instead return the download result on the download finished callback
   DownloadUrlAndUpdateCache(url, onDownloadFinished);

   return cache
}

Upvotes: 3

Views: 684

Answers (2)

Absolom
Absolom

Reputation: 1389

I would suggest to create a new class FileRetriever that would contains the cached file and a Task that would be completed when the latest revision of the file is retrieved from the server.

Something like this:

public class FileRetriever
{
    public IFile CachedFile { get; private set; } 

    // Indicate if the CachedFile is the latest version of the file. If not, 
    // then LatestFileTask will complete eventually with the latest revision 
    public bool IsLatestFileVersion { get; private set; } 
    public Task<IFile> LatestFileTask { get; private set; } 

    public FileRetriever(IFile file)
    {
        IsLatestFileVersion = true;
        CachedFile = file;
        LatestFileTask = Task.FromResult(file);
    }

    public FileRetriever(IFile file, Task<IFile> latestFileTask)
    {
        IsLatestFileVersion = false;
        CachedFile = file;
        LatestFileTask = latestFileTask;
    }
}

To initialize the FileRetriever object, you would first check your cache to see if the file is already present.

Task<FileRetriever> async GetFileRetrieverAsync(url)
{
    IFile file = GetFileFromCache();
    if (file == null)
    {
         // File is not present in cache. Download it asynchronously and await it 
         // before returning the FileRetriever
         IFile file = await DownloadUrlAndUpdateCacheAsync(url);
         return new FileRetriever(file);
    }

    // File is present in cache but a new revision might be present on server. 
    // Start the download task but don't await for its completion. 
    Task<IFile> task = DownloadUrlAndUpdateCacheAsync(url);
    return new FileRetriever(file, task);
}

And then, you would use it like this:

var fileRetriever = await GetFileRetrieverAsync(url);
DoSomethingWithFile(fileRetriever.CachedFile);

if (!fileRetriever.IsLatestFileVersion)
{
    // A new revision of the file might be present on the server.
    // Wait for the download before updating the UI with the new file revision
    fileRetriever.LatestFileTask.ContinueWith(t => DoSomethingWithFile(t.Result)); 
}

Please note that I didn't tested this and error handling should be added in case where the download of the file fails.

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 457472

If your cache is an in-memory cache, then it's easier to cache the tasks rather than their results:

ConcurrentDictionary<Url, Task<IFile>> cache = ...;

Task<IFile> DownloadFileAsync(url) // no async keyword
{
  return cache.GetOrAdd(url, url => DownloadUrlAsync(url));
}

private async Task<IFile> DownloadUrlAsync(url)
{
  ... // actual download
}

Logically, the GetOrAdd is doing this (but in a thread-safe and more efficient manner):

if (cache.ContainsKey(url))
  return cache[url];
cache[url] = DownloadUrlAsync(url);
return cache[url];

Note, however, that this will cache the complete task, so download exceptions are also cached.

Upvotes: 3

Related Questions