Skip to content

Instantly share code, notes, and snippets.

@afscrome
Created June 25, 2018 16:07
Show Gist options
  • Save afscrome/e55e5f4e3712bfcac5b02ae137ae0d7f to your computer and use it in GitHub Desktop.
Save afscrome/e55e5f4e3712bfcac5b02ae137ae0d7f to your computer and use it in GitHub Desktop.
Background Refreshable
using NUnit.Framework;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace BackgroundRefreshable
{
///<summary>
/// Provides support for loading, storing and refreshing a value asyncronously.
///
/// Once Value has been loaded once, Value is guaranteed to complete syncronously, never blocking.
///</summary>
///<typeparam name="T"></typeparam>
public class BackgroundRefreshable<T>
{
private readonly Func<Task<T>> _loadFunction;
private readonly object _initaliseLock = new object();
private Task<T> _value;
public BackgroundRefreshable(Func<Task<T>> loadFunction)
{
_loadFunction = loadFunction;
}
public Task<T> Value => _value ?? Initialise();
private Task<T> Initialise()
{
lock(_initaliseLock)
{
if (_value != null)
return _value;
var loader = _loadFunction();
loader.ContinueWith(_ => _value = null, TaskContinuationOptions.NotOnRanToCompletion);
_value = loader;
}
return _value;
}
public async Task<T> Refresh()
{
var loaderTask = _loadFunction();
await loaderTask;
_value = loaderTask;
return loaderTask.Result;
}
}
public class Tests
{
[Test]
public async Task When_Making_First_Call_To_Value_Then_Blocked_Until_Value_Is_Loaded()
{
var tcs = new TaskCompletionSource<int>();
var refreshable = RefreshableLoadingSequence(
tcs.Task
);
var initialValueTask = refreshable.Value;
//Ensure value has not been loaded prematurely
await Task.Delay(TimeSpan.FromMilliseconds(1));
Assert.False(initialValueTask.IsCompleted);
//Complete the load
tcs.SetResult(45);
//Verify value is returned
Assert.That(await refreshable.Value, Is.EqualTo(45));
}
[Test]
public async Task When_Making_Multiple_Calls_To_Value_Before_Initial_Load_Then_Value_Only_Loaded_Once()
{
var tcs = new TaskCompletionSource<string>();
var refreshable = RefreshableLoadingSequence(
tcs.Task,
Task.FromResult("Now you don't")
);
var consumer1 = refreshable.Value;
var consumer2 = refreshable.Value;
var consumer3 = refreshable.Value;
//Ensure values have not been loaded prematurely
await Task.Delay(TimeSpan.FromMilliseconds(1));
Assert.False(consumer1.IsCompleted);
Assert.False(consumer2.IsCompleted);
Assert.False(consumer3.IsCompleted);
//Complete the load
tcs.SetResult("Now you see me");
//Verify all consumers got expected value
Assert.That(await consumer1, Is.EqualTo("Now you see me"));
Assert.That(await consumer2, Is.EqualTo("Now you see me"));
Assert.That(await consumer3, Is.EqualTo("Now you see me"));
}
[Test]
public async Task When_Getting_Value_Then_Value_Is_Cached()
{
var refreshable = RefreshableLoadingSequence(
Task.FromResult("Alice"),
Task.FromResult("Bob")
);
Assert.That(await refreshable.Value, Is.EqualTo("Alice"));
Assert.That(await refreshable.Value, Is.EqualTo("Alice"));
Assert.That(await refreshable.Value, Is.EqualTo("Alice"));
Assert.That(await refreshable.Value, Is.EqualTo("Alice"));
}
[Test]
public async Task When_Refresh_Succeeds_Then_Value_Is_Updated()
{
var refreshable = RefreshableLoadingSequence(
Task.FromResult("first"),
Task.FromResult("second")
);
Assert.That(await refreshable.Value, Is.EqualTo("first"));
var refresh = await refreshable.Refresh();
Assert.That(refresh, Is.EqualTo("second"));
Assert.That(await refreshable.Value, Is.EqualTo("second"));
}
[Test]
public async Task When_Refreshing_Then_Previous_Value_Returned_Until_Refresh_Completes()
{
var tcs = new TaskCompletionSource<double>();
var refreshable = RefreshableLoadingSequence(
Task.FromResult(3.142),
tcs.Task
);
//Initial load
Assert.That(await refreshable.Value, Is.EqualTo(3.142));
//Start Refresh
var refreshTask = refreshable.Refresh();
//Ensure old value is being returned
await Task.Delay(TimeSpan.FromMilliseconds(1));
var initialValueTask = refreshable.Value;
Assert.True(initialValueTask.IsCompleted);
Assert.That(initialValueTask.Result, Is.EqualTo(3.142));
//Complete Refresh
tcs.SetResult(2.718);
Assert.That(await refreshTask, Is.EqualTo(2.718));
//Value now returns updated result
var postRefreshValueTask = refreshable.Value;
Assert.True(postRefreshValueTask.IsCompleted);
Assert.That(postRefreshValueTask.Result, Is.EqualTo(2.718));
}
[Test]
public async Task When_Refresh_Throws_Exception_Then_Old_Value_Persists()
{
var exception = new Exception("Simulated failure");
var refreshable = RefreshableLoadingSequence(
Task.FromResult('%'),
Task.FromException<char>(exception)
);
//Initial load
Assert.That(await refreshable.Value, Is.EqualTo('%'));
//Ensure Refresh throws expected exception
var caughtException = Assert.ThrowsAsync<Exception>(() => refreshable.Refresh());
Assert.That(caughtException, Is.EqualTo(exception));
//But Value still returns previous result
var postRefreshValueTask = refreshable.Value;
Assert.True(postRefreshValueTask.IsCompleted);
Assert.That(postRefreshValueTask.Result, Is.EqualTo('%'));
}
[Test]
public async Task When_Refresh_Is_Cancelled_Then_Old_Value_Persists()
{
var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
var refreshable = RefreshableLoadingSequence(
Task.FromResult('%'),
Task.FromCanceled<char>(cancellationSource.Token)
);
//Initial load
Assert.That(await refreshable.Value, Is.EqualTo('%'));
//Ensure Refresh is canceleld
Assert.ThrowsAsync<TaskCanceledException>(() => refreshable.Refresh());
//But Value still returns previous result
var postRefreshValueTask = refreshable.Value;
Assert.True(postRefreshValueTask.IsCompleted);
Assert.That(postRefreshValueTask.Result, Is.EqualTo('%'));
}
[Test]
public async Task When_Initial_Load_Throws_Exception_Then_Next_Call_To_Value_Retries()
{
var initialException = new Exception("Initial Exception");
var refreshable = RefreshableLoadingSequence(
Task.FromException<string>(initialException),
Task.FromResult("I'm alive")
);
//First Load
var caughtInitialValueException = Assert.ThrowsAsync<Exception>(() => refreshable.Value);
Assert.That(caughtInitialValueException, Is.EqualTo(initialException));
//Second Load
Assert.That(await refreshable.Value, Is.EqualTo("I'm alive"));
}
[Test]
public async Task When_Initial_Load_Is_Cancelled_Then_Next_Call_To_Value_Retries()
{
var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
var refreshable = RefreshableLoadingSequence(
Task.FromCanceled<decimal>(cancellationSource.Token),
Task.FromResult(93.42m)
);
//First Load
Assert.ThrowsAsync<TaskCanceledException>(() => refreshable.Value);
//Second Load
Assert.That(await refreshable.Value, Is.EqualTo(93.42m));
}
private BackgroundRefreshable<T> RefreshableLoadingSequence<T>(params Task<T>[] sequence)
{
int index = 0;
return new BackgroundRefreshable<T>(() =>
{
if (index >= sequence.Length)
throw new IndexOutOfRangeException();
return sequence[index++];
});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment