Update: Configuration Optimisation and Caching

Loads entire configuration at start-up (rather than scope-based
loading). Deserialization occurs once, with the resulting value cached
and replayed if the requested type matches.
This commit is contained in:
Gary Sharp
2014-05-07 22:45:59 +10:00
parent 6b2cd47610
commit fb6067afc3
19 changed files with 455 additions and 282 deletions
+321 -81
View File
@@ -1,143 +1,384 @@
using Disco.Data.Repository;
using Disco.Models.Repository;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Disco.Data.Configuration
{
using ConfigurationCacheItemType = Tuple<ConfigurationItem, object>;
using ConfigurationCacheScopeType = ConcurrentDictionary<string, Tuple<ConfigurationItem, object>>;
using ConfigurationCacheType = ConcurrentDictionary<string, ConcurrentDictionary<string, Tuple<ConfigurationItem, object>>>;
internal static class ConfigurationCache
{
#region Cache
private static Dictionary<String, Dictionary<String, ConfigurationItem>> configDictionary = new Dictionary<string, Dictionary<string, ConfigurationItem>>();
private static List<ConfigurationItem> configurationItems = new List<ConfigurationItem>();
private static object configurationItemsLock = new object();
private static ConfigurationCacheType cacheStore = null;
private static object configChangeLock = new object();
private static void LoadConfigurationItems(DiscoDataContext Database, string Scope, bool Reload)
private static ConfigurationCacheType Cache(DiscoDataContext Database)
{
if (Reload || !configDictionary.ContainsKey(Scope))
if (ConfigurationCache.cacheStore == null)
{
lock (configurationItemsLock)
lock (configChangeLock)
{
if (Reload || !configDictionary.ContainsKey(Scope))
if (ConfigurationCache.cacheStore == null)
{
if (Database == null)
throw new InvalidOperationException("Cache-miss where Configuration Item requested from Cache-Only Configuration Context");
throw new InvalidOperationException("The Configuration must be loaded at least once before a Cache-Only Configuration Context is used");
var newItems = Database.ConfigurationItems.Where(ci => ci.Scope == Scope).ToArray();
var configurationItems = Database.ConfigurationItems.ToArray();
if (configDictionary.ContainsKey(Scope))
{
var existingItems = configDictionary[Scope];
foreach (var existingItem in existingItems.Values)
{
configurationItems.Remove(existingItem);
}
}
configurationItems.AddRange(newItems);
configDictionary[Scope] = newItems.ToDictionary(ci => ci.Key);
var indexedItems = configurationItems
.GroupBy(ci => ci.Scope)
.Select(g =>
new KeyValuePair<string, ConfigurationCacheScopeType>(
g.Key,
new ConfigurationCacheScopeType(
g.Select(i => new KeyValuePair<string, ConfigurationCacheItemType>(i.Key, Tuple.Create(i, (object)null))))));
cacheStore = new ConfigurationCacheType(indexedItems);
}
}
}
return ConfigurationCache.cacheStore;
}
private static Dictionary<string, Dictionary<string, ConfigurationItem>> ConfigurationDictionary(DiscoDataContext Database, string IncludingScope)
private static ConfigurationCacheItemType CacheGetItem(DiscoDataContext Database, string Scope, string Key)
{
LoadConfigurationItems(Database, IncludingScope, false);
return configDictionary;
}
private static ConfigurationItem ConfigurationItem(DiscoDataContext Database, string Scope, string Key)
{
Dictionary<string, ConfigurationItem> scopeDict = default(Dictionary<string, ConfigurationItem>);
if (ConfigurationDictionary(Database, Scope).TryGetValue(Scope, out scopeDict))
var cache = Cache(Database);
ConfigurationCacheScopeType scopeCache;
if (cache.TryGetValue(Scope, out scopeCache))
{
ConfigurationItem item = default(ConfigurationItem);
if (scopeDict.TryGetValue(Key, out item))
ConfigurationCacheItemType item = default(ConfigurationCacheItemType);
if (scopeCache.TryGetValue(Key, out item))
return item;
}
return null;
}
private static List<ConfigurationItem> ConfigurationItems(DiscoDataContext Database, string IncludingScope)
private static ConfigurationCacheItemType CacheSetItem(DiscoDataContext Database, string Scope, string Key, string Value, object ObjectValue)
{
LoadConfigurationItems(Database, IncludingScope, false);
return configurationItems;
if (Database == null)
throw new InvalidOperationException("Cannot save changes with a Cache-Only Configuration Context");
var item = CacheGetItem(Database, Scope, Key);
if (item == null && Value == null)
{
// No Change - already null
return null;
}
else if (item == null)
{
// New Configuration Item
lock (configChangeLock)
{
// Check again for thread safety
item = CacheGetItem(Database, Scope, Key);
if (item == null)
{
// Create Configuration Item
var configItem = new ConfigurationItem() { Scope = Scope, Key = Key, Value = Value };
item = new ConfigurationCacheItemType(configItem, ObjectValue);
// Add Item to DB
Database.ConfigurationItems.Add(configItem);
// Add Item to Cache
ConfigurationCacheScopeType scopeCache;
if (!cacheStore.TryGetValue(Scope, out scopeCache))
{
scopeCache = new ConfigurationCacheScopeType();
cacheStore.TryAdd(Scope, scopeCache);
}
scopeCache.TryAdd(Key, item);
return item;
}
}
}
if (item != null)
{
// Existing Configuration Item
lock (configChangeLock)
{
var configItem = item.Item1;
// Compare Values
if (item.Item1.Value == Value)
{
// No Change - Update Cache Reference Only
return SetItemTypeValue(item, ObjectValue);
}
else
{
var entityInfo = Database.Entry(configItem);
if (entityInfo.State == System.Data.EntityState.Detached)
{
// Reload Item from DB
configItem = Database.ConfigurationItems.Where(i => i.Scope == Scope && i.Key == Key).First();
}
if (Value == null)
{
// Remove item from Database
Database.ConfigurationItems.Remove(configItem);
// Remove item from Cache
ConfigurationCacheScopeType scopeCache;
if (cacheStore.TryGetValue(Scope, out scopeCache))
{
scopeCache.TryRemove(Key, out item);
}
return null;
}
else
{
// Update Database
configItem.Value = Value;
// Update Cache
ConfigurationCacheScopeType scopeCache;
if (cacheStore.TryGetValue(Scope, out scopeCache))
{
scopeCache.TryRemove(Key, out item);
item = new ConfigurationCacheItemType(configItem, ObjectValue);
scopeCache.TryAdd(Key, item);
return item;
}
}
}
}
}
return null;
}
private static ConfigurationCacheItemType SetItemTypeValue(ConfigurationCacheItemType ExistingItem, object Value)
{
var cache = ConfigurationCache.cacheStore;
ConfigurationCacheScopeType scopeCache;
if (cache.TryGetValue(ExistingItem.Item1.Scope, out scopeCache))
{
ConfigurationCacheItemType newItem = new ConfigurationCacheItemType(ExistingItem.Item1, Value);
scopeCache.TryUpdate(ExistingItem.Item1.Key, newItem, ExistingItem);
return newItem;
}
return null;
}
#endregion
#region Public Helpers
internal static ValueType GetConfigurationValue<ValueType>(DiscoDataContext Database, string Scope, string Key, ValueType Default)
#region Helpers
private static bool IsConvertableFromString(Type t)
{
var ci = ConfigurationItem(Database, Scope, Key);
if (ci == null)
if (t == typeof(Boolean) ||
t == typeof(Char) ||
t == typeof(SByte) ||
t == typeof(Byte) ||
t == typeof(Int16) || t == typeof(UInt16) ||
t == typeof(Int32) || t == typeof(UInt32) ||
t == typeof(Int64) || t == typeof(UInt64) ||
t == typeof(Single) ||
t == typeof(Double) ||
t == typeof(Decimal) ||
t == typeof(DateTime) ||
t == typeof(String))
return true;
else
return false;
}
#endregion
#region Cache Getters/Setters
internal static T GetValue<T>(DiscoDataContext Database, string Scope, string Key, T Default)
{
var item = CacheGetItem(Database, Scope, Key);
if (item == null)
return Default;
else
return (ValueType)Convert.ChangeType(ci.Value, typeof(ValueType));
}
internal static void SetConfigurationValue<ValueType>(DiscoDataContext Database, string Scope, string Key, ValueType Value)
{
if (Database == null)
throw new InvalidOperationException("Cannot save changes with a CacheOnly Context");
var ci = ConfigurationItem(Database, Scope, Key);
if (ci == null && Value != null)
{
lock (configurationItemsLock)
if (item.Item2 != null && item.Item2.GetType() == typeof(T))
{
ci = ConfigurationItem(Database, Scope, Key);
if (ci == null)
{
// Create Configuration Item
ci = new ConfigurationItem() { Scope = Scope, Key = Key, Value = Value.ToString() };
// Add Item to DB & Internal Collections
Database.ConfigurationItems.Add(ci);
ConfigurationItems(Database, Scope).Add(ci);
ConfigurationDictionary(Database, Scope)[Scope].Add(Key, ci);
ci = null;
}
// Return Cached Item
return (T)item.Item2;
}
}
if (ci != null)
{
lock (configurationItemsLock)
else
{
var entityInfo = Database.Entry(ci);
if (entityInfo.State == System.Data.EntityState.Detached)
{
// Reload Scope from DB
LoadConfigurationItems(Database, Scope, true);
ci = ConfigurationItem(Database, Scope, Key);
}
// Convert Serialized Item
Type itemType = typeof(T);
object itemValue;
if (Value == null)
if (itemType == typeof(string))
{
Database.ConfigurationItems.Remove(ci);
configurationItems.Remove(ci);
configDictionary[Scope].Remove(Key);
// string
itemValue = item.Item1.Value;
}
else if (itemType == typeof(object))
{
// object
itemValue = item.Item1.Value;
}
else if (IsConvertableFromString(itemType))
{
// IConvertable
itemValue = Convert.ChangeType(item.Item1.Value, itemType);
}
else if (itemType.BaseType != null && itemType.BaseType == typeof(Enum))
{
// Enum
itemValue = Enum.Parse(typeof(T), item.Item1.Value);
}
else
{
ci.Value = Value.ToString();
// JSON Deserialize
itemValue = JsonConvert.DeserializeObject<T>(item.Item1.Value);
}
// Set Item in Cache
SetItemTypeValue(item, itemValue);
return (T)itemValue;
}
}
}
internal static void SetValue<T>(DiscoDataContext Database, string Scope, string Key, T Value)
{
Type valueType = typeof(T);
string stringValue;
if (Value == null)
{
stringValue = null;
}
else if (valueType == typeof(object))
{
throw new ArgumentException(string.Format("Cannot serialize the configuration item [{0}].[{1}] which defines a type of [System.Object]", Scope, Key), "Value");
}
else if (IsConvertableFromString(valueType))
{
// string or supports IConvertable
stringValue = Value.ToString();
}
else if (valueType.BaseType != null && valueType.BaseType == typeof(Enum))
{
// Enum
stringValue = Value.ToString();
}
else
{
// JSON Serialize
stringValue = JsonConvert.SerializeObject(Value);
}
CacheSetItem(Database, Scope, Key, stringValue, Value);
}
#endregion
#region Cache Helpers
internal static IEnumerable<string> GetScopeKeys(DiscoDataContext Database, string Scope)
{
var cache = Cache(Database);
ConfigurationCacheScopeType scopeCache;
if (cache.TryGetValue(Scope, out scopeCache))
{
return scopeCache.Keys.ToList();
}
else
{
return Enumerable.Empty<string>();
}
}
internal static void RemoveScope(DiscoDataContext Database, string Scope)
{
if (Database == null)
throw new InvalidOperationException("Cannot save changes with a Cache-Only Configuration Context");
lock (configChangeLock)
{
// Remove item from Database
var items = Database.ConfigurationItems.Where(i => i.Scope == Scope).ToList();
items.ForEach(i => Database.ConfigurationItems.Remove(i));
// Remove item from Cache
if (cacheStore != null)
{
ConfigurationCacheScopeType scopeCache;
cacheStore.TryRemove(Scope, out scopeCache);
}
}
}
internal static void RemoveScopeKey(DiscoDataContext Database, string Scope, string Key)
{
if (Database == null)
throw new InvalidOperationException("Cannot save changes with a Cache-Only Configuration Context");
lock (configChangeLock)
{
var cacheItem = CacheGetItem(Database, Scope, Key);
ConfigurationItem configItem = null;
// Remove item from Database
if (cacheItem != null)
{
configItem = cacheItem.Item1;
var entityInfo = Database.Entry(configItem);
if (entityInfo.State == System.Data.EntityState.Detached)
{
// Reload Item from DB
configItem = Database.ConfigurationItems.Where(i => i.Scope == Scope && i.Key == Key).FirstOrDefault();
}
}
if (configItem == null)
{
// Load Item from DB
configItem = Database.ConfigurationItems.Where(i => i.Scope == Scope && i.Key == Key).FirstOrDefault();
}
if (configItem != null)
{
Database.ConfigurationItems.Remove(configItem);
}
// Remove item from Cache
if (cacheItem != null)
{
ConfigurationCacheScopeType scopeCache;
if (cacheStore.TryGetValue(Scope, out scopeCache))
{
scopeCache.TryRemove(Key, out cacheItem);
}
}
}
}
internal static List<ConfigurationItem> GetConfigurationItems(DiscoDataContext Database, string Scope)
{
return ConfigurationDictionary(Database, Scope)[Scope].Values.ToList();
}
#endregion
internal static string ObsfucateValue(string Value)
#region Obsfucation Helpers
internal static string Obsfucate(this string Value)
{
if (string.IsNullOrEmpty(Value))
return Value;
else
return Convert.ToBase64String(Encoding.Unicode.GetBytes(Value));
}
internal static string DeobsfucateValue(string ObsfucatedValue)
internal static string Deobsfucate(this string ObsfucatedValue)
{
if (string.IsNullOrEmpty(ObsfucatedValue))
return ObsfucatedValue;
@@ -145,6 +386,5 @@ namespace Disco.Data.Configuration
return Encoding.Unicode.GetString(Convert.FromBase64String(ObsfucatedValue));
}
#endregion
}
}