Created
January 2, 2018 13:46
-
-
Save meziantou/2da16a5721ecea60739898d1507cbf99 to your computer and use it in GitHub Desktop.
RemoteDataCache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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