Skip to content

Commit bb22cbb

Browse files
claudiamurialdoclaudiamurialdo
andauthored
Make distributed session loading asynchronous to improve scalability (#1195)
* Introduce CustomRedisSessionStore to reduce excessive EXPIRE operations in Redis and improve session handling performance. Converted ProcessRestRequest method in GXRouting.cs to fully async implementation. * Ensures sessions load asynchronously * Apply suggestions from FAzzatto -Simplified exception handling by replacing await Task.FromException(ex) with throw -Refactored to throw PageNotFoundException directly when the route cannot be handled, removed unnecessary result task, and moved CommitSessionAsync() to a finally block for clearer async flow Removed CustomRedisSessionStore which was previously used as an experiment to reduce synchronous Redis Refresh calls. Now that LoadAsync is executed at the start of each request, those synchronous Refresh calls no longer appear to be necessary, because they were caused by the previous synchronous load. * Refactored method to remove the result variable and await tasks directly, preserving the original behavior. --------- Co-authored-by: claudiamurialdo <c.murialdo@globant.com>
1 parent 2febeb8 commit bb22cbb

File tree

6 files changed

+119
-97
lines changed

6 files changed

+119
-97
lines changed

dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ public GXRouting(string baseURL)
6262
}
6363

6464
static public List<ControllerInfo> GetRouteController(Dictionary<string, string> apiPaths,
65-
Dictionary<string, List<string>> sValid,
66-
Dictionary<string, Dictionary<string, SingleMap>> sMap,
67-
Dictionary<string, Dictionary<Tuple<string, string>, string>> sMapData,
68-
string basePath, string verb, string path)
65+
Dictionary<string, List<string>> sValid,
66+
Dictionary<string, Dictionary<string, SingleMap>> sMap,
67+
Dictionary<string, Dictionary<Tuple<string, string>, string>> sMapData,
68+
string basePath, string verb, string path)
6969
{
7070
List<ControllerInfo> result = new List<ControllerInfo>();
7171
string parms = string.Empty;
@@ -78,10 +78,10 @@ static public List<ControllerInfo> GetRouteController(Dictionary<string, string>
7878
int questionMarkIdx = path.IndexOf(QUESTIONMARK);
7979
string controller;
8080
if (apiPaths.ContainsKey(basePath)
81-
&& sValid.ContainsKey(basePath)
82-
&& sMap.ContainsKey(basePath)
83-
&& sMapData.ContainsKey(basePath)
84-
)
81+
&& sValid.ContainsKey(basePath)
82+
&& sMap.ContainsKey(basePath)
83+
&& sMapData.ContainsKey(basePath)
84+
)
8585
{
8686
if (sValid[basePath].Contains(path.ToLower()))
8787
{
@@ -174,21 +174,20 @@ internal async Task RouteHttpService(HttpContext context)
174174
HandlerFactory handlerFactory = new HandlerFactory();
175175
await handlerFactory.Invoke(context);
176176
}
177-
catch (Exception ex)
177+
catch
178178
{
179-
await Task.FromException(ex);
179+
throw;
180180
}
181181
}
182-
public Task ProcessRestRequest(HttpContext context)
182+
public async Task ProcessRestRequest(HttpContext context)
183183
{
184184
try
185185
{
186-
if (context.GetType()==typeof(DefaultHttpContext))
186+
if (context.GetType() == typeof(DefaultHttpContext))
187187
{
188188
IHttpContextAccessor contextAccessor = context.RequestServices.GetService<IHttpContextAccessor>();
189189
context = new GxHttpContextAccesor(contextAccessor);
190190
}
191-
Task result = Task.CompletedTask;
192191
string path = context.Request.Path.ToString();
193192
string actualPath = string.Empty;
194193
bool isServiceInPath = ServiceInPath(path, out actualPath);
@@ -214,43 +213,46 @@ public Task ProcessRestRequest(HttpContext context)
214213
}
215214
string controllerPath = path.ToLower().Split(actualPath).Last<string>();
216215
controllerWithParms = controllerPath.Split(QUESTIONMARK).First<string>();
217-
216+
218217
}
219218
}
220219
else
221220
{
222-
if (path.Contains(oauthRoute) && (AzureDeploy.GAM == "true"))
223-
return (RouteHttpService(context));
221+
if (path.Contains(oauthRoute) && (AzureDeploy.GAM == "true"))
222+
{
223+
await (RouteHttpService(context));
224+
return;
225+
}
224226
controllerWithParms = GetGxRouteValue(path);
225227
GXLogging.Debug(log, $"Running Azure functions. ControllerWithParms :{controllerWithParms} path:{path}");
226228
}
227-
229+
228230
List<ControllerInfo> controllers = GetRouteController(servicesPathUrl, servicesValidPath, servicesMap, servicesMapData, actualPath, context.Request.Method, controllerWithParms);
229231
GxRestWrapper controller = null;
230232
ControllerInfo controllerInfo = controllers.FirstOrDefault(c => (controller = GetController(context, c)) != null);
231-
233+
232234

