Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 83 additions & 16 deletions dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
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
{
public class TenantRedisCache : IDistributedCache
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<string, RedisCache> _redisCaches = new();
private readonly ConcurrentDictionary<string, IDistributedCache> _redisCaches = new();

public TenantRedisCache(IHttpContextAccessor httpContextAccessor, IServiceProvider serviceProvider)
public TenantRedisCache(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_serviceProvider = serviceProvider;
}

private IDistributedCache GetTenantCache()
Expand All @@ -29,12 +27,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);
});
}

Expand All @@ -48,6 +41,80 @@ 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<byte[]> GetAsync(string key, CancellationToken token = default)
{
string redisKey = FormatKey(key);
var value = await _db.StringGetAsync(redisKey);

await RefreshKeyIfNeededAsync(redisKey);

return value;
}

public void Refresh(string key)
{
string redisKey = FormatKey(key);

var ttl = _db.KeyTimeToLive(redisKey);

if (ShouldRefreshKey(ttl))
{
_db.KeyExpire(redisKey, _idleTimeout);
}
Comment on lines +78 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. How checking for TTL before invoking expire results in performance gains, if both must reach a Redis?

}
private bool ShouldRefreshKey(TimeSpan? ttl)
{
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);
Comment on lines +96 to +100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really more performant to check for the TTL (invoking a Redis operation) before expiring the key? Isn't the Redis endpoint hit anyway?

Shouldn't the invocation to KeyExpireAsync be awaited?

}
}
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
{
Expand Down
11 changes: 5 additions & 6 deletions dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDistributedCache>(sp =>
new CustomRedisSessionStore(sessionService.ConnectionString, TimeSpan.FromMinutes(Preferences.SessionTimeout), sessionService.InstanceName));

services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(sessionService.ConnectionString), DATA_PROTECTION_KEYS).SetApplicationName(sessionService.InstanceName);
}
}
Expand Down
Loading