Handle cache unreliability #99

This commit is contained in:
John Luo 2016-05-17 14:42:55 -07:00
parent dabd28a5d9
commit d61c5100c9
9 changed files with 383 additions and 181 deletions

View File

@ -12,13 +12,13 @@
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"Hosting:Environment": "Development"
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"SessionSample": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000/",
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

@ -19,6 +19,9 @@ namespace SessionSample
public void ConfigureServices(IServiceCollection services)
{
// Adds a default in-memory implementation of IDistributedCache
services.AddDistributedMemoryCache();
// Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache.
// Note that this would require setting up the session state database.
//services.AddSqlServerCache(o =>
@ -32,10 +35,6 @@ namespace SessionSample
// This will override any previously registered IDistributedCache service.
//services.AddSingleton<IDistributedCache, RedisCache>();
#endif
// Adds a default in-memory implementation of IDistributedCache
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromSeconds(10);

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
@ -26,10 +25,11 @@ namespace Microsoft.AspNetCore.Session
private readonly string _sessionKey;
private readonly TimeSpan _idleTimeout;
private readonly Func<bool> _tryEstablishSession;
private readonly IDictionary<EncodedKey, byte[]> _store;
private readonly ILogger _logger;
private IDictionary<EncodedKey, byte[]> _store;
private bool _isModified;
private bool _loaded;
private bool _isAvailable;
private bool _isNewSessionKey;
private string _sessionId;
private byte[] _sessionIdBytes;
@ -71,11 +71,20 @@ namespace Microsoft.AspNetCore.Session
_isNewSessionKey = isNewSessionKey;
}
public bool IsAvailable
{
get
{
Load();
return _isAvailable;
}
}
public string Id
{
get
{
Load(); // TODO: Silent failure
Load();
if (_sessionId == null)
{
_sessionId = new Guid(IdBytes).ToString();
@ -88,8 +97,7 @@ namespace Microsoft.AspNetCore.Session
{
get
{
Load(); // TODO: Silent failure
if (_sessionIdBytes == null)
if (IsAvailable && _sessionIdBytes == null)
{
_sessionIdBytes = new byte[IdByteCount];
CryptoRandom.GetBytes(_sessionIdBytes);
@ -102,14 +110,14 @@ namespace Microsoft.AspNetCore.Session
{
get
{
Load(); // TODO: Silent failure
Load();
return _store.Keys.Select(key => key.KeyString);
}
}
public bool TryGetValue(string key, out byte[] value)
{
Load(); // TODO: Silent failure
Load();
return _store.TryGetValue(new EncodedKey(key), out value);
}
@ -120,22 +128,24 @@ namespace Microsoft.AspNetCore.Session
throw new ArgumentNullException(nameof(value));
}
var encodedKey = new EncodedKey(key);
if (encodedKey.KeyBytes.Length > KeyLengthLimit)
if (IsAvailable)
{
throw new ArgumentOutOfRangeException(nameof(key),
Resources.FormatException_KeyLengthIsExceeded(KeyLengthLimit));
}
var encodedKey = new EncodedKey(key);
if (encodedKey.KeyBytes.Length > KeyLengthLimit)
{
throw new ArgumentOutOfRangeException(nameof(key),
Resources.FormatException_KeyLengthIsExceeded(KeyLengthLimit));
}
Load();
if (!_tryEstablishSession())
{
throw new InvalidOperationException(Resources.Exception_InvalidSessionEstablishment);
if (!_tryEstablishSession())
{
throw new InvalidOperationException(Resources.Exception_InvalidSessionEstablishment);
}
_isModified = true;
byte[] copy = new byte[value.Length];
Buffer.BlockCopy(src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
_store[encodedKey] = copy;
}
_isModified = true;
byte[] copy = new byte[value.Length];
Buffer.BlockCopy(src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
_store[encodedKey] = copy;
}
public void Remove(string key)
@ -155,21 +165,35 @@ namespace Microsoft.AspNetCore.Session
{
if (!_loaded)
{
var data = _cache.Get(_sessionKey);
if (data != null)
try
{
Deserialize(new MemoryStream(data));
var data = _cache.Get(_sessionKey);
if (data != null)
{
Deserialize(new MemoryStream(data));
}
else if (!_isNewSessionKey)
{
_logger.AccessingExpiredSession(_sessionKey);
}
_isAvailable = true;
}
else if (!_isNewSessionKey)
catch (Exception exception)
{
_logger.AccessingExpiredSession(_sessionKey);
_logger.SessionCacheReadException(_sessionKey, exception);
_isAvailable = false;
_sessionId = string.Empty;
_sessionIdBytes = null;
_store = new NoOpSessionStore();
}
finally
{
_loaded = true;
}
_loaded = true;
}
}
// TODO: This should throw if called directly, but most other places it should fail silently
// (e.g. TryGetValue should just return null).
// This will throw if called directly and a failure occurs. The user is expected to handle the failures.
public async Task LoadAsync()
{
if (!_loaded)
@ -183,6 +207,7 @@ namespace Microsoft.AspNetCore.Session
{
_logger.AccessingExpiredSession(_sessionKey);
}
_isAvailable = true;
_loaded = true;
}
}
@ -191,24 +216,32 @@ namespace Microsoft.AspNetCore.Session
{
if (_isModified)
{
var data = await _cache.GetAsync(_sessionKey);
if (_logger.IsEnabled(LogLevel.Information) && data == null)
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.SessionStarted(_sessionKey, Id);
try
{
var data = await _cache.GetAsync(_sessionKey);
if (data == null)
{
_logger.SessionStarted(_sessionKey, Id);
}
}
catch (Exception exception)
{
_logger.SessionCacheReadException(_sessionKey, exception);
}
}
_isModified = false;
var stream = new MemoryStream();
Serialize(stream);
await _cache.SetAsync(
_sessionKey,
stream.ToArray(),
new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout));
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.SessionStored(_sessionKey, Id, _store.Count);
}
_isModified = false;
_logger.SessionStored(_sessionKey, Id, _store.Count);
}
else
{
@ -245,7 +278,6 @@ namespace Microsoft.AspNetCore.Session
{
if (content == null || content.ReadByte() != SerializationRevision)
{
// TODO: Throw?
// Replace the un-readable format.
_isModified = true;
return;
@ -333,77 +365,5 @@ namespace Microsoft.AspNetCore.Session
return output;
}
// Keys are stored in their utf-8 encoded state.
// This saves us from de-serializing and re-serializing every key on every request.
private class EncodedKey
{
private string _keyString;
private int? _hashCode;
internal EncodedKey(string key)
{
_keyString = key;
KeyBytes = Encoding.UTF8.GetBytes(key);
}
public EncodedKey(byte[] key)
{
KeyBytes = key;
}
internal string KeyString
{
get
{
if (_keyString == null)
{
_keyString = Encoding.UTF8.GetString(KeyBytes, 0, KeyBytes.Length);
}
return _keyString;
}
}
internal byte[] KeyBytes { get; private set; }
public override bool Equals(object obj)
{
var otherKey = obj as EncodedKey;
if (otherKey == null)
{
return false;
}
if (KeyBytes.Length != otherKey.KeyBytes.Length)
{
return false;
}
if (_hashCode.HasValue && otherKey._hashCode.HasValue
&& _hashCode.Value != otherKey._hashCode.Value)
{
return false;
}
for (int i = 0; i < KeyBytes.Length; i++)
{
if (KeyBytes[i] != otherKey.KeyBytes[i])
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
if (!_hashCode.HasValue)
{
_hashCode = SipHash.GetHashCode(KeyBytes);
}
return _hashCode.Value;
}
public override string ToString()
{
return KeyString;
}
}
}
}

View File

@ -29,14 +29,6 @@ namespace Microsoft.AspNetCore.Session
_loggerFactory = loggerFactory;
}
public bool IsAvailable
{
get
{
return true; // TODO:
}
}
public ISession Create(string sessionKey, TimeSpan idleTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey)
{
if (string.IsNullOrEmpty(sessionKey))

View File

@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
namespace Microsoft.AspNetCore.Session
{
// Keys are stored in their utf-8 encoded state.
// This saves us from de-serializing and re-serializing every key on every request.
internal class EncodedKey
{
private string _keyString;
private int? _hashCode;
internal EncodedKey(string key)
{
_keyString = key;
KeyBytes = Encoding.UTF8.GetBytes(key);
}
public EncodedKey(byte[] key)
{
KeyBytes = key;
}
internal string KeyString
{
get
{
if (_keyString == null)
{
_keyString = Encoding.UTF8.GetString(KeyBytes, 0, KeyBytes.Length);
}
return _keyString;
}
}
internal byte[] KeyBytes { get; private set; }
public override bool Equals(object obj)
{
var otherKey = obj as EncodedKey;
if (otherKey == null)
{
return false;
}
if (KeyBytes.Length != otherKey.KeyBytes.Length)
{
return false;
}
if (_hashCode.HasValue && otherKey._hashCode.HasValue
&& _hashCode.Value != otherKey._hashCode.Value)
{
return false;
}
for (int i = 0; i < KeyBytes.Length; i++)
{
if (KeyBytes[i] != otherKey.KeyBytes[i])
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
if (!_hashCode.HasValue)
{
_hashCode = SipHash.GetHashCode(KeyBytes);
}
return _hashCode.Value;
}
public override string ToString()
{
return KeyString;
}
}
}

View File

@ -8,8 +8,6 @@ namespace Microsoft.AspNetCore.Session
{
public interface ISessionStore
{
bool IsAvailable { get; }
ISession Create(string sessionKey, TimeSpan idleTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey);
}
}

View File

@ -12,6 +12,7 @@ namespace Microsoft.Extensions.Logging
private static Action<ILogger, string, string, Exception> _sessionStarted;
private static Action<ILogger, string, string, int, Exception> _sessionLoaded;
private static Action<ILogger, string, string, int, Exception> _sessionStored;
private static Action<ILogger, string, Exception> _sessionCacheReadException;
private static Action<ILogger, Exception> _errorUnprotectingCookie;
static LoggingExtensions()
@ -23,7 +24,7 @@ namespace Microsoft.Extensions.Logging
_accessingExpiredSession = LoggerMessage.Define<string>(
eventId: 2,
logLevel: LogLevel.Warning,
formatString: "Accessing expired session; Key:{sessionKey}");
formatString: "Accessing expired session, Key:{sessionKey}");
_sessionStarted = LoggerMessage.Define<string, string>(
eventId: 3,
logLevel: LogLevel.Information,
@ -36,8 +37,12 @@ namespace Microsoft.Extensions.Logging
eventId: 5,
logLevel: LogLevel.Debug,
formatString: "Session stored; Key:{sessionKey}, Id:{sessionId}, Count:{count}");
_errorUnprotectingCookie = LoggerMessage.Define(
_sessionCacheReadException = LoggerMessage.Define<string>(
eventId: 6,
logLevel: LogLevel.Error,
formatString: "Session cache read exception, Key:{sessionKey}");
_errorUnprotectingCookie = LoggerMessage.Define(
eventId: 7,
logLevel: LogLevel.Warning,
formatString: "Error unprotecting the session cookie.");
}
@ -67,6 +72,11 @@ namespace Microsoft.Extensions.Logging
_sessionStored(logger, sessionKey, sessionId, count, null);
}
public static void SessionCacheReadException(this ILogger logger, string sessionKey, Exception exception)
{
_sessionCacheReadException(logger, sessionKey, exception);
}
public static void ErrorUnprotectingSessionCookie(this ILogger logger, Exception exception)
{
_errorUnprotectingCookie(logger, exception);

View File

@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Session
{
internal class NoOpSessionStore : IDictionary<EncodedKey, byte[]>
{
public byte[] this[EncodedKey key]
{
get
{
return null;
}
set
{
}
}
public int Count { get; } = 0;
public bool IsReadOnly { get; } = false;
public ICollection<EncodedKey> Keys { get; } = new EncodedKey[0];
public ICollection<byte[]> Values { get; } = new byte[0][];
public void Add(KeyValuePair<EncodedKey, byte[]> item) { }
public void Add(EncodedKey key, byte[] value) { }
public void Clear() { }
public bool Contains(KeyValuePair<EncodedKey, byte[]> item) => false;
public bool ContainsKey(EncodedKey key) => false;
public void CopyTo(KeyValuePair<EncodedKey, byte[]>[] array, int arrayIndex) { }
public IEnumerator<KeyValuePair<EncodedKey, byte[]>> GetEnumerator() => Enumerable.Empty<KeyValuePair<EncodedKey, byte[]>>().GetEnumerator();
public bool Remove(KeyValuePair<EncodedKey, byte[]> item) => false;
public bool Remove(EncodedKey key) => false;
public bool TryGetValue(EncodedKey key, out byte[] value)
{
value = null;
return false;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@ -40,7 +40,6 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -72,7 +71,6 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -111,9 +109,7 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -166,9 +162,7 @@ namespace Microsoft.AspNetCore.Session
.ConfigureServices(
services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -219,9 +213,7 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -258,10 +250,7 @@ namespace Microsoft.AspNetCore.Session
.ConfigureServices(services =>
{
services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -310,10 +299,7 @@ namespace Microsoft.AspNetCore.Session
.ConfigureServices(services =>
{
services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession(o => o.IdleTimeout = TimeSpan.FromMilliseconds(30));
});
@ -373,10 +359,7 @@ namespace Microsoft.AspNetCore.Session
.ConfigureServices(services =>
{
services.AddSingleton(typeof(ILoggerFactory), new NullLoggerFactory());
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession(o => o.IdleTimeout = TimeSpan.FromMinutes(20));
services.Configure<MemoryCacheOptions>(o => o.Clock = clock);
});
@ -426,9 +409,7 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -471,9 +452,7 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -502,9 +481,7 @@ namespace Microsoft.AspNetCore.Session
})
.ConfigureServices(services =>
{
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
});
@ -516,6 +493,132 @@ namespace Microsoft.AspNetCore.Session
}
}
[Fact]
public async Task SessionLogsCacheReadException()
{
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseSession();
app.Run(context =>
{
byte[] value;
Assert.False(context.Session.TryGetValue("key", out value));
Assert.Equal(null, value);
Assert.Equal(string.Empty, context.Session.Id);
Assert.False(context.Session.Keys.Any());
return Task.FromResult(0);
});
})
.ConfigureServices(services =>
{
services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
services.AddSingleton<IDistributedCache>(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
{
DisableGet = true
});
services.AddSession();
});
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
response.EnsureSuccessStatusCode();
var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<DistributedSession>().ToArray();
Assert.Equal(1, sessionLogMessages.Length);
Assert.Contains("Session cache read exception", sessionLogMessages[0].State.ToString());
Assert.Equal(LogLevel.Error, sessionLogMessages[0].LogLevel);
}
}
[Fact]
public async Task SessionLogsCacheWriteException()
{
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseSession();
app.Run(context =>
{
context.Session.SetInt32("key", 0);
return Task.FromResult(0);
});
})
.ConfigureServices(services =>
{
services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
services.AddSingleton<IDistributedCache>(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
{
DisableSetAsync = true
});
services.AddSession();
});
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
response.EnsureSuccessStatusCode();
var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<DistributedSession>().ToArray();
Assert.Equal(1, sessionLogMessages.Length);
Assert.Contains("Session started", sessionLogMessages[0].State.ToString());
Assert.Equal(LogLevel.Information, sessionLogMessages[0].LogLevel);
var sessionMiddlewareLogMessages = sink.Writes.OnlyMessagesFromSource<SessionMiddleware>().ToArray();
Assert.Equal(1, sessionMiddlewareLogMessages.Length);
Assert.Contains("Error closing the session.", sessionMiddlewareLogMessages[0].State.ToString());
Assert.Equal(LogLevel.Error, sessionMiddlewareLogMessages[0].LogLevel);
}
}
[Fact]
public async Task SessionLogsCacheRefreshException()
{
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseSession();
app.Run(context =>
{
// The middleware calls context.Session.CommitAsync() once per request
return Task.FromResult(0);
});
})
.ConfigureServices(services =>
{
services.AddSingleton(typeof(ILoggerFactory), loggerFactory);
services.AddSingleton<IDistributedCache>(new UnreliableCache(new MemoryCache(new MemoryCacheOptions()))
{
DisableRefreshAsync = true
});
services.AddSession();
});
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var response = await client.GetAsync(string.Empty);
response.EnsureSuccessStatusCode();
var sessionLogMessages = sink.Writes.OnlyMessagesFromSource<SessionMiddleware>().ToArray();
Assert.Equal(1, sessionLogMessages.Length);
Assert.Contains("Error closing the session.", sessionLogMessages[0].State.ToString());
Assert.Equal(LogLevel.Error, sessionLogMessages[0].LogLevel);
}
}
private class TestClock : ISystemClock
{
public TestClock()
@ -531,46 +634,47 @@ namespace Microsoft.AspNetCore.Session
}
}
private class TestDistributedCache : IDistributedCache
private class UnreliableCache : IDistributedCache
{
private readonly MemoryDistributedCache _cache;
public bool DisableGet { get; set; }
public bool DisableSetAsync { get; set; }
public bool DisableRefreshAsync { get; set; }
public UnreliableCache(IMemoryCache memoryCache)
{
_cache = new MemoryDistributedCache(memoryCache);
}
public byte[] Get(string key)
{
throw new NotImplementedException();
if (DisableGet)
{
throw new InvalidOperationException();
}
return _cache.Get(key);
}
public Task<byte[]> GetAsync(string key)
{
throw new NotImplementedException();
}
public void Refresh(string key)
{
throw new NotImplementedException();
}
public Task<byte[]> GetAsync(string key) => _cache.GetAsync(key);
public void Refresh(string key) => _cache.Refresh(key);
public Task RefreshAsync(string key)
{
throw new NotImplementedException();
if (DisableRefreshAsync)
{
throw new InvalidOperationException();
}
return _cache.RefreshAsync(key);
}
public void Remove(string key)
{
throw new NotImplementedException();
}
public Task RemoveAsync(string key)
{
throw new NotImplementedException();
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
throw new NotImplementedException();
}
public void Remove(string key) => _cache.Remove(key);
public Task RemoveAsync(string key) => _cache.RemoveAsync(key);
public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => _cache.Set(key, value, options);
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options)
{
throw new NotImplementedException();
if (DisableSetAsync)
{
throw new InvalidOperationException();
}
return _cache.SetAsync(key, value, options);
}
}
}