233235
if (controller != null)
234236
{
235237
if (HttpMethods.IsGet(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsGet(controllerInfo.Verb)))
236238
{
237-
result = controller.Get(controllerInfo.Parameters);
239+
await controller.Get(controllerInfo.Parameters);
238240
}
239241
else if (HttpMethods.IsPost(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsPost(controllerInfo.Verb)))
240242
{
241-
result = controller.Post();
243+
await controller.Post();
242244
}
243245
else if (HttpMethods.IsDelete(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsDelete(controllerInfo.Verb)))
244246
{
245-
result = controller.Delete(controllerInfo.Parameters);
247+
await controller.Delete(controllerInfo.Parameters);
246248
}
247249
else if (HttpMethods.IsPut(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsPut(controllerInfo.Verb)))
248250
{
249-
result = controller.Put(controllerInfo.Parameters);
251+
await controller.Put(controllerInfo.Parameters);
250252
}
251253
else if (HttpMethods.IsPatch(context.Request.Method) && (controllerInfo.Verb == null || HttpMethods.IsPatch(controllerInfo.Verb)))
252254
{
253-
result = controller.Patch(controllerInfo.Parameters);
255+
await controller.Patch(controllerInfo.Parameters);
254256
}
255257
else if (HttpMethods.IsOptions(context.Request.Method))
256258
{
@@ -284,18 +286,20 @@ public Task ProcessRestRequest(HttpContext context)
284286
{
285287
GXLogging.Error(log, $"ProcessRestRequest controller not found path:{path} controllerWithParms:{controllerWithParms}");
286288
context.Response.Headers.Clear();
287-
result = Task.FromException(new PageNotFoundException(path));
289+
throw new PageNotFoundException(path);
288290
}
289291
}
290-
context.CommitSession();
291-
292-
return result;
292+
293293
}
294294
catch (Exception ex)
295295
{
296296
GXLogging.Error(log, "ProcessRestRequest", ex);
297297
HttpHelper.SetUnexpectedError(context, HttpStatusCode.InternalServerError, ex);
298-
return Task.FromException(ex);
298+
throw;
299+
}
300+
finally
301+
{
302+
await context.CommitSessionAsync();
299303
}
300304
}
301305

@@ -374,20 +378,21 @@ public bool ServiceInPath(String path, out String actualPath)
374378
return true;
375379
}
376380
}
377-
else {
381+
else
382+
{
378383
return true;
379-
}
384+
}
380385
}
381386

