Skip to content

Commit ce86f1f

Browse files
claudiamurialdoclaudiamurialdo
andauthored
Implement graceful shutdown with background thread completion (#1173)
Co-authored-by: claudiamurialdo <c.murialdo@globant.com>
1 parent b0613ab commit ce86f1f

File tree

1 file changed

+81
-52
lines changed

1 file changed

+81
-52
lines changed

dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Microsoft.AspNetCore.Builder;
1818
using Microsoft.AspNetCore.DataProtection;
1919
using Microsoft.AspNetCore.Diagnostics;
20+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
2021
using Microsoft.AspNetCore.Hosting;
2122
using Microsoft.AspNetCore.Http;
2223
using Microsoft.AspNetCore.Http.Features;
@@ -32,6 +33,7 @@
3233
using Microsoft.Extensions.Caching.SqlServer;
3334
using Microsoft.Extensions.Configuration;
3435
using Microsoft.Extensions.DependencyInjection;
36+
using Microsoft.Extensions.Diagnostics.HealthChecks;
3537
using Microsoft.Extensions.FileProviders;
3638
using Microsoft.Extensions.Logging;
3739
using StackExchange.Redis;
@@ -41,6 +43,7 @@ namespace GeneXus.Application
4143
public class Program
4244
{
4345
const string DEFAULT_PORT = "80";
46+
const int GRACEFUL_SHUTDOWN_DELAY_SECONDS = 30;
4447
static string DEFAULT_SCHEMA = Uri.UriSchemeHttp;
4548

4649
public static void Main(string[] args)
@@ -78,13 +81,14 @@ public static void Main(string[] args)
7881
{
7982
Console.Error.WriteLine("ERROR:");
8083
Console.Error.WriteLine("Web Host terminated unexpectedly: {0}", e.Message);
81-
}
84+
}
8285
}
8386

8487
public static IWebHost BuildWebHost(string[] args) =>
8588
WebHost.CreateDefaultBuilder(args)
8689
.UseStartup<Startup>()
8790
.UseContentRoot(Startup.LocalPath)
91+
.UseShutdownTimeout(TimeSpan.FromSeconds(GRACEFUL_SHUTDOWN_DELAY_SECONDS))
8892
.Build();
8993

9094
public static IWebHost BuildWebHostPort(string[] args, string port)
@@ -94,11 +98,12 @@ public static IWebHost BuildWebHostPort(string[] args, string port)
9498
static IWebHost BuildWebHostPort(string[] args, string port, string schema)
9599
{
96100
return WebHost.CreateDefaultBuilder(args)
97-
.UseUrls($"{schema}://*:{port}")
98-
.UseStartup<Startup>()
99-
.UseWebRoot(Startup.LocalPath)
100-
.UseContentRoot(Startup.LocalPath)
101-
.Build();
101+
.UseUrls($"{schema}://*:{port}")
102+
.UseStartup<Startup>()
103+
.UseWebRoot(Startup.LocalPath)
104+
.UseContentRoot(Startup.LocalPath)
105+
.UseShutdownTimeout(TimeSpan.FromSeconds(GRACEFUL_SHUTDOWN_DELAY_SECONDS))
106+
.Build();
102107
}
103108

