From a900abf2615076b51c197ecc6feaddcbc3e91988 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Wed, 27 Oct 2021 23:18:59 +0000 Subject: [PATCH] --- .vscode/launch.json | 2 +- .../AyaNova/Controllers/ReportController.cs | 2 +- server/AyaNova/biz/ReportBiz.cs | 7 +- server/AyaNova/util/ReportProcessManager.cs | 86 +++++++++++++------ 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d762b8d3..2f386ae8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -50,7 +50,7 @@ "AYANOVA_DB_CONNECTION": "Server=localhost;Username=postgres;Password=raven;Database=AyaNova;CommandTimeout=120;", //"AYANOVA_DB_CONNECTION": "Server=localhost;Username=postgres;Password=abraxis;Database=AyaNova;CommandTimeout=120;", "AYANOVA_USE_URLS": "http://*:7575;", - "AYANOVA_REPORT_RENDERING_TIMEOUT": "5000", + "AYANOVA_REPORT_RENDERING_TIMEOUT": "2000", "AYANOVA_REPORT_RENDERING_MAX_INSTANCES": "2", "AYANOVA_FOLDER_USER_FILES": "c:\\temp\\RavenTestData\\userfiles", "AYANOVA_FOLDER_BACKUP_FILES": "c:\\temp\\RavenTestData\\backupfiles", diff --git a/server/AyaNova/Controllers/ReportController.cs b/server/AyaNova/Controllers/ReportController.cs index 9b48231c..b3ca0469 100644 --- a/server/AyaNova/Controllers/ReportController.cs +++ b/server/AyaNova/Controllers/ReportController.cs @@ -200,7 +200,7 @@ namespace AyaNova.Api.Controllers //Note that this *should* normally return a 503 however we're pretty tightly wired into that meaning the server is closed at the client end which //handles it at a lower level //returning an OK method here allows the client to handle it at the level of the report dialog rather than the api handler which will short circuit if it was a 503 - if (!Util.ReportRenderManager.RenderSlotAvailable()) + if (!Util.ReportRenderManager.RenderSlotAvailable(log)) return Ok(ApiOkResponse.Response(new { busy = true, retryafter = DateTime.UtcNow.AddMilliseconds(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT) })); ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index 1ebb803d..9149655f 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -434,7 +434,7 @@ namespace AyaNova.Biz log.LogDebug("Initializing report system"); var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); - var lo = new LaunchOptions { Headless = true }; + var lo = new LaunchOptions { Headless = true }; bool isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); if (!isWindows) { @@ -489,7 +489,7 @@ namespace AyaNova.Biz using (var browser = await Puppeteer.LaunchAsync(lo)) using (var page = await browser.NewPageAsync()) { - //mark this process so it can be cancelled if it times out + //track this process so it can be cancelled if it times out ReportRenderManager.AddProcess(browser.Process.Id); try @@ -708,7 +708,7 @@ namespace AyaNova.Biz //render to pdf and return log.LogDebug($"Calling render page contents to PDF"); - + await page.PdfAsync(outputFullPath, PdfOptions);//###### TODO: SLOW NEEDS TIMEOUT HERE ONCE SUPPORTED, open bug: https://github.com/hardkoded/puppeteer-sharp/issues/1710 log.LogDebug($"Completed, returning results"); @@ -732,6 +732,7 @@ namespace AyaNova.Biz { log.LogDebug($"Closing browser"); await browser.CloseAsync(); + ReportRenderManager.RemoveProcess(browser.Process.Id, log); } } } diff --git a/server/AyaNova/util/ReportProcessManager.cs b/server/AyaNova/util/ReportProcessManager.cs index ac5a113e..e32d364c 100644 --- a/server/AyaNova/util/ReportProcessManager.cs +++ b/server/AyaNova/util/ReportProcessManager.cs @@ -44,55 +44,77 @@ namespace AyaNova.Util public class ReportRenderInstanceInfo { public int ReporterProcessId { get; set; } - public DateTime Started { get; set; } + public DateTime Expires { get; set; } public ReportRenderInstanceInfo(int processId) { ReporterProcessId = processId; - Started = DateTime.UtcNow; + Expires = DateTime.UtcNow.AddMilliseconds(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT); } } - public static bool RenderSlotAvailable() + public static bool RenderSlotAvailable(ILogger log) { - if (_baginstances.Count >= ServerBootConfig.AYANOVA_REPORT_RENDERING_MAX_INSTANCES) + log.LogTrace("RenderSlotAvailable check"); + var count = _baginstances.Count; +#if (DEBUG) + log.LogInformation($"DBG: RenderSlotAvailable check, there are currently {count} instances in the bag"); +#endif + if (count >= ServerBootConfig.AYANOVA_REPORT_RENDERING_MAX_INSTANCES) { + log.LogTrace($"RenderSlotAvailable there are no free report rendering slots available, current count is {count}, checking for expired slots to force closed"); //check for expired and remove var Instances = _baginstances.ToArray(); - ReportRenderInstanceInfo oldest = null; + var dtNow = DateTime.UtcNow; foreach (ReportRenderInstanceInfo i in Instances) { - if (oldest == null) + if (i.Expires < dtNow) { - oldest = i; - continue; +#if (DEBUG) + log.LogInformation($"DBG: RenderSlotAvailable attempting kill of expired process {i.ReporterProcessId}"); +#endif + ForceCloseProcess(i, log); } - if (i.Started < oldest.Started) oldest = i; } - if (oldest != null) - { - try - { - var p = Process.GetProcessById(oldest.ReporterProcessId); - if (p != null) - { - //try to kill it - p.Kill(); - if (p?.HasExited == false) return false;//can't kill it so can't free up a slot - } - _baginstances.TryTake(out oldest); - return true;//process that was there is now not there so while not perfect system we will consider it free + } + //allow to continue if there are now fewer than max instances in the bag + return _baginstances.Count < ServerBootConfig.AYANOVA_REPORT_RENDERING_MAX_INSTANCES; + } - } - catch (ArgumentException) + private static bool ForceCloseProcess(ReportRenderInstanceInfo instance, ILogger log) + { + log.LogTrace($"ForceCloseProcess on instance id {instance.ReporterProcessId} started {instance.Expires.ToString()} utc"); + try + { + var p = Process.GetProcessById(instance.ReporterProcessId); + if (p != null) + { + //we have an existing process + //try to kill it + p.Kill(); + if (p.HasExited == false) { - return true;//no process available / not running + + log.LogDebug($"RenderSlotAvailable oldest slot could not be stopped"); + return false;//can't kill it so can't free up a slot } } + //remove it from the list, it's either gone or killed at this point + //this would not be unexpected since it will normally just close on it's own + //at the finally in render report + _baginstances.TryTake(out instance); + return true;//process that was there is now not there so while not perfect system we will consider it free } - return true; + catch (ArgumentException) + { + //do nothing, this is normal, the process could not be found and this means it's already been removed: + //ArgumentException + //The process specified by the processId parameter is not running. The identifier might be expired. + _baginstances.TryTake(out instance); + return true; + } } @@ -101,6 +123,18 @@ namespace AyaNova.Util _baginstances.Add(new ReportRenderInstanceInfo(processId)); } + internal static void RemoveProcess(int processId, ILogger log) + { + foreach (var i in _baginstances) + { + if (i.ReporterProcessId == processId) + { + ForceCloseProcess(i, log); + break; + } + } + } + // internal async static Task EnsureReporterAvailableAsync(ILogger log)