382-
private String FindPath(string innerPath, Dictionary<string,string > servicesPathUrl, bool startTxt)
387+
private String FindPath(string innerPath, Dictionary<string, string> servicesPathUrl, bool startTxt)
383388
{
384389
string actualPath = String.Empty;
385-
foreach (string subPath in from String subPath in servicesPathUrl.Keys
386-
select subPath)
390+
foreach (string subPath in from String subPath in servicesPathUrl.Keys
391+
select subPath)
387392
{
388393
bool match = false;
389394
innerPath = innerPath.ToLower();
390-
match = (startTxt)? innerPath.StartsWith($"/{subPath.ToLower()}"): innerPath.Contains($"/{subPath.ToLower()}");
395+
match = (startTxt) ? innerPath.StartsWith($"/{subPath.ToLower()}") : innerPath.Contains($"/{subPath.ToLower()}");
391396
if (match)
392397
{
393398
actualPath = subPath.ToLower();
@@ -431,7 +436,7 @@ public GxRestWrapper GetController(HttpContext context, ControllerInfo controlle
431436
bool privateDirExists = Directory.Exists(privateDir);
432437

433438
GXLogging.Debug(log, $"PrivateDir:{privateDir} asssemblycontroller:{asssemblycontroller}");
434-
string svcFile=null;
439+
string svcFile = null;
435440
if (privateDirExists && File.Exists(Path.Combine(privateDir, $"{asssemblycontroller.ToLower()}.grp.json")))
436441
{
437442
controller = tmpController;
@@ -498,7 +503,7 @@ string SvcFile(string controller)
498503
GXLogging.Warn(log, "Service file not found:" + controllerFullName);
499504
return null;
500505
}
501-
506+
502507
}
503508
public void ServicesGroupSetting()
504509
{
@@ -524,7 +529,7 @@ public void ServicesGroupSetting()
524529
string mapPath = (m.BasePath.EndsWith("/")) ? m.BasePath : m.BasePath + "/";
525530
string mapPathLower = mapPath.ToLower();
526531
string mNameLower = m.Name.ToLower();
527-
servicesPathUrl[mapPathLower]= mNameLower;
532+
servicesPathUrl[mapPathLower] = mNameLower;
528533
if (!RestAPIHelpers.ServiceAsController())
529534
{
530535
GXLogging.Debug(log, $"addServicesPathUrl key:{mapPathLower} value:{mNameLower}");
@@ -575,7 +580,8 @@ public void ServicesGroupSetting()
575580
}
576581
}
577582
}
578-
}catch (Exception ex)
583+
}
584+
catch (Exception ex)
579585
{
580586
GXLogging.Error(log, $"Error Loading Services Group Settings", ex);
581587
throw;
@@ -676,7 +682,7 @@ public class Binding
676682

677683
[DataContract()]
678684
public class MapGroup
679-
{
685+
{
680686
String _objectType;
681687
String _name;
682688
String _basePath;

dotnet/src/dotnetcore/GxClasses.Web/Middleware/HandlerFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ public async Task Invoke(HttpContext context)
8989
}
9090
else
9191
{
92-
await Task.FromException(new PageNotFoundException(url));
92+
throw new PageNotFoundException(url);
9393
}
9494
}
9595
catch (Exception ex)
9696
{
9797
GXLogging.Error(log, $"Handler Factory failed creating {url}", ex);
98-
await Task.FromException(ex);
98+
throw;
9999
}
100100
}
101101
private static string ObjectUrl(string requestPath, string basePath)

dotnet/src/dotnetcore/GxClasses/Domain/HttpSessionState.cs

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Concurrent;
34
using System.Collections.Generic;
45
using System.Linq;
56
using System.Net;
67
using System.Reflection;
8+
using System.Threading;
79
using Microsoft.AspNetCore.Http;
810