104109
private static void LocatePhysicalLocalPath()
@@ -123,15 +128,15 @@ public static IApplicationBuilder UseGXHandlerFactory(this IApplicationBuilder b
123128
public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app, string basePath)
124129
{
125130
return app
126-
.Map($"{basePath}/gxwebsocket" , (_app) => _app.UseMiddleware<Notifications.WebSocket.WebSocketManagerMiddleware>())
127-
.Map($"{basePath}/gxwebsocket.svc", (_app) => _app.UseMiddleware<Notifications.WebSocket.WebSocketManagerMiddleware>()); //Compatibility reasons. Remove in the future.
131+
.Map($"{basePath}/gxwebsocket", (_app) => _app.UseMiddleware<Notifications.WebSocket.WebSocketManagerMiddleware>())
132+
.Map($"{basePath}/gxwebsocket.svc", (_app) => _app.UseMiddleware<Notifications.WebSocket.WebSocketManagerMiddleware>()); //Compatibility reasons. Remove in the future.
128133
}
129134
}
130135
public class CustomBadRequestObjectResult : ObjectResult
131136
{
132137
static readonly IGXLogger log = GXLoggerFactory.GetLogger(typeof(CustomBadRequestObjectResult).FullName);
133138
public CustomBadRequestObjectResult(ActionContext context)
134-
: base(HttpHelper.GetJsonError(StatusCodes.Status400BadRequest.ToString(), HttpHelper.StatusCodeToTitle(HttpStatusCode.BadRequest)))
139+
: base(HttpHelper.GetJsonError(StatusCodes.Status400BadRequest.ToString(), HttpHelper.StatusCodeToTitle(HttpStatusCode.BadRequest)))
135140
{
136141
LogErrorResponse(context);
137142
StatusCode = StatusCodes.Status400BadRequest;
@@ -180,7 +185,7 @@ public class Startup
180185
internal const string GX_CONTROLLERS = "gxcontrollers";
181186
internal static string DefaultFileName { get; set; }
182187

183-
public List<string> servicesBase = new List<string>();
188+
public List<string> servicesBase = new List<string>();
184189

185190
private GXRouting gxRouting;
186191
public Startup(IConfiguration configuration, IHostingEnvironment env)
@@ -197,6 +202,10 @@ public void ConfigureServices(IServiceCollection services)
197202
{
198203
OpenTelemetryService.Setup(services);
199204

205+
services.AddHealthChecks()
206+
.AddCheck("liveness", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
207+
.AddCheck("readiness", () => HealthCheckResult.Healthy(), tags: new[] { "ready" });
208+
200209
IMvcBuilder builder = services.AddMvc(option =>
201210
{
202211
option.EnableEndpointRouting = false;
@@ -249,7 +258,7 @@ public void ConfigureServices(IServiceCollection services)
249258
string sessionCookieName = GxWebSession.GetSessionCookieName(VirtualPath);
250259
if (!string.IsNullOrEmpty(sessionCookieName))
251260
{
252-
options.Cookie.Name=sessionCookieName;
261+
options.Cookie.Name = sessionCookieName;
253262
GxWebSession.SessionCookieName = sessionCookieName;
254263
}
255264
string sameSite;
@@ -274,20 +283,20 @@ public void ConfigureServices(IServiceCollection services)
274283
services.AddResponseCompression(options =>
275284
{
276285
options.MimeTypes = new[]
277-
{
278-
// Default
279-
"text/plain",
280-
"text/css",
281-
"application/javascript",
282-
"text/html",
283-
"application/xml",
284-
"text/xml",
285-
"application/json",
286-
"text/json",
287-
// Custom
288-
"application/json",
289-
"application/pdf"
290-
};
286+
{
287+
// Default
288+
"text/plain",
289+
"text/css",
290+
"application/javascript",
291+
"text/html",
292+
"application/xml",
293+
"text/xml",
294+
"application/json",
295+
"text/json",
296+
// Custom
297+
"application/json",
298+
"application/pdf"
299+
};
291300
options.EnableForHttps = true;
292301
});
293302
}
@@ -296,7 +305,7 @@ public void ConfigureServices(IServiceCollection services)
296305

297306
private void RegisterControllerAssemblies(IMvcBuilder mvcBuilder)
298307
{
299-
308+
300309
if (RestAPIHelpers.ServiceAsController())
301310
{
302311
mvcBuilder.AddMvcOptions(options => options.ModelBinderProviders.Insert(0, new QueryStringModelBinderProvider()));
@@ -365,9 +374,9 @@ private void RegisterRestServices(IMvcBuilder mvcBuilder)
365374
try
366375
{
367376
string[] controllerAssemblyQualifiedName = new string(File.ReadLines(svcFile).First().SkipWhile(c => c != '"')
368-
.Skip(1)
369-
.TakeWhile(c => c != '"')
370-
.ToArray()).Trim().Split(',');
377+
.Skip(1)
378+
.TakeWhile(c => c != '"')
379+
.ToArray()).Trim().Split(',');
371380
string controllerAssemblyName = controllerAssemblyQualifiedName.Last();
372381
if (!serviceAssemblies.Contains(controllerAssemblyName))
373382
{
@@ -428,25 +437,25 @@ private void DefineCorsPolicy(IServiceCollection services)
428437
services.AddCors(options =>
429438
{
430439
options.AddPolicy(name: CORS_POLICY_NAME,
431-
policy =>
432-
{
433-
policy.WithOrigins(origins);
434-
if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN))
435-
{
436-
policy.AllowCredentials();
437-
}
438-
policy.AllowAnyHeader();
439-
policy.AllowAnyMethod();
440-
policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS));
441-
});
440+
policy =>
441+
{
442+
policy.WithOrigins(origins);
443+
if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN))
444+
{
445+
policy.AllowCredentials();
446+
}
447+
policy.AllowAnyHeader();
448+
policy.AllowAnyMethod();
449+
policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS));
450+
});
442451
});
443452
}
444453
}
445454
}
446455

