From bb0f2a9101c32ef836667811f093db9c609e8a4e Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Wed, 15 Oct 2025 14:51:07 -0300 Subject: [PATCH 1/3] Introduce CustomRedisSessionStore to reduce excessive EXPIRE operations in Redis and improve session handling performance (cherry picked from commit 2f29b172ac63f4d3c68292728c9922c832597c16) # Conflicts: # dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs (cherry picked from commit 04bd601b216fc72cebb3d6b8d8ac67b800e204b0) # Conflicts: # dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs --- .../GxNetCoreStartup/SessionHelper.cs | 79 ++++++++++++++++--- .../dotnetcore/GxNetCoreStartup/Startup.cs | 11 ++- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs index 98e8eee23..07a9c3945 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs @@ -1,12 +1,12 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.StackExchangeRedis; +using System; using System.Collections.Concurrent; -using System.Threading.Tasks; +using System.Linq; using System.Threading; -using System; +using System.Threading.Tasks; using GeneXus.Services; -using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Distributed; +using StackExchange.Redis; namespace GeneXus.Application { @@ -29,12 +29,7 @@ private IDistributedCache GetTenantCache() return _redisCaches.GetOrAdd(tenantId, id => { ISessionService sessionService = GXSessionServiceFactory.GetProvider(); - var options = new RedisCacheOptions - { - Configuration = sessionService.ConnectionString, - InstanceName = $"{id}:" - }; - return new RedisCache(options); + return new CustomRedisSessionStore(sessionService.ConnectionString, TimeSpan.FromMinutes(sessionService.SessionTimeout), id); }); } @@ -48,6 +43,66 @@ private IDistributedCache GetTenantCache() public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => GetTenantCache().SetAsync(key, value, options, token); } + public class CustomRedisSessionStore : IDistributedCache + { + private readonly IDatabase _db; + private readonly TimeSpan _idleTimeout; + private readonly TimeSpan _refreshThreshold; + private readonly string _instanceName; + + public CustomRedisSessionStore(string connectionString, TimeSpan idleTimeout, string instanceName) + { + var mux = ConnectionMultiplexer.Connect(connectionString); + _db = mux.GetDatabase(); + _idleTimeout = idleTimeout; + _refreshThreshold = TimeSpan.FromTicks((long)(idleTimeout.Ticks * 0.2)); + _instanceName = instanceName ?? string.Empty; + } + + private string FormatKey(string key) => string.IsNullOrEmpty(_instanceName) ? key : $"{_instanceName}:{key}"; + + public byte[] Get(string key) => _db.StringGet(FormatKey(key)); + + public async Task GetAsync(string key, CancellationToken token = default) + { + string redisKey = FormatKey(key); + var value = await _db.StringGetAsync(redisKey); + + var ttl = await _db.KeyTimeToLiveAsync(redisKey); + + if (ttl.HasValue && ttl.Value < _refreshThreshold) + { + _ = _db.KeyExpireAsync(redisKey, _idleTimeout); + } + + return value; + } + + public void Refresh(string key) + { + string redisKey = FormatKey(key); + } + + public Task RefreshAsync(string key, CancellationToken token = default) + { + return Task.CompletedTask; + } + + public void Remove(string key) => _db.KeyDelete(FormatKey(key)); + + public Task RemoveAsync(string key, CancellationToken token = default) + => _db.KeyDeleteAsync(FormatKey(key)); + + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + _db.StringSet(FormatKey(key), value, _idleTimeout); + } + + public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) + { + return _db.StringSetAsync(FormatKey(key), value, _idleTimeout); + } + } public class TenantMiddleware { diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs index 7e11e9e7b..7c8fc52d2 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs @@ -468,12 +468,11 @@ private void ConfigureSessionService(IServiceCollection services, ISessionServic } else { - services.AddStackExchangeRedisCache(options => - { - GXLogging.Info(log, $"Using Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}"); - options.Configuration = sessionService.ConnectionString; - options.InstanceName = sessionService.InstanceName; - }); + GXLogging.Info(log, $"Using Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}"); + + services.AddSingleton(sp => + new CustomRedisSessionStore(sessionService.ConnectionString, TimeSpan.FromMinutes(Preferences.SessionTimeout), sessionService.InstanceName)); + services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(sessionService.ConnectionString), DATA_PROTECTION_KEYS).SetApplicationName(sessionService.InstanceName); } } From 45736658fde832ae21329ae8fcf6d830915c55cf Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Wed, 15 Oct 2025 17:33:04 -0300 Subject: [PATCH 2/3] Fix build error (cherry picked from commit aebd49a70668872aa8831f1af22d31fcc79d98f4) --- dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs index 07a9c3945..d68d9ebc6 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs @@ -13,13 +13,11 @@ namespace GeneXus.Application public class TenantRedisCache : IDistributedCache { private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IServiceProvider _serviceProvider; - private readonly ConcurrentDictionary _redisCaches = new(); + private readonly ConcurrentDictionary _redisCaches = new(); - public TenantRedisCache(IHttpContextAccessor httpContextAccessor, IServiceProvider serviceProvider) + public TenantRedisCache(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; - _serviceProvider = serviceProvider; } private IDistributedCache GetTenantCache() From dc5bb96909176aa4017a88144645a8895c4cfe2f Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Mon, 3 Nov 2025 11:48:11 -0300 Subject: [PATCH 3/3] Introduce CustomRedisSessionStore to reduce excessive EXPIRE operations in Redis and improve session handling performance --- .../GxNetCoreStartup/SessionHelper.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs index d68d9ebc6..46201846e 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs @@ -66,12 +66,7 @@ public async Task GetAsync(string key, CancellationToken token = default string redisKey = FormatKey(key); var value = await _db.StringGetAsync(redisKey); - var ttl = await _db.KeyTimeToLiveAsync(redisKey); - - if (ttl.HasValue && ttl.Value < _refreshThreshold) - { - _ = _db.KeyExpireAsync(redisKey, _idleTimeout); - } + await RefreshKeyIfNeededAsync(redisKey); return value; } @@ -79,13 +74,32 @@ public async Task GetAsync(string key, CancellationToken token = default public void Refresh(string key) { string redisKey = FormatKey(key); - } - public Task RefreshAsync(string key, CancellationToken token = default) + var ttl = _db.KeyTimeToLive(redisKey); + + if (ShouldRefreshKey(ttl)) + { + _db.KeyExpire(redisKey, _idleTimeout); + } + } + private bool ShouldRefreshKey(TimeSpan? ttl) { - return Task.CompletedTask; + return ttl.HasValue && ttl.Value < _refreshThreshold; } + public async Task RefreshAsync(string key, CancellationToken token = default) + { + string redisKey = FormatKey(key); + await RefreshKeyIfNeededAsync(redisKey); + } + private async Task RefreshKeyIfNeededAsync(string redisKey) + { + var ttl = await _db.KeyTimeToLiveAsync(redisKey); + if (ShouldRefreshKey(ttl)) + { + _ = _db.KeyExpireAsync(redisKey, _idleTimeout); + } + } public void Remove(string key) => _db.KeyDelete(FormatKey(key)); public Task RemoveAsync(string key, CancellationToken token = default)