Skip to content

Commit

Permalink
Merge pull request #91 from mdsol/mauth_caching
Browse files Browse the repository at this point in the history
[MCC-839304] Fix MAuth Caching issues
  • Loading branch information
dcassidy-mdsol authored Nov 1, 2021
2 parents ef7a681 + ec9458e commit 6bb7fac
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 85 deletions.
49 changes: 0 additions & 49 deletions src/Medidata.MAuth.Core/CacheExtensions.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace Medidata.MAuth.Core
namespace Medidata.MAuth.Core.Caching
{
[ExcludeFromCodeCoverage]
internal class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory)
Expand All @@ -17,8 +19,5 @@ public AsyncLazy(Func<Task<T>> taskFactory)
}

public TaskAwaiter<T> GetAwaiter() => Value.GetAwaiter();

public ConfiguredTaskAwaitable<T> ConfigureAwait(bool continueOnCapturedContext)
=> Value.ConfigureAwait(continueOnCapturedContext);
}
}
26 changes: 26 additions & 0 deletions src/Medidata.MAuth.Core/Caching/CacheResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Medidata.MAuth.Core.Caching
{
/// <summary>
/// Dto to store cache fields
/// </summary>
/// <typeparam name="TItem">Cache result type.</typeparam>
public class CacheResult<TItem>
{
/// <summary>
/// Item to store in cache.
/// </summary>
public TItem Result { get; set; }

/// <summary>
/// Flag to determine if the result should be cache.
/// </summary>
public bool ShouldCache { get; set; }

/// <summary>
/// Cache duration.
/// </summary>
public TimeSpan AbsoluteCacheDuration { get; set; }
}
}
28 changes: 28 additions & 0 deletions src/Medidata.MAuth.Core/Caching/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;

namespace Medidata.MAuth.Core.Caching
{
/// <summary>
/// Caching abstraction
/// </summary>
public interface ICacheService
{
/// <summary>
/// Gets or create cache item with locking.
/// </summary>
/// <typeparam name="TItem">The cached item type.</typeparam>
/// <param name="key">Cache Key</param>
/// <param name="factory">Factory method to create cached item</param>
/// <returns>The item that was retrieved from the cache or created.</returns>
Task<TItem> GetOrCreateWithLock<TItem>(
string key,
Func<Task<CacheResult<TItem>>> factory);

/// <summary>
/// Remove key from cache if exists.
/// </summary>
/// <param name="key"></param>
void Remove(string key);
}
}
127 changes: 127 additions & 0 deletions src/Medidata.MAuth.Core/Caching/MemoryCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;