447456
private void ConfigureSessionService(IServiceCollection services, ISessionService sessionService)
448457
{
449-
458+
450459
if (sessionService is GxRedisSession)
451460
{
452461
GxRedisSession gxRedisSession = (GxRedisSession)sessionService;
@@ -504,8 +513,11 @@ private void ConfigureSessionService(IServiceCollection services, ISessionServic
504513
}
505514
}
506515
}
507-
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env, ILoggerFactory loggerFactory, IHttpContextAccessor contextAccessor)
516+
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env, ILoggerFactory loggerFactory, IHttpContextAccessor contextAccessor, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime)
508517
{
518+
// Registrar para el graceful shutdown
519+
applicationLifetime.ApplicationStopping.Register(OnShutdown);
520+
509521
string baseVirtualPath = string.IsNullOrEmpty(VirtualPath) ? VirtualPath : $"/{VirtualPath}";
510522
LogConfiguration.SetupLog4Net();
511523
AppContext.Configure(contextAccessor);
@@ -567,6 +579,17 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
567579
app.UseEndpoints(endpoints =>
568580
{
569581
endpoints.MapControllers();
582+
583+
// Endpoints para health checks (Kubernetes probes)
584+
endpoints.MapHealthChecks($"{baseVirtualPath}/_gx/health/live", new HealthCheckOptions
585+
{
586+
Predicate = check => check.Tags.Contains("live")
587+
});
588+
589+
endpoints.MapHealthChecks($"{baseVirtualPath }/_gx/health/ready", new HealthCheckOptions
590+
{
591+
Predicate = check => check.Tags.Contains("ready")
592+
});
570593
});
571594
if (log.IsCriticalEnabled && env.IsDevelopment())
572595
{
@@ -619,7 +642,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
619642
},
620643
ContentTypeProvider = provider
621644
});
622-
645+
623646
app.UseExceptionHandler(new ExceptionHandlerOptions
624647
{
625648
ExceptionHandler = new CustomExceptionHandlerMiddleware().Invoke,
@@ -662,7 +685,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
662685

663686
app.UseGXHandlerFactory(basePath);
664687

665-
app.Run(async context =>
688+
app.Run(async context =>
666689
{
667690
await Task.FromException(new PageNotFoundException(context.Request.Path.Value));
668691
});
@@ -688,13 +711,13 @@ private void ConfigureSwaggerUI(IApplicationBuilder app, string baseVirtualPath)
688711
app.UseSwaggerUI(options =>
689712
{
690713
options.SwaggerEndpoint($"../../{finfo.Name}", finfo.Name);
691-
options.RoutePrefix =$"{baseVirtualPathWithSep}{finfo.Name}/{SWAGGER_SUFFIX}";
714+
options.RoutePrefix = $"{baseVirtualPathWithSep}{finfo.Name}/{SWAGGER_SUFFIX}";
692715
});
693716
if (finfo.Name.Equals(SWAGGER_DEFAULT_YAML, StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(LocalPath, DEVELOPER_MENU)))
694717
app.UseSwaggerUI(options =>
695718
{
696719
options.SwaggerEndpoint($"../../{SWAGGER_DEFAULT_YAML}", SWAGGER_DEFAULT_YAML);
697-
options.RoutePrefix =$"{baseVirtualPathWithSep}{DEVELOPER_MENU}/{SWAGGER_SUFFIX}";
720+
options.RoutePrefix = $"{baseVirtualPathWithSep}{DEVELOPER_MENU}/{SWAGGER_SUFFIX}";
698721
});
699722

700723
}
@@ -705,11 +728,17 @@ private void ConfigureSwaggerUI(IApplicationBuilder app, string baseVirtualPath)
705728
}
706729
}
707730

