As part of the task, it is allowed to use slightly outdated data. It is required that only one thread per key is involved in the critical section, while the remaining threads use data from the cache despite the expired TTL. The following code is an attempt to write a function calling the delegate within a blocking section based on SemaphoreSlim:
/// <summary>
/// Ensures consistency and
/// avoid extra requests to
/// datastore.
/// </summary>
///
/// <exception cref="ArgumentNullException"/>
/// <exception cref="ObjectDisposedException"/>
/// <exception cref="OperationCanceledException"/>
protected virtual (bool err, T val) AsCriticalLocal<T>(string k, string region, Func<(bool err, ICacheWrapper<T> val)> preCheck, Func<(bool err, T val)> method, CancellationToken token)
{
k = HelperIO(k, region);
/*
* Since this is a local cache, it
* checks for the presence of data
* in it.
*/
(bool err, ICacheWrapper<T> val) preCheckRes = preCheck();
SemaphoreSlim semaphore = Lock.GetOrAdd(k, f => new
SemaphoreSlim
(1, 1));
int m_currentCount = semaphore.CurrentCount;
// Means that the value has not been removed from the cache.
if (!preCheckRes.err)
{
// Call Wait will be blocking.
if (m_currentCount == 0)
{
// The update is already running, so don't care about TTL.
return (preCheckRes.err, preCheckRes.val.Val);
}
else
{
// The update has not been started and the data is still relevant.
if (!preCheckRes.val.IsExpired)
{
return (preCheckRes.err, preCheckRes.val.Val);
}
}
}
(bool err, T val) output = default;
try
{
Logger.Debug($"Cache: { Name }. Key: { k }. Lock: BLOCKED. Date: { DateTime.Now }. Type: { typeof(T) }");
semaphore.Wait(GlobalTimeout, token);
preCheckRes = preCheck();
// Means that the value has not been removed from the cache.
if (!preCheckRes.err)
{
/*
* If the semaphore is involved after
* updating the data in the cache and
* until the TTL expires
*/
if (!preCheckRes.val.IsExpired)
{
return (preCheckRes.err, preCheckRes.val.Val);
}
}
output = method();
}
catch (Exception exception)
{
Logger.Error(exception);
throw;
}
finally
{
if (output.err)
{
/*
* Remove data from the cache in order to avoid
* the use of outdated data since an exception
* was thrown in the update section.
*/
preCheckRes = preCheck();
if (!preCheckRes.err)
{
if (preCheckRes.val.IsExpired)
{
Instance.Remove(HelperIO(k, region));
}
}
}
semaphore.Release();
Logger.Debug($"Cache: { Name }. Key: { k }. Lock: RELEASED. Date: { DateTime.Now }. Type: { typeof(T) }");
}
return output;
}
Сhecks that there is data in the cache and the semaphore is already used to block when executing the delegate returning data from the datastore (database, etc). If the update is not started and the data is still relevant, immediately returns it:
int m_currentCount = semaphore.CurrentCount;
// Means that the value has not been removed from the cache.
if (!preCheckRes.err)
{
// Call Wait will be blocking.
if (m_currentCount == 0)
{
// The update is already running, so don't care about TTL.
return (preCheckRes.err, preCheckRes.val.Val);
}
else
{
// The update has not been started and the data is still relevant.
if (!preCheckRes.val.IsExpired)
{
return (preCheckRes.err, preCheckRes.val.Val);
}
}
}
If the method was not returned in the previous two paragraphs, then the Wait method is called and the data is extracted from cache and verified again: if the data is not removed and the data is still relevant, immediately returns it. If none of the above is done, then the delegate is called, inside which the query to the datastore is executed:
semaphore.Wait(GlobalTimeout, token);
preCheckRes = preCheck();
// Means that the value has not been removed from the cache.
if (!preCheckRes.err)
{
/*
* If the semaphore is involved after
* updating the data in the cache and
* until the TTL expires
*/
if (!preCheckRes.val.IsExpired)
{
return (preCheckRes.err, preCheckRes.val.Val);
}
}
output = method();
If an error occurs during operation, the data will be deleted from the cache to avoid the use of non-updated data:
finally
{
if (output.err)
{
/*
* Remove data from the cache in order to avoid
* the use of outdated data since an exception
* was thrown in the update section.
*/
preCheckRes = preCheck();
if (!preCheckRes.err)
{
if (preCheckRes.val.IsExpired)
{
Instance.Remove(HelperIO(k, region));
}
}
}
semaphore.Release();
Logger.Debug($"Cache: { Name }. Key: { k }. Lock: RELEASED. Date: { DateTime.Now }. Type: { typeof(T) }");
}
UPD:
preCheck
is a simple delegate that calls the Get
method internally and retrieves the data from the MemoryCache and returns a tuple of the form (bool err, T val)
:
{
return AsCriticalLocal(k, region, () =>
{
ICacheWrapper<T> item = Get<T>(k, region);
return (Common.IsDefault(item), item);
},
() =>
{
T val = method();
return Set(k, TTL, val, true, region);
},
token);
}
Questions:
- Will this code be thread safe?
- Is the described goal achieved?
- What changes are worth making in terms of code structure?
- Assuming that this code works as expected, is there any point in duplicating the code where the asynchronous version of Wait - WaitAsync will be used for
async
methods? It is Implied that there will not be a large number of blocked threads.