-
Notifications
You must be signed in to change notification settings - Fork 12
Reduce Redis EXPIRE calls using CustomRedisSessionStoreCustom redis session store #1221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
claudiamurialdo
wants to merge
3
commits into
master
Choose a base branch
from
custom-redis-session-store
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
|
|
@@ -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); | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
| } | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
| } | ||
| 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 | ||
| { | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?