Skip to content

Instantly share code, notes, and snippets.

@meziantou
Created January 2, 2018 13:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save meziantou/2da16a5721ecea60739898d1507cbf99 to your computer and use it in GitHub Desktop.
Save meziantou/2da16a5721ecea60739898d1507cbf99 to your computer and use it in GitHub Desktop.
RemoteDataCache
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ClassLibrary1
{
public abstract class RemoteDataCache<T> : IDisposable
{
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
private CacheValue<T> _value;
private TimeSpan _refreshPeriod;
private readonly Timer _timer;
public bool HasValue => _value.HasValue;
public TimeSpan RefreshPeriod
{
get
{
return _refreshPeriod;
}
set
{
_refreshPeriod = value;
_timer.Change(TimeSpan.Zero, value);
}
}
public RemoteDataCache()
{
_timer = new Timer(OnTick);
}
// For testing purpose
public void SetValue(T value)
{
_value = new CacheValue<T>(value);
}
public async Task<T> GetValueAsync(CancellationToken cancellationToken = default)
{
var value = _value;
if (value.HasValue)
return value.Value;
var mustRelease = true; // REVIEW?
await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
value = _value;
if (value.HasValue)
return value.Value;
value = await DeserializeAsync(cancellationToken).ConfigureAwait(false);
if (value.HasValue)
{
_value = value;
mustRelease = false;
// Don't wait for completion (how to handle errors properly?)
// Create a thread because the implementation of LoadAsync may be synchronous
var _ = ReloadInThreadAsync(cancellationToken);
return value.Value;
}
mustRelease = false;
value = await ReloadAsync(cancellationToken).ConfigureAwait(false);
return value.Value;
}
finally
{
if (mustRelease)
{
_semaphoreSlim.Release();
}
}
}
private Task<CacheValue<T>> ReloadInThreadAsync(CancellationToken cancellationToken)
{
return Task.Run(() => ReloadAsync(cancellationToken), cancellationToken);
}
private async Task<CacheValue<T>> ReloadAsync(CancellationToken cancellationToken)
{
try
{
var value = await LoadAsync(cancellationToken).ConfigureAwait(false);
var result = new CacheValue<T>(value);
_value = result;
await SerializeAsync(value).ConfigureAwait(false); // Another thread?
return result;
}
finally
{
_semaphoreSlim.Release();
}
}
protected abstract Task<T> LoadAsync(CancellationToken cancellationToken);
protected virtual Task SerializeAsync(T data)
{
return Task.CompletedTask;
}
protected virtual Task<CacheValue<T>> DeserializeAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new CacheValue<T>());
}
public void Dispose()
{
_timer.Dispose();
_semaphoreSlim.Dispose();
}
private async void OnTick(object state)
{
var cancellationToken = CancellationToken.None;
await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
await ReloadAsync(cancellationToken).ConfigureAwait(false);
}
}
public struct CacheValue<T>
{
public bool HasValue { get; }
public T Value { get; }
public CacheValue(T value)
{
Value = value;
HasValue = true;
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using ClassLibrary1;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTestProject1
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public async Task TestMethod1()
{
using (var cache = new Cache())
{
Assert.IsFalse(cache.HasValue);
Assert.AreEqual(0, await cache.GetValueAsync());
Assert.IsTrue(cache.HasValue);
Assert.AreEqual(0, await cache.GetValueAsync());
cache.RefreshPeriod = TimeSpan.FromSeconds(1);
var value1 = await cache.GetValueAsync();
await Task.Delay(TimeSpan.FromSeconds(2));
var value2 = await cache.GetValueAsync();
Assert.IsTrue(value1 < value2);
}
}
[TestMethod]
public async Task TestMethod2()
{
using (var cache = new Cache2())
{
Assert.IsFalse(cache.HasValue);
Assert.AreEqual(100, await cache.GetValueAsync());
Assert.IsTrue(cache.HasValue);
Assert.AreEqual(0, await cache.GetValueAsync());
}
}
private class Cache : RemoteDataCache<int>
{
private int _count = 0;
protected override Task<int> LoadAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_count++);
}
}
private class Cache2 : RemoteDataCache<int>
{
private int _count = 0;
protected override Task<int> LoadAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_count++);
}
protected override Task<CacheValue<int>> DeserializeAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new CacheValue<int>(100));
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment