diff --git a/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs b/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs index c5ae1f6a7..8f372b2f2 100644 --- a/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs +++ b/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs @@ -19,8 +19,6 @@ public class GXSessionServiceFactory private static readonly IGXLogger log = GXLoggerFactory.GetLogger(); static ISessionService sessionService; - static string REDIS = "REDIS"; - static string DATABASE = "DATABASE"; public static ISessionService GetProvider() { if (sessionService != null) @@ -35,9 +33,9 @@ public static ISessionService GetProvider() //Compatibility if (string.IsNullOrEmpty(className)) { - if (providerService.Name.Equals(REDIS, StringComparison.OrdinalIgnoreCase)) + if (providerService.Name.Equals(GXServices.REDIS_CACHE_SERVICE, StringComparison.OrdinalIgnoreCase)) type = typeof(GxRedisSession); - else if (providerService.Name.Equals(DATABASE, StringComparison.OrdinalIgnoreCase)) + else if (providerService.Name.Equals(GXServices.DATABASE_CACHE_SERVICE, StringComparison.OrdinalIgnoreCase)) type = typeof(GxDatabaseSession); } else diff --git a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj index a7257bac7..f312f6e6a 100644 --- a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj +++ b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj @@ -16,7 +16,7 @@ - + diff --git a/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs b/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs index fc59e7b5c..390fe5c1e 100644 --- a/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs +++ b/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs @@ -15,6 +15,8 @@ public class GXServices public static string STORAGE_SERVICE = "Storage"; public static string STORAGE_APISERVICE = "StorageAPI"; public static string CACHE_SERVICE = "Cache"; + public static string REDIS_CACHE_SERVICE = "Redis"; + public static string DATABASE_CACHE_SERVICE = "DATABASE"; public static string DATA_ACCESS_SERVICE = "DataAccess"; public static string SESSION_SERVICE = "Session"; public static string WEBNOTIFICATIONS_SERVICE = "WebNotifications"; @@ -47,6 +49,7 @@ public static GXServices Instance } set { } } + public void AddService(string name, GXService service) { services[name] = service; diff --git a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs index 7e79f6998..367df68db 100644 --- a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs +++ b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs @@ -2,15 +2,17 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading.Tasks; #if NETCORE using GeneXus.Application; +using GxClasses.Helpers; +using Microsoft.Extensions.Caching.Memory; #endif using GeneXus.Encryption; using GeneXus.Services; using GeneXus.Utils; using StackExchange.Redis; using StackExchange.Redis.KeyspaceIsolation; +using GeneXus.Configuration; namespace GeneXus.Cache { @@ -22,6 +24,12 @@ public sealed class Redis : ICacheService2 IDatabase _redisDatabase; #if NETCORE bool _multitenant; + MemoryCache _localCache; + private const double DEFAULT_LOCAL_CACHE_FACTOR = 0.2; + private TimeSpan MAX_LOCAL_CACHE_TTL; + private long MAX_LOCAL_CACHE_TTL_TICKS; + private const int MAX_LOCAL_CACHE_TTL_DEFAULT_MIMUTES = 5; + #endif ConfigurationOptions _redisConnectionOptions; private const int REDIS_DEFAULT_PORT = 6379; @@ -34,10 +42,8 @@ public Redis(string connectionString) _redisConnectionOptions.AllowAdmin = true; } - public Redis(string connectionString, int sessionTimeout) + public Redis(string connectionString, int sessionTimeout):this(connectionString) { - _redisConnectionOptions = ConfigurationOptions.Parse(connectionString); - _redisConnectionOptions.AllowAdmin = true; redisSessionTimeout = sessionTimeout; } public Redis() @@ -76,8 +82,37 @@ public Redis() _redisConnectionOptions = ConfigurationOptions.Parse(address); } _redisConnectionOptions.AllowAdmin = true; + InitLocalCache(providerService); + } + } + private void InitLocalCache(GXService providerService) + { +#if NETCORE + string localCache = providerService.Properties.Get("ENABLE_MEMORY_CACHE"); + if (!string.IsNullOrEmpty(localCache) && localCache.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase)) + { + GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache."); + _localCache = new MemoryCache(new MemoryCacheOptions()); + if (Config.GetValueOrEnvironmentVarOf("MAX_LOCAL_CACHE_TTL", out string maxCacheTtlMinutesStr) && long.TryParse(maxCacheTtlMinutesStr, out long maxCacheTtlMinutes)) + { + MAX_LOCAL_CACHE_TTL = TimeSpan.FromMinutes(maxCacheTtlMinutes); + GXLogging.Debug(log, $"MAX_LOCAL_CACHE_TTL read from config: {MAX_LOCAL_CACHE_TTL}"); + } + else + { + MAX_LOCAL_CACHE_TTL = TimeSpan.FromMinutes(MAX_LOCAL_CACHE_TTL_DEFAULT_MIMUTES); + GXLogging.Debug(log, $"MAX_LOCAL_CACHE_TTL using default value: {MAX_LOCAL_CACHE_TTL}"); + } + + MAX_LOCAL_CACHE_TTL_TICKS = MAX_LOCAL_CACHE_TTL.Ticks; + } + else + { + GXLogging.Debug(log, "Using Redis only mode without local memory cache."); } +#endif } + IDatabase RedisDatabase { get @@ -120,43 +155,65 @@ public void Clear(string cacheid, string key) public void ClearKey(string key) { RedisDatabase.KeyDelete(key); + ClearKeyLocal(key); } public void ClearCache(string cacheid) { Nullable prefix = new Nullable(KeyPrefix(cacheid).Value + 1); RedisDatabase.StringSet(cacheid, prefix); + SetPersistentLocal(cacheid, prefix); } public void ClearAllCaches() { - var endpoints = _redisConnection.GetEndPoints(true); + IConnectionMultiplexer multiplexer = RedisDatabase.Multiplexer; + System.Net.EndPoint[] endpoints = multiplexer.GetEndPoints(true); foreach (var endpoint in endpoints) { - var server = _redisConnection.GetServer(endpoint); + var server = multiplexer.GetServer(endpoint); server.FlushAllDatabases(); } + ClearAllCachesLocal(); } public bool KeyExpire(string cacheid, string key, TimeSpan expiry, CommandFlags flags = CommandFlags.None) { - Task t = RedisDatabase.KeyExpireAsync(Key(cacheid, key), expiry, flags); - t.Wait(); - return t.Result; + string fullKey = Key(cacheid, key); + bool expirationSaved = RedisDatabase.KeyExpire(fullKey, expiry, flags); + if (expirationSaved) + KeyExpireLocal(fullKey); + return expirationSaved; } public bool KeyExists(string cacheid, string key) { - Task t = RedisDatabase.KeyExistsAsync(Key(cacheid, key)); - t.Wait(); - return t.Result; + string fullKey = Key(cacheid, key); + + if (KeyExistsLocal(fullKey)) + { + GXLogging.Debug(log, $"KeyExists hit local cache {fullKey}"); + return true; + } + + return RedisDatabase.KeyExists(fullKey); } + private bool Get(string key, out T value) { + if (GetLocal(key, out value)) + { + GXLogging.Debug(log, $"Get hit local cache {key}"); + return true; + } + if (default(T) == null) { value = Deserialize(RedisDatabase.StringGet(key)); - if (value == null) GXLogging.Debug(log, "Get, misses key '" + key + "'"); + if (value == null) + GXLogging.Debug(log, "Get, misses key '" + key + "'"); + else + SetLocal(key, value); return value != null; } else @@ -164,6 +221,7 @@ private bool Get(string key, out T value) if (RedisDatabase.KeyExists(key)) { value = Deserialize(RedisDatabase.StringGet(key)); + SetLocal(key, value); return true; } else @@ -175,6 +233,81 @@ private bool Get(string key, out T value) } } +#if NETCORE + public IDictionary GetAll(string cacheid, IEnumerable keys) + { + if (keys == null) return null; + + var results = new Dictionary(); + var keysToFetch = new List(); + + foreach (string k in keys) + { + string fullKey = Key(cacheid, k); + if (GetLocal(fullKey, out T value)) + { + GXLogging.Debug(log, $"Get hit local cache {fullKey}"); + results[k] = value; + } + else + { + keysToFetch.Add(k); + } + } + + if (keysToFetch.Count > 0) + { + var prefixedKeys = Key(cacheid, keysToFetch); + RedisValue[] values = RedisDatabase.StringGet(prefixedKeys.ToArray()); + + int i = 0; + foreach (string k in keysToFetch) + { + string fullKey = Key(cacheid, k); + T value = Deserialize(values[i]); + results[k] = value; + + SetLocal(fullKey, value); + i++; + } + } + + return results; + } + public void SetAll(string cacheid, IEnumerable keys, IEnumerable values, int duration = 0) + { + if (keys == null || values == null || keys.Count() != values.Count()) + return; + + IEnumerable prefixedKeys = Key(cacheid, keys); + IEnumerator valuesEnumerator = values.GetEnumerator(); + KeyValuePair[] redisBatch = new KeyValuePair[prefixedKeys.Count()]; + + int i = 0; + foreach (RedisKey redisKey in prefixedKeys) + { + if (valuesEnumerator.MoveNext()) + { + T value = valuesEnumerator.Current; + redisBatch[i] = new KeyValuePair(redisKey, Serialize(value)); + SetLocal(redisKey.ToString(), value, duration); + } + i++; + } + if (redisBatch.Length > 0) + { + if (duration > 0) + { + foreach (var pair in redisBatch) + RedisDatabase.StringSet(pair.Key, pair.Value, TimeSpan.FromMinutes(duration)); + } + else + { + RedisDatabase.StringSet(redisBatch); + } + } + } +#else public IDictionary GetAll(string cacheid, IEnumerable keys) { if (keys != null) @@ -212,7 +345,7 @@ public void SetAll(string cacheid, IEnumerable keys, IEnumerable v RedisDatabase.StringSet(dictionary); } } - +#endif private void Set(string key, T value, int duration) { GXLogging.Debug(log, "Set key:" + key + " value " + value + " valuetype:" + value.GetType()); @@ -220,11 +353,12 @@ private void Set(string key, T value, int duration) RedisDatabase.StringSet(key, Serialize(value), TimeSpan.FromMinutes(duration)); else RedisDatabase.StringSet(key, Serialize(value)); + SetLocal(key, value, duration); } private void Set(string key, T value) { - RedisDatabase.StringSet(key, Serialize(value)); + Set(key, value, 0); } public bool Get(string cacheid, string key, out T value) @@ -293,5 +427,85 @@ static T Deserialize(string value) opts.Converters.Add(new ObjectToInferredTypesConverter()); return JsonSerializer.Deserialize(value, opts); } +#if NETCORE + private TimeSpan LocalCacheTTL(int durationMinutes) + { + return LocalCacheTTL(durationMinutes > 0 ? TimeSpan.FromMinutes(durationMinutes) : (TimeSpan?)null); + } + private TimeSpan LocalCacheTTL(TimeSpan? ttl) + { + if (ttl.HasValue) + { + double ttlTicks = ttl.Value.Ticks * DEFAULT_LOCAL_CACHE_FACTOR; + if (ttlTicks < MAX_LOCAL_CACHE_TTL_TICKS) + return ttl.Value; + } + return MAX_LOCAL_CACHE_TTL; + } +#endif + private void ClearKeyLocal(string key) + { +#if NETCORE + _localCache?.Remove(key); +#endif + } + void ClearAllCachesLocal() + { +#if NETCORE + _localCache?.Compact(1.0); +#endif + } + + private void KeyExpireLocal(string fullKey) + { +#if NETCORE + _localCache?.Remove(fullKey); +#endif + } + private bool KeyExistsLocal(string fullKey) + { +#if NETCORE + return _localCache?.TryGetValue(fullKey, out _) ?? false; +#else + return false; +#endif + } + + private void SetLocal(string key, T value) + { +#if NETCORE + if (_localCache != null) + { + TimeSpan? redisTTL = RedisDatabase.KeyTimeToLive(key); + _localCache.Set(key, value, LocalCacheTTL(redisTTL)); + } +#endif + } + private void SetPersistentLocal(string cacheid, long? prefix) + { +#if NETCORE + _localCache?.Set(cacheid, prefix, LocalCacheTTL(MAX_LOCAL_CACHE_TTL)); +#endif + } + private void SetLocal(string key, T value, int duration) + { +#if NETCORE + _localCache?.Set(key, value, LocalCacheTTL(duration)); +#endif + } + private bool GetLocal(string key, out T value) + { +#if NETCORE + if (_localCache == null) + { + value = default(T); + return false; + } + return _localCache.TryGetValue(key, out value); +#else + value = default(T); + return false; +#endif + } } } \ No newline at end of file