Skip to content

Commit 6693d3b

Browse files
claudiamurialdoclaudiamurialdo
andauthored
Add local memory cache to Redis implementation to reduce roundtrips - release-1.30 (#1216)
* Add local memory cache to Redis implementation to reduce roundtrips (cherry picked from commit 2b922f6) # Conflicts: # dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj * It seems better to keep GxRedis.cs unified for both .NET Framework and .NET Core. (cherry picked from commit f97a253) # Conflicts: # dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs * Applied Fazzato’s review suggestions (cherry picked from commit e46abf1) * Fix build error * Do not change version of StackExchange.Redis * Read ENABLE_MEMORY_CACHE from provider settings --------- Co-authored-by: claudiamurialdo <[email protected]>
1 parent 9bfff7a commit 6693d3b

File tree

5 files changed

+223
-21
lines changed

5 files changed

+223
-21
lines changed

dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
[assembly: InternalsVisibleTo("DotNetCoreChunkedTest")]
1919
[assembly: InternalsVisibleTo("DotNetCoreChunkedTest")]
2020
[assembly: InternalsVisibleTo("GeneXus.OpenTelemetry.Diagnostics")]
21+
[assembly: InternalsVisibleTo("GxRedis")]

dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ public class GXSessionServiceFactory
1313
private static readonly IGXLogger log = GXLoggerFactory.GetLogger<GXSessionServiceFactory>();
1414

1515
static ISessionService sessionService;
16-
static string REDIS = "REDIS";
17-
static string DATABASE = "DATABASE";
1816
public static ISessionService GetProvider()
1917
{
2018
if (sessionService != null)
@@ -29,9 +27,9 @@ public static ISessionService GetProvider()
2927
//Compatibility
3028
if (string.IsNullOrEmpty(className))
3129
{
32-
if (providerService.Name.Equals(REDIS, StringComparison.OrdinalIgnoreCase))
30+
if (providerService.Name.Equals(GXServices.REDIS_CACHE_SERVICE, StringComparison.OrdinalIgnoreCase))
3331
type = typeof(GxRedisSession);
34-
else if (providerService.Name.Equals(DATABASE, StringComparison.OrdinalIgnoreCase))
32+
else if (providerService.Name.Equals(GXServices.DATABASE_CACHE_SERVICE, StringComparison.OrdinalIgnoreCase))
3533
type = typeof(GxDatabaseSession);
3634
}
3735
else

dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2-
<PropertyGroup>
2+
<PropertyGroup>
33
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
44
<PackageTags>Redis</PackageTags>
55
<PackageId>GeneXus.Redis.Core</PackageId>
6+
<DefineConstants>NETCORE</DefineConstants>
67
</PropertyGroup>
78
<PropertyGroup>
89
<AppDesignerFolder>Properties</AppDesignerFolder>
@@ -15,7 +16,7 @@
1516
</PropertyGroup>
1617

1718
<ItemGroup>
18-
<Compile Include="..\..\..\..\dotnetframework\Providers\Cache\GxRedis\GxRedis.cs" Link="GxRedis.cs" />
19+
<Compile Include="..\..\..\..\dotnetframework\Providers\Cache\GxRedis\GxRedis.cs" Link="GxRedis.cs" />
1920
</ItemGroup>
2021
<ItemGroup>
2122
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />

dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class GXServices
1515
public static string STORAGE_SERVICE = "Storage";
1616
public static string STORAGE_APISERVICE = "StorageAPI";
1717
public static string CACHE_SERVICE = "Cache";
18+
public static string REDIS_CACHE_SERVICE = "Redis";
19+
public static string DATABASE_CACHE_SERVICE = "DATABASE";
1820
public static string DATA_ACCESS_SERVICE = "DataAccess";
1921
public static string SESSION_SERVICE = "Session";
2022
public static string WEBNOTIFICATIONS_SERVICE = "WebNotifications";
@@ -47,6 +49,7 @@ public static GXServices Instance
4749
}
4850
set { }
4951
}
52+
5053
public void AddService(string name, GXService service)
5154
{
5255
services[name] = service;

dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs

Lines changed: 214 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Text.Json;
5-
using System.Threading.Tasks;
5+
#if NETCORE
6+
using GeneXus.Application;
7+
using GxClasses.Helpers;
8+
using Microsoft.Extensions.Caching.Memory;
9+
#endif
610
using GeneXus.Services;
711
using GeneXus.Utils;
812
using StackExchange.Redis;
@@ -15,19 +19,24 @@ public sealed class Redis : ICacheService2
1519

1620
ConnectionMultiplexer _redisConnection;
1721
IDatabase _redisDatabase;
22+
#if NETCORE
23+
MemoryCache _localCache;
24+
private const double DEFAULT_LOCAL_CACHE_FACTOR = 0.8;
25+
private static readonly TimeSpan LOCAL_CACHE_PERSISTENT_KEY_TTL = TimeSpan.FromMinutes(5);
26+
27+
#endif
1828
ConfigurationOptions _redisConnectionOptions;
1929
private const int REDIS_DEFAULT_PORT = 6379;
2030
public int redisSessionTimeout;
31+
2132
public Redis(string connectionString)
2233
{
2334
_redisConnectionOptions = ConfigurationOptions.Parse(connectionString);
2435
_redisConnectionOptions.AllowAdmin = true;
2536
}
2637

27-
public Redis(string connectionString, int sessionTimeout)
38+
public Redis(string connectionString, int sessionTimeout):this(connectionString)
2839
{
29-
_redisConnectionOptions = ConfigurationOptions.Parse(connectionString);
30-
_redisConnectionOptions.AllowAdmin = true;
3140
redisSessionTimeout = sessionTimeout;
3241
}
3342
public Redis()
@@ -56,8 +65,25 @@ public Redis()
5665
_redisConnectionOptions = ConfigurationOptions.Parse(address);
5766
}
5867
_redisConnectionOptions.AllowAdmin = true;
68+
InitLocalCache(providerService);
5969
}
6070
}
71+
private void InitLocalCache(GXService providerService)
72+
{
73+
#if NETCORE
74+
string localCache = providerService.Properties.Get("ENABLE_MEMORY_CACHE");
75+
if (!string.IsNullOrEmpty(localCache) && localCache.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase))
76+
{
77+
GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache.");
78+
_localCache = new MemoryCache(new MemoryCacheOptions());
79+
}
80+
else
81+
{
82+
GXLogging.Debug(log, "Using Redis only mode without local memory cache.");
83+
}
84+
#endif
85+
}
86+
6187
IDatabase RedisDatabase
6288
{
6389
get
@@ -78,50 +104,73 @@ public void Clear(string cacheid, string key)
78104
public void ClearKey(string key)
79105
{
80106
RedisDatabase.KeyDelete(key);
107+
ClearKeyLocal(key);
81108
}
82109

83110
public void ClearCache(string cacheid)
84111
{
85112
Nullable<long> prefix = new Nullable<long>(KeyPrefix(cacheid).Value + 1);
86113
RedisDatabase.StringSet(cacheid, prefix);
114+
SetPersistentLocal(cacheid, prefix);
87115
}
88116

89117
public void ClearAllCaches()
90118
{
91-
var endpoints = _redisConnection.GetEndPoints(true);
119+
IConnectionMultiplexer multiplexer = RedisDatabase.Multiplexer;
120+
System.Net.EndPoint[] endpoints = multiplexer.GetEndPoints(true);
92121
foreach (var endpoint in endpoints)
93122
{
94-
var server = _redisConnection.GetServer(endpoint);
123+
var server = multiplexer.GetServer(endpoint);
95124
server.FlushAllDatabases();
96125
}
126+
ClearAllCachesLocal();
97127
}
98128

99129
public bool KeyExpire(string cacheid, string key, TimeSpan expiry, CommandFlags flags = CommandFlags.None)
100130
{
101-
Task<bool> t = RedisDatabase.KeyExpireAsync(Key(cacheid, key), expiry, flags);
102-
t.Wait();
103-
return t.Result;
131+
string fullKey = Key(cacheid, key);
132+
bool expirationSaved = RedisDatabase.KeyExpire(fullKey, expiry, flags);
133+
if (expirationSaved)
134+
KeyExpireLocal(fullKey);
135+
return expirationSaved;
104136
}
105137

106138
public bool KeyExists(string cacheid, string key)
107139
{
108-
Task<bool> t = RedisDatabase.KeyExistsAsync(Key(cacheid, key));
109-
t.Wait();
110-
return t.Result;
140+
string fullKey = Key(cacheid, key);
141+
142+
if (KeyExistsLocal(fullKey))
143+
{
144+
GXLogging.Debug(log, $"KeyExists hit local cache {fullKey}");
145+
return true;
146+
}
147+
148+
return RedisDatabase.KeyExists(fullKey);
111149
}
150+
112151
private bool Get<T>(string key, out T value)
113152
{
153+
if (GetLocal(key, out value))
154+
{
155+
GXLogging.Debug(log, $"Get<T> hit local cache {key}");
156+
return true;
157+
}
158+
114159
if (default(T) == null)
115160
{
116161
value = Deserialize<T>(RedisDatabase.StringGet(key));
117-
if (value == null) GXLogging.Debug(log, "Get<T>, misses key '" + key + "'");
162+
if (value == null)
163+
GXLogging.Debug(log, "Get<T>, misses key '" + key + "'");
164+
else
165+
SetLocal(key, value);
118166
return value != null;
119167
}
120168
else
121169
{
122170
if (RedisDatabase.KeyExists(key))
123171
{
124172
value = Deserialize<T>(RedisDatabase.StringGet(key));
173+
SetLocal(key, value);
125174
return true;
126175
}
127176
else
@@ -133,6 +182,81 @@ private bool Get<T>(string key, out T value)
133182
}
134183
}
135184

185+
#if NETCORE
186+
public IDictionary<string, T> GetAll<T>(string cacheid, IEnumerable<string> keys)
187+
{
188+
if (keys == null) return null;
189+
190+
var results = new Dictionary<string, T>();
191+
var keysToFetch = new List<string>();
192+
193+
foreach (string k in keys)
194+
{
195+
string fullKey = Key(cacheid, k);
196+
if (GetLocal<T>(fullKey, out T value))
197+
{
198+
GXLogging.Debug(log, $"Get<T> hit local cache {fullKey}");
199+
results[k] = value;
200+
}
201+
else
202+
{
203+
keysToFetch.Add(k);
204+
}
205+
}
206+
207+
if (keysToFetch.Count > 0)
208+
{
209+
var prefixedKeys = Key(cacheid, keysToFetch);
210+
RedisValue[] values = RedisDatabase.StringGet(prefixedKeys.ToArray());
211+
212+
int i = 0;
213+
foreach (string k in keysToFetch)
214+
{
215+
string fullKey = Key(cacheid, k);
216+
T value = Deserialize<T>(values[i]);
217+
results[k] = value;
218+
219+
SetLocal(fullKey, value);
220+
i++;
221+
}
222+
}
223+
224+
return results;
225+
}
226+
public void SetAll<T>(string cacheid, IEnumerable<string> keys, IEnumerable<T> values, int duration = 0)
227+
{
228+
if (keys == null || values == null || keys.Count() != values.Count())
229+
return;
230+
231+
IEnumerable<RedisKey> prefixedKeys = Key(cacheid, keys);
232+
IEnumerator<T> valuesEnumerator = values.GetEnumerator();
233+
KeyValuePair<RedisKey, RedisValue>[] redisBatch = new KeyValuePair<RedisKey, RedisValue>[prefixedKeys.Count()];
234+
235+
int i = 0;
236+
foreach (RedisKey redisKey in prefixedKeys)
237+
{
238+
if (valuesEnumerator.MoveNext())
239+
{
240+
T value = valuesEnumerator.Current;
241+
redisBatch[i] = new KeyValuePair<RedisKey, RedisValue>(redisKey, Serialize(value));
242+
SetLocal<T>(redisKey.ToString(), value, duration);
243+
}
244+
i++;
245+
}
246+
if (redisBatch.Length > 0)
247+
{
248+
if (duration > 0)
249+
{
250+
foreach (var pair in redisBatch)
251+
RedisDatabase.StringSet(pair.Key, pair.Value, TimeSpan.FromMinutes(duration));
252+
}
253+
else
254+
{
255+
RedisDatabase.StringSet(redisBatch);
256+
}
257+
}
258+
}
259+
#else
136260
public IDictionary<string, T> GetAll<T>(string cacheid, IEnumerable<string> keys)
137261
{
138262
if (keys != null)
@@ -170,19 +294,20 @@ public void SetAll<T>(string cacheid, IEnumerable<string> keys, IEnumerable<T> v
170294
RedisDatabase.StringSet(dictionary);
171295
}
172296
}
173-
297+
#endif
174298
private void Set<T>(string key, T value, int duration)
175299
{
176300
GXLogging.Debug(log, "Set<T> key:" + key + " value " + value + " valuetype:" + value.GetType());
177301
if (duration > 0)
178302
RedisDatabase.StringSet(key, Serialize(value), TimeSpan.FromMinutes(duration));
179303
else
180304
RedisDatabase.StringSet(key, Serialize(value));
305+
SetLocal(key, value, duration);
181306
}
182307

183308
private void Set<T>(string key, T value)
184309
{
185-
RedisDatabase.StringSet(key, Serialize(value));
310+
Set<T>(key, value, 0);
186311
}
187312

188313
public bool Get<T>(string cacheid, string key, out T value)
@@ -245,5 +370,79 @@ static T Deserialize<T>(string value)
245370
opts.Converters.Add(new ObjectToInferredTypesConverter());
246371
return JsonSerializer.Deserialize<T>(value, opts);
247372
}
373+
#if NETCORE
374+
private TimeSpan LocalCacheTTL(int durationMinutes)
375+
{
376+
return LocalCacheTTL(durationMinutes > 0 ? TimeSpan.FromMinutes(durationMinutes) : (TimeSpan?)null);
377+
}
378+
private TimeSpan LocalCacheTTL(TimeSpan? ttl)
379+
{
380+
return ttl.HasValue ? TimeSpan.FromTicks((long)(ttl.Value.Ticks * DEFAULT_LOCAL_CACHE_FACTOR)) : LOCAL_CACHE_PERSISTENT_KEY_TTL;
381+
}
382+
#endif
383+
private void ClearKeyLocal(string key)
384+
{
385+
#if NETCORE
386+
_localCache?.Remove(key);
387+
#endif
388+
}
389+
void ClearAllCachesLocal()
390+
{
391+
#if NETCORE
392+
_localCache?.Compact(1.0);
393+
#endif
394+
}
395+
396+
private void KeyExpireLocal(string fullKey)
397+
{
398+
#if NETCORE
399+
_localCache?.Remove(fullKey);
400+
#endif
401+
}
402+
private bool KeyExistsLocal(string fullKey)
403+
{
404+
#if NETCORE
405+
return _localCache?.TryGetValue(fullKey, out _) ?? false;
406+
#else
407+
return false;
408+
#endif
409+
}
410+
411+
private void SetLocal<T>(string key, T value)
412+
{
413+
#if NETCORE
414+
if (_localCache != null)
415+
{
416+
TimeSpan? redisTTL = RedisDatabase.KeyTimeToLive(key);
417+
_localCache.Set(key, value, LocalCacheTTL(redisTTL));
418+
}
419+
#endif
420+
}
421+
private void SetPersistentLocal(string cacheid, long? prefix)
422+
{
423+
#if NETCORE
424+
_localCache?.Set(cacheid, prefix, LocalCacheTTL(LOCAL_CACHE_PERSISTENT_KEY_TTL));
425+
#endif
426+
}
427+
private void SetLocal<T>(string key, T value, int duration)
428+
{
429+
#if NETCORE
430+
_localCache?.Set(key, value, LocalCacheTTL(duration));
431+
#endif
432+
}
433+
private bool GetLocal<T>(string key, out T value)
434+
{
435+
#if NETCORE
436+
if (_localCache == null)
437+
{
438+
value = default(T);
439+
return false;
440+
}
441+
return _localCache.TryGetValue(key, out value);
442+
#else
443+
value = default(T);
444+
return false;
445+
#endif
446+
}
248447
}
249448
}

0 commit comments

Comments
 (0)