731+
private void OnShutdown()
732+
{
733+
GXLogging.Info(log, "Application gracefully shutting down... Waiting for in-process requests to complete.");
734+
ThreadUtil.WaitForEnd();
735+
}
736+
708737
private void AddRewrite(IApplicationBuilder app, string rewriteFile, string baseURL)
709738
{
710739
string rules = File.ReadAllText(rewriteFile);
711740
rules = rules.Replace("{BASEURL}", baseURL);
712-
741+
713742
using (var apacheModRewriteStreamReader = new StringReader(rules))
714743
{
715744
var options = new RewriteOptions().AddApacheModRewrite(apacheModRewriteStreamReader);
@@ -722,10 +751,10 @@ public class CustomExceptionHandlerMiddleware
722751
static readonly IGXLogger log = GXLoggerFactory.GetLogger<CustomExceptionHandlerMiddleware>();
723752
public async Task Invoke(HttpContext httpContext)
724753
{
725-
string httpReasonPhrase=string.Empty;
754+
string httpReasonPhrase = string.Empty;
726755
Exception ex = httpContext.Features.Get<IExceptionHandlerFeature>()?.Error;
727756
HttpStatusCode httpStatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
728-
if (ex!=null)
757+
if (ex != null)
729758
{
730759
if (ex is PageNotFoundException)
731760
{
@@ -743,7 +772,7 @@ public async Task Invoke(HttpContext httpContext)
743772
GXLogging.Error(log, $"Internal error", ex);
744773
}
745774
}
746-
if (httpStatusCode!= HttpStatusCode.OK)
775+
if (httpStatusCode != HttpStatusCode.OK)
747776
{
748777
string redirectPage = Config.MapCustomError(httpStatusCode.ToString(HttpHelper.INT_FORMAT));
749778
if (!string.IsNullOrEmpty(redirectPage))
@@ -757,7 +786,7 @@ public async Task Invoke(HttpContext httpContext)
757786
if (!string.IsNullOrEmpty(httpReasonPhrase))
758787
{
759788
IHttpResponseFeature responseReason = httpContext.Response.HttpContext.Features.Get<IHttpResponseFeature>();
760-
if (responseReason!=null)
789+
if (responseReason != null)
761790
responseReason.ReasonPhrase = httpReasonPhrase;
762791
}
763792
}
@@ -814,7 +843,7 @@ public IActionResult Index()
814843
}
815844
internal class SetRoutePrefix : IApplicationModelConvention
816845
{
817-
private readonly AttributeRouteModel _routePrefix ;
846+
private readonly AttributeRouteModel _routePrefix;
818847
public SetRoutePrefix(IRouteTemplateProvider route)
819848
{
820849
_routePrefix = new AttributeRouteModel(route);

0 commit comments

Comments
 (0)