namespace Medidata.MAuth.Core.Caching
{
/// <summary>
/// In-Memory Cache service
/// </summary>
public class MemoryCacheService : ICacheService
{
private readonly IMemoryCache _cache;

/// <summary>
/// In Memory Cache Service
/// </summary>
/// <param name="cache"></param>
public MemoryCacheService(IMemoryCache cache)
{
_cache = cache;
}

/// <summary>
/// Checks if key is in the cache
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <typeparam name="T"></typeparam>
/// <returns>True when key found in cache.</returns>
public bool TryGetValue<T>(string key, out T value)
=> _cache.TryGetValue(key, out value);

/// <summary>
///
/// </summary>
/// <param name="key"></param>
public void Remove(string key) => _cache.Remove(key);

/// <summary>
/// Add an item to the cache.
/// </summary>
/// <param name="key">Cache Key</param>
/// <param name="value">Item to cache</param>
/// <param name="expiration">Cache Expiration</param>
public void SetItem<T>(string key, T value, DateTimeOffset expiration)
=> _cache.Set(key, value, expiration);

/// <summary>
/// Gets or create cache item with locking.
/// </summary>
/// <typeparam name="TItem">The cached item type.</typeparam>
/// <param name="key">Cache Key</param>
/// <param name="factory">Factory method to create cached item</param>
/// <returns>Cached Item</returns>
public async Task<TItem> GetOrCreateWithLock<TItem>(
string key,
Func<Task<CacheResult<TItem>>> factory)
{
if (TryGetValue(key, out TItem item))
{
return item;
}

return await CreateAsyncLazyWithLock(key, factory);
}

[ExcludeFromCodeCoverage]
private AsyncLazy<TItem> CreateAsyncLazyWithLock<TItem>(string key, Func<Task<CacheResult<TItem>>> factory)
{
var asyncKey = CreateAsyncKey(key);

if (TryGetValue(asyncKey, out AsyncLazy<TItem> asyncItem))
{
return asyncItem;
}

var lockStr = string.Intern("mutex_" + asyncKey);

lock (lockStr)
{
if (TryGetValue(asyncKey, out asyncItem))
{
return asyncItem;
}

if (TryGetValue(key, out TItem item))
{
return new AsyncLazy<TItem>(() => item);
}

asyncItem = new AsyncLazy<TItem>(() => CreateLazyFactory(key, factory));

_cache.Set(asyncKey, asyncItem);
}

return asyncItem;
}

private async Task<TItem> CreateLazyFactory<TItem>(string key, Func<Task<CacheResult<TItem>>> factory)
{
try
{
var result = await factory().ConfigureAwait(false);

if (!result.ShouldCache)
{
return result.Result;
}

using var entry = _cache.CreateEntry(key);
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + result.AbsoluteCacheDuration;
entry.SetValue(result.Result);
return result.Result;
}
finally
{
var asyncKey = CreateAsyncKey(key);
_cache.Remove(asyncKey);
}

}

private string CreateAsyncKey(string key) => $"{key}-async";

}
}
36 changes: 36 additions & 0 deletions src/Medidata.MAuth.Core/Caching/NoopCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace Medidata.MAuth.Core.Caching
{
/// <summary>
/// Noop Cache Service
/// </summary>
[ExcludeFromCodeCoverage]
public class NoopCacheService : ICacheService
{
/// <summary>
/// Gets or create cache item with locking.
/// </summary>
/// <typeparam name="TItem">The cached item type.</typeparam>
/// <param name="key">Cache Key</param>
/// <param name="factory">Factory method to create cached item</param>
/// <returns>Cached Item</returns>
public async Task<TItem> GetOrCreateWithLock<TItem>(
string key,
Func<Task<CacheResult<TItem>>> factory)
{
var data = await factory().ConfigureAwait(false);
return data.Result;
}

/// <summary>
/// Remove key from cache if exists.
/// </summary>
/// <param name="key"></param>
public void Remove(string key)
{
}
}
}
52 changes: 22 additions & 30 deletions src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Medidata.MAuth.Core.Caching;
using Medidata.MAuth.Core.Exceptions;
using Microsoft.Extensions.Caching.Memory;
using Org.BouncyCastle.Crypto;
Expand All @@ -12,14 +13,14 @@ namespace Medidata.MAuth.Core
{
internal class MAuthAuthenticator
{
private readonly IMemoryCache _cache;
private readonly ICacheService _cache;
private readonly MAuthOptionsBase _options;
private readonly ILogger _logger;
private readonly Lazy<HttpClient> _lazyHttpClient;

public Guid ApplicationUuid => _options.ApplicationUuid;

public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger, IMemoryCache cache = null)
public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger, ICacheService cacheService = null)
{
if (options.ApplicationUuid == default)
throw new ArgumentException(nameof(options.ApplicationUuid));
Expand All @@ -30,7 +31,7 @@ public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger, IMemoryCache
if (string.IsNullOrWhiteSpace(options.PrivateKey))
throw new ArgumentNullException(nameof(options.PrivateKey));

_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
_cache = cacheService ?? new MemoryCacheService(new MemoryCache(new MemoryCacheOptions()));
_options = options;
_logger = logger;
_lazyHttpClient = new Lazy<HttpClient>(() => CreateHttpClient(options));
Expand Down Expand Up @@ -103,29 +104,17 @@ private async Task<bool> Authenticate(HttpRequestMessage request, MAuthVersion v

var mAuthCore = MAuthCoreFactory.Instantiate(version);
var authInfo = GetAuthenticationInfo(request, mAuthCore);

var appInfo = await _cache.GetOrCreateWithLock(
authInfo.ApplicationUuid.ToString(),
() => SendApplicationInfoRequest(authInfo.ApplicationUuid)).ConfigureAwait(false);

try
{
var appInfo = await GetApplicationInfo(authInfo.ApplicationUuid).ConfigureAwait(false);
var signature = await mAuthCore.GetSignature(request, authInfo).ConfigureAwait(false);
return mAuthCore.Verify(authInfo.Payload, signature, appInfo.PublicKey);

var signature = await mAuthCore.GetSignature(request, authInfo).ConfigureAwait(false);
return mAuthCore.Verify(authInfo.Payload, signature, appInfo.PublicKey);
}
catch (RetriedRequestException)
{
// If the appliation info could not be fetched, remove the lazy
// object from the cache to allow for another attempt.
_cache.Remove(authInfo.ApplicationUuid);
throw;
}
}

private AsyncLazy<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
_cache.GetOrCreateWithLock(
applicationUuid,
entry => new AsyncLazy<ApplicationInfo>(() => SendApplicationInfoRequest(entry, applicationUuid)));

private async Task<ApplicationInfo> SendApplicationInfoRequest(ICacheEntry entry, Guid applicationUuid)

private async Task<CacheResult<ApplicationInfo>> SendApplicationInfoRequest(Guid applicationUuid)
{
var logMessage = "Mauth-client requesting from mAuth service application info not available " +
$"in the local cache for app uuid {applicationUuid}.";
Expand All @@ -139,16 +128,19 @@ private async Task<ApplicationInfo> SendApplicationInfoRequest(ICacheEntry entry
).ConfigureAwait(false);

var result = await response.Content.FromResponse().ConfigureAwait(false);

entry.SetOptions(
new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(response.Headers.CacheControl?.MaxAge ?? TimeSpan.FromMinutes(5))
);


logMessage = $"Mauth-client application info for app uuid {applicationUuid} cached in memory.";
_logger.LogInformation(logMessage);

return result;
var cacheResult = new CacheResult<ApplicationInfo>
{
ShouldCache = true,
Result = result,
AbsoluteCacheDuration = response.Headers.CacheControl?.MaxAge ?? TimeSpan.FromMinutes(5),
};

return cacheResult;

}

/// <summary>
Expand Down
Loading

0 comments on commit 6bb7fac

Please sign in to comment.