911
namespace GeneXus.Http
@@ -32,34 +34,13 @@ public static CookieCollection GetCookies(this CookieContainer container)
3234
return allCookies;
3335
}
3436
}
35-
internal class LockTracker : IDisposable
37+
public static class LockTracker
3638
{
37-
private static Dictionary<string, LockTracker> _locks = new Dictionary<string, LockTracker>();
38-
private int _activeUses = 0;
39-
private readonly string _id;
39+
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
4040

41-
private LockTracker(string id) => _id = id;
42-
43-
internal static LockTracker Get(string id)
44-
{
45-
lock (_locks)
46-
{
47-
if (!_locks.ContainsKey(id))
48-
_locks.Add(id, new LockTracker(id));
49-
var res = _locks[id];
50-
res._activeUses += 1;
51-
return res;
52-
}
53-
}
54-
55-
void IDisposable.Dispose()
41+
public static SemaphoreSlim Get(string sessionId)
5642
{
57-
lock (_locks)
58-
{
59-
_activeUses--;
60-
if (_activeUses == 0)
61-
_locks.Remove(_id);
62-
}
43+
return _locks.GetOrAdd(sessionId, _ => new SemaphoreSlim(1, 1));
6344
}
6445
}
6546
internal class HttpSyncSessionState : HttpSessionState

dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
using Microsoft.AspNetCore.Http;
2-
using Microsoft.Extensions.Caching.Distributed;
3-
using Microsoft.Extensions.Caching.StackExchangeRedis;
1+
using System;
42
using System.Collections.Concurrent;
5-
using System.Threading.Tasks;
3+
using System.Linq;
64
using System.Threading;
7-
using System;
5+
using System.Threading.Tasks;
86
using GeneXus.Services;
9-
using System.Linq;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.Caching.Distributed;
9+
using Microsoft.Extensions.Caching.StackExchangeRedis;
10+
using StackExchange.Redis;
1011

1112
namespace GeneXus.Application
1213
{
@@ -63,9 +64,10 @@ public async Task Invoke(HttpContext context)
6364
{
6465
string host = context.Request.Host.Host;
6566
string subdomain = host.Split('.').FirstOrDefault();
66-
context.Items[TENANT_ID] = subdomain;
67+
context.Items[TENANT_ID] = subdomain;
6768

6869
await _next(context);
6970
}
7071
}
72+
7173
}

dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
474474
}
475475
app.UseRouting();
476476
app.UseCookiePolicy();
477-
app.UseSession();
477+
app.UseAsyncSession();
478478
app.UseStaticFiles();
479479

480480
ISessionService sessionService = GXSessionServiceFactory.GetProvider();
@@ -768,4 +768,36 @@ public void Apply(ApplicationModel application)
768768
}
769769
}
770770
}
771+
public static class SesssionAsyncExtensions
772+
{
773+
/// <summary>
774+
/// Ensures sessions load asynchronously by calling LoadAsync before accessing session data,
775+
/// forcing the session provider to avoid synchronous operations.
776+
/// </summary>
777+
/// <remarks>
778+
/// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-5.0
779+
/// The default session provider in ASP.NET Core will only load the session record from the underlying IDistributedCache store asynchronously if the
780+
/// ISession.LoadAsync method is explicitly called before calling the TryGetValue, Set or Remove methods.
781+
/// Failure to call LoadAsync first will result in the underlying session record being loaded synchronously,
782+
/// which could potentially impact the ability of an application to scale.
783+
///
784+
/// See also:
785+
/// https://github.com/aspnet/Session/blob/master/src/Microsoft.AspNetCore.Session/DistributedSession.cs
786+
/// https://github.com/dotnet/AspNetCore.Docs/issues/1840#issuecomment-454182594
787+
/// </remarks>
788+
public static IApplicationBuilder UseAsyncSession(this IApplicationBuilder app)
789+
{
790+
app.UseSession();
791+
app.Use(async (context, next) =>
792+
{
793+
if (context.Session != null)
794+
{
795+
await context.Session.LoadAsync();
796+
}
797+
await next();
798+
});
799+
return app;
800+
}
801+
}
771802
}
803+

0 commit comments

Comments
 (0)