1717using Microsoft . AspNetCore . Builder ;
1818using Microsoft . AspNetCore . DataProtection ;
1919using Microsoft . AspNetCore . Diagnostics ;
20+ using Microsoft . AspNetCore . Diagnostics . HealthChecks ;
2021using Microsoft . AspNetCore . Hosting ;
2122using Microsoft . AspNetCore . Http ;
2223using Microsoft . AspNetCore . Http . Features ;
3233using Microsoft . Extensions . Caching . SqlServer ;
3334using Microsoft . Extensions . Configuration ;
3435using Microsoft . Extensions . DependencyInjection ;
36+ using Microsoft . Extensions . Diagnostics . HealthChecks ;
3537using Microsoft . Extensions . FileProviders ;
3638using Microsoft . Extensions . Logging ;
3739using 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