From 5cf5c831e67780bfa24499fe6873b7fd6254f8fe Mon Sep 17 00:00:00 2001 From: Neil South Date: Wed, 12 Apr 2023 16:21:06 +0100 Subject: [PATCH] squashed commits Signed-off-by: Neil South --- .github/workflows/test.yml | 2 +- doc/dependency_decisions.yml | 30 ++ src/.sonarlint/sonar.settings.json | 2 +- src/Shared/Configuration/PagedOptions.cs | 28 ++ .../Configuration/WorkflowManagerOptions.cs | 8 +- .../Shared}/ApiControllerBase.cs | 47 +-- .../Shared}/Filter/PaginationFilter.cs | 4 +- ...Monai.Deploy.WorkflowManager.Shared.csproj | 4 + .../Shared}/Services/IUriService.cs | 6 +- .../Shared}/Services/UriService.cs | 7 +- .../Shared}/Wrappers/PagedResponse.cs | 28 +- .../Shared}/Wrappers/Response.cs | 6 +- .../Shared/Wrappers/StatsPagedResponse.cs | 36 ++ src/Shared/Shared/packages.lock.json | 179 +++++++++ .../M001_TaskExecutionStats_addVersion.cs | 41 +++ .../API/Models/ExecutionStatDTO.cs | 38 ++ .../API/Models/TaskExecutionStats.cs | 140 +++++++ .../Database/ITaskExecutionStatsRepository.cs | 71 ++++ .../Options/TaskExecutionDatabaseSettings.cs | 29 ++ .../Database/TaskExecutionStatsRepository.cs | 232 ++++++++++++ .../AideClinicalReview/packages.lock.json | 3 +- src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs | 12 + .../Plug-ins/Argo/packages.lock.json | 3 +- .../TaskManager/ApplicationPartsLogger.cs | 57 +++ .../Controllers/TaskStatsController.cs | 165 +++++++++ .../TaskManager/Filter/TimeFilter.cs | 26 ++ src/TaskManager/TaskManager/Logging/Log.cs | 6 + src/TaskManager/TaskManager/Program.cs | 30 +- src/TaskManager/TaskManager/TaskManager.cs | 5 + src/TaskManager/TaskManager/appsettings.json | 2 +- .../TaskManager/packages.lock.json | 3 +- src/TaskManager/TaskManager/stylecop.json | 14 + .../PayloadListener/packages.lock.json | 3 +- .../WorkflowExecuter/packages.lock.json | 3 +- .../AuthenticatedApiControllerBase.cs | 4 +- .../Controllers/PayloadsController.cs | 10 +- .../Controllers/TasksController.cs | 8 +- .../Controllers/WFMApiControllerBase.cs | 42 +++ .../Controllers/WorkflowInstanceController.cs | 6 +- .../Controllers/WorkflowsController.cs | 10 +- .../WorkflowManager/Program.cs | 3 +- .../WorkflowManager/packages.lock.json | 3 +- .../Features/ExecutionStats.feature | 70 ++++ .../Features/HealthApi.feature | 4 +- ...owOrTaskIsNotFound_Workflow_1_Task_3_.snap | 36 ++ ...owOrTaskIsNotFound_Workflow_2_Task_1_.snap | 36 ++ ...orATaskAreReturned_Workflow_1_Task_2_.snap | 44 +++ ...orATaskAreReturned_workflow_1_task_1_.snap | 65 ++++ ...mmaryOfExecutionStatsAreReturned_-31_.snap | 24 ++ ...mmaryOfExecutionStatsAreReturned_-61_.snap | 24 ++ ...iFeature.GetHealthStatusOfTaskManager.snap | 45 +++ .../TaskManager.IntegrationTests/Hooks.cs | 5 + ...anager.TaskManager.IntegrationTests.csproj | 4 +- .../POCO/TestExecutionConfig.cs | 4 + .../StepDefinitions/CommonApiDefinitions.cs | 16 +- .../StepDefinitions/CommonStepDefinitions.cs | 206 +++++------ .../ExecutionStatsStepDefinitions.cs | 143 +++++++ .../Support/ApiHelper.cs | 12 + .../Support/Assertions.cs | 23 +- .../Support/DataHelper.cs | 29 ++ .../Support/MongoClientUtil.cs | 41 +++ .../Support/RabbitConnectionFactory.cs | 5 +- .../Support/TaskManagerStartup.cs | 126 ------- .../Support/TaskMangerStartup.cs | 133 +++++++ .../TestData/ExecutionStatsTestData.cs | 140 +++++++ .../TestData/Helper.cs | 14 +- .../TestData/TaskCallbackTestData.cs | 48 ++- .../PayloadApiStepDefinitions.cs | 4 +- .../TasksApiStepDefinitions.cs | 4 +- .../WorkflowApiStepDefinitions.cs | 4 +- .../WorkflowInstancesApiStepDefinitions.cs | 4 +- .../Support/RabbitConnectionFactory.cs | 1 - .../Support/WorkflowExecutorStartup.cs | 3 +- .../UriServiceTests.cs | 62 ++++ .../TaskManager.Argo.Tests/ArgoPluginTest.cs | 54 +++ .../TaskExecutionStatsControllerTests.cs | 249 +++++++++++++ ...y.WorkflowManager.TaskManager.Tests.csproj | 2 + .../TaskExecutionStatsRepositoryTests.cs | 275 ++++++++++++++ .../TaskExecutionStatsTests.cs | 348 ++++++++++++++++++ .../TaskManager.Tests/TaskManagerTest.cs | 7 + .../Controllers/PayloadControllerTests.cs | 17 +- .../Controllers/TasksControllerTests.cs | 15 +- .../WorkflowInstanceControllerTests.cs | 21 +- .../Controllers/WorkflowsControllerTests.cs | 17 +- .../WorkflowManager.Tests/packages.lock.json | 3 +- 85 files changed, 3375 insertions(+), 368 deletions(-) create mode 100644 src/Shared/Configuration/PagedOptions.cs rename src/{WorkflowManager/WorkflowManager/Controllers => Shared/Shared}/ApiControllerBase.cs (57%) rename src/{WorkflowManager/WorkflowManager => Shared/Shared}/Filter/PaginationFilter.cs (95%) rename src/{WorkflowManager/WorkflowManager => Shared/Shared}/Services/IUriService.cs (87%) rename src/{WorkflowManager/WorkflowManager => Shared/Shared}/Services/UriService.cs (93%) rename src/{WorkflowManager/WorkflowManager => Shared/Shared}/Wrappers/PagedResponse.cs (65%) rename src/{WorkflowManager/WorkflowManager => Shared/Shared}/Wrappers/Response.cs (92%) create mode 100644 src/Shared/Shared/Wrappers/StatsPagedResponse.cs create mode 100644 src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs create mode 100644 src/TaskManager/API/Models/ExecutionStatDTO.cs create mode 100644 src/TaskManager/API/Models/TaskExecutionStats.cs create mode 100644 src/TaskManager/Database/ITaskExecutionStatsRepository.cs create mode 100644 src/TaskManager/Database/Options/TaskExecutionDatabaseSettings.cs create mode 100644 src/TaskManager/Database/TaskExecutionStatsRepository.cs create mode 100644 src/TaskManager/TaskManager/ApplicationPartsLogger.cs create mode 100644 src/TaskManager/TaskManager/Controllers/TaskStatsController.cs create mode 100644 src/TaskManager/TaskManager/Filter/TimeFilter.cs create mode 100644 src/TaskManager/TaskManager/stylecop.json create mode 100644 src/WorkflowManager/WorkflowManager/Controllers/WFMApiControllerBase.cs create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/ExecutionStats.feature create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_1_Task_3_.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_2_Task_1_.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_Workflow_1_Task_2_.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_workflow_1_task_1_.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-31_.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-61_.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/HealthApiFeature.GetHealthStatusOfTaskManager.snap create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/StepDefinitions/ExecutionStatsStepDefinitions.cs delete mode 100755 tests/IntegrationTests/TaskManager.IntegrationTests/Support/TaskManagerStartup.cs create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/Support/TaskMangerStartup.cs create mode 100644 tests/IntegrationTests/TaskManager.IntegrationTests/TestData/ExecutionStatsTestData.cs create mode 100644 tests/UnitTests/Monai.Deploy.WorkflowManager.Shared.Tests/UriServiceTests.cs create mode 100644 tests/UnitTests/TaskManager.Tests/Controllers/TaskExecutionStatsControllerTests.cs create mode 100644 tests/UnitTests/TaskManager.Tests/TaskExecutionStatsRepositoryTests.cs create mode 100644 tests/UnitTests/TaskManager.Tests/TaskExecutionStatsTests.cs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1d31d337..2949f98b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -251,7 +251,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: dotnet sonarscanner begin /k:"Project-MONAI_monai-deploy-workflow-manager" /o:"project-monai" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="../**/coverage.opencover.xml" /d:sonar.coverage.exclusions="src/WorkflowManager/Database/Repositories/**/*,src/TaskManager/Database/TaskDispatchEventRepository.cs" + run: dotnet sonarscanner begin /k:"Project-MONAI_monai-deploy-workflow-manager" /o:"project-monai" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="../**/coverage.opencover.xml" /d:sonar.coverage.exclusions="src/WorkflowManager/Database/Repositories/**/*,src/TaskManager/Database/TaskDispatchEventRepository.cs,**/Migrations/M0*.cs" working-directory: ./src - name: Restore Solution diff --git a/doc/dependency_decisions.yml b/doc/dependency_decisions.yml index f15a91b3c..aef2004fb 100755 --- a/doc/dependency_decisions.yml +++ b/doc/dependency_decisions.yml @@ -2506,3 +2506,33 @@ :versions: - 4.3.0 :when: 2023-02-02 15:35:00.000000000 Z + +- - :approve + - System.IO.Pipelines + - :who: neildsouth + :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) + :versions: + - 6.0.3 + :when: 2023-04-11 13:37:00.000000000 Z +- - :approve + - Microsoft.Extensions.DependencyModel + - :who: neildsouth + :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) + :versions: + - 6.0.0 + :when: 2023-04-11 13:37:00.000000000 Z +- - :approve + - Microsoft.AspNetCore.TestHost + - :who: neildsouth + :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) + :versions: + - 6.0.15 + :when: 2023-04-11 13:37:00.000000000 Z +- - :approve + - Microsoft.AspNetCore.Mvc.Testing + - :who: neildsouth + :why: MIT (https://github.com/dotnet/runtime/raw/main/LICENSE.TXT) + :versions: + - 6.0.15 + :when: 2023-04-11 13:37:00.000000000 Z + diff --git a/src/.sonarlint/sonar.settings.json b/src/.sonarlint/sonar.settings.json index 10d827d5c..23258086d 100644 --- a/src/.sonarlint/sonar.settings.json +++ b/src/.sonarlint/sonar.settings.json @@ -1 +1 @@ -{"sonar.exclusions":[],"sonar.global.exclusions":["**/build-wrapper-dump.json"],"sonar.inclusions":[]} \ No newline at end of file +{"sonar.exclusions":[],"sonar.global.exclusions":["**/build-wrapper-dump.json","**/Migrations/*.cs"],"sonar.inclusions":[]} diff --git a/src/Shared/Configuration/PagedOptions.cs b/src/Shared/Configuration/PagedOptions.cs new file mode 100644 index 000000000..56796bc7d --- /dev/null +++ b/src/Shared/Configuration/PagedOptions.cs @@ -0,0 +1,28 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Microsoft.Extensions.Configuration; + +namespace Monai.Deploy.WorkflowManager.Configuration +{ + public class PagedOptions + { + /// + /// Represents the endpointSettings section of the configuration file. + /// + [ConfigurationKeyName("endpointSettings")] + public EndpointSettings EndpointSettings { get; set; } + } +} diff --git a/src/Shared/Configuration/WorkflowManagerOptions.cs b/src/Shared/Configuration/WorkflowManagerOptions.cs index 801f6b283..0359ae4c4 100755 --- a/src/Shared/Configuration/WorkflowManagerOptions.cs +++ b/src/Shared/Configuration/WorkflowManagerOptions.cs @@ -19,7 +19,7 @@ namespace Monai.Deploy.WorkflowManager.Configuration { - public class WorkflowManagerOptions + public class WorkflowManagerOptions : PagedOptions { /// /// Name of the key for retrieve database connection string. @@ -44,12 +44,6 @@ public class WorkflowManagerOptions [ConfigurationKeyName("taskManager")] public TaskManagerConfiguration TaskManager { get; set; } - /// - /// Represents the endpointSettings section of the configuration file. - /// - [ConfigurationKeyName("endpointSettings")] - public EndpointSettings EndpointSettings { get; set; } - [ConfigurationKeyName("taskTimeoutMinutes")] public double TaskTimeoutMinutes { get; set; } = 60; diff --git a/src/WorkflowManager/WorkflowManager/Controllers/ApiControllerBase.cs b/src/Shared/Shared/ApiControllerBase.cs similarity index 57% rename from src/WorkflowManager/WorkflowManager/Controllers/ApiControllerBase.cs rename to src/Shared/Shared/ApiControllerBase.cs index ab623d7de..9033334fb 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/ApiControllerBase.cs +++ b/src/Shared/Shared/ApiControllerBase.cs @@ -14,18 +14,17 @@ * limitations under the License. */ -using System; -using System.Collections.Generic; using System.Net; using Ardalis.GuardClauses; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Monai.Deploy.WorkflowManager.Configuration; -using Monai.Deploy.WorkflowManager.Filter; -using Monai.Deploy.WorkflowManager.Services; -using Monai.Deploy.WorkflowManager.Wrappers; +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Wrappers; +using Monai.Deploy.WorkflowManager.Shared.Services; +using Microsoft.AspNetCore.Routing; -namespace Monai.Deploy.WorkflowManager.Controllers +namespace Monai.Deploy.WorkflowManager.ControllersShared { /// /// Base Api Controller. @@ -33,15 +32,15 @@ namespace Monai.Deploy.WorkflowManager.Controllers [ApiController] public class ApiControllerBase : ControllerBase { - private readonly IOptions _options; + public IOptions Options { get; set; } /// /// Initializes a new instance of the class. /// /// Workflow manager options. - public ApiControllerBase(IOptions options) + public ApiControllerBase(IOptions Options) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + this.Options = Options ?? throw new ArgumentNullException(nameof(Options)); } /// @@ -69,34 +68,26 @@ public ApiControllerBase(IOptions options) /// Uri service. /// Route. /// Returns . - public PagedResponse> CreatePagedReponse(List pagedData, PaginationFilter validFilter, long totalRecords, IUriService uriService, string route) + public PagedResponse> CreatePagedReponse(IEnumerable pagedData, PaginationFilter validFilter, long totalRecords, IUriService uriService, string route) { Guard.Against.Null(pagedData); Guard.Against.Null(validFilter); Guard.Against.Null(route); Guard.Against.Null(uriService); - var pageSize = validFilter.PageSize ?? _options.Value.EndpointSettings.DefaultPageSize; - var respose = new PagedResponse>(pagedData, validFilter.PageNumber, pageSize); - var totalPages = (double)totalRecords / pageSize; - var roundedTotalPages = Convert.ToInt32(Math.Ceiling(totalPages)); + var pageSize = validFilter.PageSize ?? Options.Value.EndpointSettings.DefaultPageSize; + var respose = new PagedResponse>(pagedData, validFilter.PageNumber, pageSize); - respose.NextPage = - validFilter.PageNumber >= 1 && validFilter.PageNumber < roundedTotalPages - ? uriService.GetPageUriString(new PaginationFilter(validFilter.PageNumber + 1, pageSize), route) - : null; - - respose.PreviousPage = - validFilter.PageNumber - 1 >= 1 && validFilter.PageNumber <= roundedTotalPages - ? uriService.GetPageUriString(new PaginationFilter(validFilter.PageNumber - 1, pageSize), route) - : null; + respose.SetUp(validFilter, totalRecords, uriService, route); + return respose; + } - respose.FirstPage = uriService.GetPageUriString(new PaginationFilter(1, pageSize), route); - respose.LastPage = uriService.GetPageUriString(new PaginationFilter(roundedTotalPages, pageSize), route); - respose.TotalPages = roundedTotalPages; - respose.TotalRecords = totalRecords; - return respose; + public StatsPagedResponse> CreateStatsPagedReponse(IEnumerable pagedData, PaginationFilter validFilter, long totalRecords, IUriService uriService, string route) + { + var response = new StatsPagedResponse>(pagedData, validFilter.PageNumber, validFilter.PageSize.Value); + response.SetUp(validFilter, totalRecords, uriService, route); + return response; } } } diff --git a/src/WorkflowManager/WorkflowManager/Filter/PaginationFilter.cs b/src/Shared/Shared/Filter/PaginationFilter.cs similarity index 95% rename from src/WorkflowManager/WorkflowManager/Filter/PaginationFilter.cs rename to src/Shared/Shared/Filter/PaginationFilter.cs index ae8dd7466..921ffd36e 100644 --- a/src/WorkflowManager/WorkflowManager/Filter/PaginationFilter.cs +++ b/src/Shared/Shared/Filter/PaginationFilter.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -namespace Monai.Deploy.WorkflowManager.Filter +namespace Monai.Deploy.WorkflowManager.Shared.Filter { /// /// Pagination Filter class. diff --git a/src/Shared/Shared/Monai.Deploy.WorkflowManager.Shared.csproj b/src/Shared/Shared/Monai.Deploy.WorkflowManager.Shared.csproj index 267900ecc..abeeba7a4 100755 --- a/src/Shared/Shared/Monai.Deploy.WorkflowManager.Shared.csproj +++ b/src/Shared/Shared/Monai.Deploy.WorkflowManager.Shared.csproj @@ -51,6 +51,10 @@ + + + + true true diff --git a/src/WorkflowManager/WorkflowManager/Services/IUriService.cs b/src/Shared/Shared/Services/IUriService.cs similarity index 87% rename from src/WorkflowManager/WorkflowManager/Services/IUriService.cs rename to src/Shared/Shared/Services/IUriService.cs index 8ddad0fec..1a4311d7f 100644 --- a/src/WorkflowManager/WorkflowManager/Services/IUriService.cs +++ b/src/Shared/Shared/Services/IUriService.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ * limitations under the License. */ -using Monai.Deploy.WorkflowManager.Filter; +using Monai.Deploy.WorkflowManager.Shared.Filter; -namespace Monai.Deploy.WorkflowManager.Services +namespace Monai.Deploy.WorkflowManager.Shared.Services { /// /// Uri Serivce. diff --git a/src/WorkflowManager/WorkflowManager/Services/UriService.cs b/src/Shared/Shared/Services/UriService.cs similarity index 93% rename from src/WorkflowManager/WorkflowManager/Services/UriService.cs rename to src/Shared/Shared/Services/UriService.cs index 292eeec3b..3f1131d5f 100644 --- a/src/WorkflowManager/WorkflowManager/Services/UriService.cs +++ b/src/Shared/Shared/Services/UriService.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,10 @@ * limitations under the License. */ -using System; using Microsoft.AspNetCore.WebUtilities; -using Monai.Deploy.WorkflowManager.Filter; +using Monai.Deploy.WorkflowManager.Shared.Filter; -namespace Monai.Deploy.WorkflowManager.Services +namespace Monai.Deploy.WorkflowManager.Shared.Services { /// /// Uri Service. diff --git a/src/WorkflowManager/WorkflowManager/Wrappers/PagedResponse.cs b/src/Shared/Shared/Wrappers/PagedResponse.cs similarity index 65% rename from src/WorkflowManager/WorkflowManager/Wrappers/PagedResponse.cs rename to src/Shared/Shared/Wrappers/PagedResponse.cs index 13b9ac2b0..741a516c2 100644 --- a/src/WorkflowManager/WorkflowManager/Wrappers/PagedResponse.cs +++ b/src/Shared/Shared/Wrappers/PagedResponse.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,10 @@ * limitations under the License. */ -namespace Monai.Deploy.WorkflowManager.Wrappers +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Services; + +namespace Monai.Deploy.WorkflowManager.Shared.Wrappers { /// /// Paged Response for use with paginations. @@ -77,5 +80,26 @@ public PagedResponse(T data, int pageNumber, int pageSize) /// Gets or sets previousPage. /// public string PreviousPage { get; set; } + + public void SetUp(PaginationFilter validFilter, long totalRecords, IUriService uriService, string route) + { + var totalPages = (double)totalRecords / PageSize; + var roundedTotalPages = Convert.ToInt32(Math.Ceiling(totalPages)); + + NextPage = + validFilter.PageNumber >= 1 && validFilter.PageNumber < roundedTotalPages + ? uriService.GetPageUriString(new PaginationFilter(validFilter.PageNumber + 1, PageSize), route) + : null; + + PreviousPage = + validFilter.PageNumber - 1 >= 1 && validFilter.PageNumber <= roundedTotalPages + ? uriService.GetPageUriString(new PaginationFilter(validFilter.PageNumber - 1, PageSize), route) + : null; + + FirstPage = uriService.GetPageUriString(new PaginationFilter(1, PageSize), route); + LastPage = uriService.GetPageUriString(new PaginationFilter(roundedTotalPages, PageSize), route); + TotalPages = roundedTotalPages; + TotalRecords = totalRecords; + } } } diff --git a/src/WorkflowManager/WorkflowManager/Wrappers/Response.cs b/src/Shared/Shared/Wrappers/Response.cs similarity index 92% rename from src/WorkflowManager/WorkflowManager/Wrappers/Response.cs rename to src/Shared/Shared/Wrappers/Response.cs index 1af398ebe..eb4ee6e6b 100644 --- a/src/WorkflowManager/WorkflowManager/Wrappers/Response.cs +++ b/src/Shared/Shared/Wrappers/Response.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -namespace Monai.Deploy.WorkflowManager.Wrappers +namespace Monai.Deploy.WorkflowManager.Shared.Wrappers { /// /// Response object. @@ -37,7 +37,7 @@ public Response(T data) { Succeeded = true; Message = string.Empty; - Errors = null; + Errors = Array.Empty(); Data = data; } diff --git a/src/Shared/Shared/Wrappers/StatsPagedResponse.cs b/src/Shared/Shared/Wrappers/StatsPagedResponse.cs new file mode 100644 index 000000000..c0c45adbf --- /dev/null +++ b/src/Shared/Shared/Wrappers/StatsPagedResponse.cs @@ -0,0 +1,36 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace Monai.Deploy.WorkflowManager.Shared.Wrappers +{ + public class StatsPagedResponse : PagedResponse + { + public DateTime PeriodStart { get; set; } + public DateTime PeriodEnd { get; set; } + public long TotalExecutions { get; set; } + public long TotalFailures { get; set; } + public double AverageTotalExecutionSeconds { get; set; } + public double AverageArgoExecutionSeconds { get; set; } + + public StatsPagedResponse(T data, int pageNumber, int pageSize) : base(data, pageNumber, pageSize) + { + + } + //public StatsPagedResponse(PagedResponse paged) : base(paged.Data, paged.PageNumber, paged.PageSize) + //{ + // int re = 0; + //} + } +} diff --git a/src/Shared/Shared/packages.lock.json b/src/Shared/Shared/packages.lock.json index 0ede9e08f..2de08c2a5 100755 --- a/src/Shared/Shared/packages.lock.json +++ b/src/Shared/Shared/packages.lock.json @@ -17,10 +17,189 @@ "resolved": "6.0.15", "contentHash": "LmB5kbbc0Sr+XvnYj8tReZzubS50h1g463zpbnnjqT/k6fM8/od9hFCBj52dorXfp/DDfm5+rUdKaPRUsX70Jg==" }, + "AWSSDK.Core": { + "type": "Transitive", + "resolved": "3.7.105.20", + "contentHash": "ZHuTxP1J8g91+YSV0YLzm5te5lG+zkiUH/+NDHFpLf1cBD6iw2kUo5AkYEVxfEur1OTdYJxEZ5jDuOBE4pubkg==" + }, + "AWSSDK.SecurityToken": { + "type": "Transitive", + "resolved": "3.7.101.26", + "contentHash": "/y64ogftqwGa07HNOj2Dh08oqYIgbIyfJFncneHy+fzC54VFhEIN5+pSOHS4Also1SSb9Erk/Knuf3L6jrTVEg==", + "dependencies": { + "AWSSDK.Core": "[3.7.105.20, 4.0.0)" + } + }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2021.3.0", "contentHash": "Ddxjs5RRjf+c8m9m++WvhW1lz1bqNhsTjWvCLbQN9bvKbkJeR9MhtfNwKgBRRdG2yLHcXFr5Lf7fsvvkiPaDRg==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "BUyFU9t+HzlSE7ri4B+AQN2BgTgHv/uM82s5ZkgU1BApyzWzIl48nDsG5wR1t0pniNuuyTBzG3qCW8152/NtSw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "6.0.15", + "contentHash": "crR/15PKDgVIQmH9uGJuQVg4RGbaxwG3cseRRMisPG/2LkiQV71EkNRGPV4cI61Waywc1Wn5sYXE8bo2qCf+/Q==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "6.0.15", + "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.3", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.3", + "contentHash": "SUpStcdjeBbdKjPKe53hVVLkFjylX0yIXY8K+xWa47+o1d+REDyOMZjHZa+chsQI1K9qZeiHWk9jos0TFU7vGg==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Monai.Deploy.Messaging": { + "type": "Transitive", + "resolved": "0.1.22", + "contentHash": "pFZBuV3TaZvZJz8wTib8G/Doa/XHkM8uv12VtuLkQc7lI8AbJmH1eIHnpRliyuKPmw7VMhOMiS7JhyqutC0uvQ==", + "dependencies": { + "Ardalis.GuardClauses": "4.0.1", + "Microsoft.Extensions.Configuration": "6.0.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.14", + "Microsoft.Extensions.Logging": "6.0.0", + "Newtonsoft.Json": "13.0.3", + "System.ComponentModel.Annotations": "5.0.0", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage": { + "type": "Transitive", + "resolved": "0.2.15", + "contentHash": "5VCzUVZek/1LB+4V7l2Ubg1gqzxn4wVPrpZG9SqCsUYtXBzpY73ohmyCXE0PpgO1z6WpWKH3IaYOJqWvAUeFXw==", + "dependencies": { + "AWSSDK.SecurityToken": "3.7.101.26", + "Ardalis.GuardClauses": "4.0.1", + "Microsoft.Extensions.Configuration": "6.0.1", + "Microsoft.Extensions.Diagnostics.HealthChecks": "6.0.15", + "Microsoft.Extensions.Logging": "6.0.0", + "Monai.Deploy.Storage.S3Policy": "0.2.15", + "System.IO.Abstractions": "17.2.3" + } + }, + "Monai.Deploy.Storage.S3Policy": { + "type": "Transitive", + "resolved": "0.2.15", + "contentHash": "0+FCC5nltIDEXuBAJSDba2DUTm+yQ7KgZLavASt5wyF842VtTcLTG2uPHfHy+nJ6hfT7zCoBEsVup3g9KGC56w==", + "dependencies": { + "Ardalis.GuardClauses": "4.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "17.2.3", + "contentHash": "VcozGeE4SxIo0cnXrDHhbrh/Gb8KQnZ3BvMelvh+iw0PrIKtuuA46U2Xm4e4pgnaWFgT4RdZfTpWl/WPRdw0WQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "monai.deploy.workflowmanager.configuration": { + "type": "Project", + "dependencies": { + "Monai.Deploy.Messaging": "[0.1.22, )", + "Monai.Deploy.Storage": "[0.2.15, )" + } } } } diff --git a/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs b/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs new file mode 100644 index 000000000..3f8948a0d --- /dev/null +++ b/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs @@ -0,0 +1,41 @@ +// +// Copyright 2023 Guy’s and St Thomas’ NHS Foundation Trust +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// using Monai.Deploy.WorkflowManager.TaskManager.API.Models; +using Monai.Deploy.WorkflowManager.TaskManager.API.Models; +using Mongo.Migration.Migrations.Document; +using MongoDB.Bson; + +namespace Monai.Deploy.WorkflowManager.TaskManager.API.Migrations +{ + public class M001_TaskExecutionStats_addVersion : DocumentMigration + { + public M001_TaskExecutionStats_addVersion() : base("1.0.0") { } + + public override void Up(BsonDocument document) + { + // empty, but this will make all objects re-saved with a version + } + public override void Down(BsonDocument document) + { + try + { + document.Remove("Version"); + } + catch (Exception) + { + } + } + } +} diff --git a/src/TaskManager/API/Models/ExecutionStatDTO.cs b/src/TaskManager/API/Models/ExecutionStatDTO.cs new file mode 100644 index 000000000..6afbb1dd8 --- /dev/null +++ b/src/TaskManager/API/Models/ExecutionStatDTO.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace Monai.Deploy.WorkflowManager.TaskManager.API.Models +{ + + public class ExecutionStatDTO + { + public ExecutionStatDTO(TaskExecutionStats stats) + { + ExecutionId = stats.ExecutionId; + StartedAt = stats.StartedUTC; + FinishedAt = stats.CompletedAtUTC; + Status = stats.Status; + ExecutionDurationSeconds = stats.ExecutionTimeSeconds; + } + + public string ExecutionId { get; set; } = string.Empty; + public DateTime StartedAt { get; set; } + public DateTime FinishedAt { get; set; } + public double ExecutionDurationSeconds { get; set; } + public string Status { get; set; } = TaskStatus.Created.ToString(); + } + +} diff --git a/src/TaskManager/API/Models/TaskExecutionStats.cs b/src/TaskManager/API/Models/TaskExecutionStats.cs new file mode 100644 index 000000000..595a9a43f --- /dev/null +++ b/src/TaskManager/API/Models/TaskExecutionStats.cs @@ -0,0 +1,140 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.ComponentModel.DataAnnotations; +using Ardalis.GuardClauses; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.WorkflowManager.TaskManager.Migrations; +using Mongo.Migration.Documents; +using Mongo.Migration.Documents.Attributes; +using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json; + +namespace Monai.Deploy.WorkflowManager.TaskManager.API.Models +{ + [CollectionLocation("ExecutionStats"), RuntimeVersion("1.0.0")] + public class TaskExecutionStats : IDocument + { + /// + /// Gets or sets the ID of the object. + /// + [BsonId] + [JsonProperty(PropertyName = "id")] + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// Gets or sets Db version. + /// + [JsonConverter(typeof(DocumentVersionConvert)), BsonSerializer(typeof(DocumentVersionConverBson))] + public DocumentVersion Version { get; set; } = new DocumentVersion(1, 0, 0); + + /// + /// the correlationId of the event + /// + [JsonProperty(PropertyName = "correlation_id")] + [Required] + public string CorrelationId { get; set; } + + /// + /// the workflow Instance that triggered the event + /// + [JsonProperty(PropertyName = "workflow_instance_id")] + [Required] + public string WorkflowInstanceId { get; set; } + + /// + /// This execution ID + /// + [JsonProperty(PropertyName = "execution_id")] + [Required] + public string ExecutionId { get; set; } + + /// + /// The event Task ID + /// + [Required] + [JsonProperty(PropertyName = "task_id")] + public string TaskId { get; set; } + + /// + /// Gets or sets the date time that the task started with the plug-in. + /// + [JsonProperty(PropertyName = "startedUTC")] + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime StartedUTC { get; set; } + + /// + /// Gets or sets the date time that the task last updated. + /// + [JsonProperty(PropertyName = "lastUpdatedUTC")] + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime LastUpdatedUTC { get; set; } + + /// + /// Gets or sets the date time that the task completed. + /// + [JsonProperty(PropertyName = "completedAtUTC")] + [BsonDateTimeOptions(Kind = DateTimeKind.Utc)] + public DateTime CompletedAtUTC { get; set; } + + /// + /// Gets or sets the duration of time actually executing in Argo, calculated from the metadata. + /// + [JsonProperty(PropertyName = "executionTimeSeconds")] + public double ExecutionTimeSeconds { get; set; } + + /// + /// Gets or sets the status. + /// + [JsonProperty(PropertyName = "status")] + public string Status { get; set; } = TaskExecutionStatus.Created.ToString(); + + /// + /// Gets or sets the duration, difference between startedAt and CompletedAt time. + /// + [JsonProperty(PropertyName = "durationSeconds")] + public double DurationSeconds + { + get; set; + } + + public TaskExecutionStats() + { + + } + + public TaskExecutionStats(TaskDispatchEventInfo dispatchInfo) + { + Guard.Against.Null(dispatchInfo, "dispatchInfo"); + CorrelationId = dispatchInfo.Event.CorrelationId; + WorkflowInstanceId = dispatchInfo.Event.WorkflowInstanceId; + ExecutionId = dispatchInfo.Event.ExecutionId; + TaskId = dispatchInfo.Event.TaskId; + StartedUTC = dispatchInfo.Started.ToUniversalTime(); + Status = dispatchInfo.Event.Status.ToString(); + } + + public TaskExecutionStats(TaskUpdateEvent taskUpdateEvent) + { + Guard.Against.Null(taskUpdateEvent, "taskUpdateEvent"); + CorrelationId = taskUpdateEvent.CorrelationId; + WorkflowInstanceId = taskUpdateEvent.WorkflowInstanceId; + ExecutionId = taskUpdateEvent.ExecutionId; + TaskId = taskUpdateEvent.TaskId; + Status = taskUpdateEvent.Status.ToString(); + } + } +} diff --git a/src/TaskManager/Database/ITaskExecutionStatsRepository.cs b/src/TaskManager/Database/ITaskExecutionStatsRepository.cs new file mode 100644 index 000000000..d00cf32f0 --- /dev/null +++ b/src/TaskManager/Database/ITaskExecutionStatsRepository.cs @@ -0,0 +1,71 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.WorkflowManager.TaskManager.API.Models; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Database +{ + public interface ITaskExecutionStatsRepository + { + /// + /// Creates a task dispatch event in the database. + /// + /// A TaskDispatchEvent to create. + /// Returns the created TaskDispatchEventInfo. + Task CreateAsync(TaskDispatchEventInfo taskDispatchEventInfo); + + /// + /// Updates user accounts of a task dispatch event in the database. + /// + /// A TaskDispatchEvent to update. + /// Returns the created TaskDispatchEventInfo. + Task UpdateExecutionStatsAsync(TaskUpdateEvent taskUpdateEvent); + + /// + /// Returns paged entries between the two given dates. + /// + /// start of the range. + /// end of the range. + /// a paged view of entried in range + Task> GetStatsAsync(DateTime startTime, DateTime endTime, int PageSize = 10, int PageNumber = 1, string workflowInstanceId = "", string taskId = ""); + + /// + /// Return the total number of stats between the dates + /// + /// start of the range. + /// end of the range. + /// The count of all records in range + Task GetStatsCountAsync(DateTime startTime, DateTime endTime, string workflowInstanceId = "", string taskId = ""); + + /// + /// Returns all stats in Failed or PartialFail status. + /// + /// start of the range. + /// end of the range. + /// All stats NOT of that status + Task GetStatsStatusFailedCountAsync(DateTime startTime, DateTime endTime, string workflowInstanceId = "", string taskId = ""); + + /// + /// Calculates the average exection time for the given range + /// + /// start of the range. + /// end of the range. + /// the average exection times in the time range + Task<(double avgTotalExecution, double avgArgoExecution)> GetAverageStats(DateTime startTime, DateTime endTime, string workflowInstanceId = "", string taskId = ""); + + } +} diff --git a/src/TaskManager/Database/Options/TaskExecutionDatabaseSettings.cs b/src/TaskManager/Database/Options/TaskExecutionDatabaseSettings.cs new file mode 100644 index 000000000..2ead641db --- /dev/null +++ b/src/TaskManager/Database/Options/TaskExecutionDatabaseSettings.cs @@ -0,0 +1,29 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.Extensions.Configuration; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Database.Options +{ + public class TaskExecutionDatabaseSettings + { + [ConfigurationKeyName("ConnectionString")] + public string ConnectionString { get; set; } = null!; + + [ConfigurationKeyName("DatabaseName")] + public string DatabaseName { get; set; } = null!; + } +} diff --git a/src/TaskManager/Database/TaskExecutionStatsRepository.cs b/src/TaskManager/Database/TaskExecutionStatsRepository.cs new file mode 100644 index 000000000..908fa47db --- /dev/null +++ b/src/TaskManager/Database/TaskExecutionStatsRepository.cs @@ -0,0 +1,232 @@ +/* + * Copyright 2022 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Ardalis.GuardClauses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.WorkflowManager.TaskManager.API.Models; +using Monai.Deploy.WorkflowManager.TaskManager.Database.Options; +using Monai.Deploy.WorkflowManager.TaskManager.Logging; +using MongoDB.Driver; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Database +{ + public class TaskExecutionStatsRepository : ITaskExecutionStatsRepository + { + + private readonly IMongoCollection _taskExecutionStatsCollection; + private readonly ILogger _logger; + + public TaskExecutionStatsRepository( + IMongoClient client, + IOptions databaseSettings, + ILogger logger) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + var mongoDatabase = client.GetDatabase(databaseSettings.Value.DatabaseName, null); + _taskExecutionStatsCollection = mongoDatabase.GetCollection("ExecutionStats", null); + EnsureIndex(_taskExecutionStatsCollection).GetAwaiter().GetResult(); + } + + private static async Task EnsureIndex(IMongoCollection TaskExecutionStatsCollection) + { + Guard.Against.Null(TaskExecutionStatsCollection, "TaskExecutionStatsCollection"); + + var asyncCursor = (await TaskExecutionStatsCollection.Indexes.ListAsync()); + var bsonDocuments = (await asyncCursor.ToListAsync()); + var indexes = bsonDocuments.Select(_ => _.GetElement("name").Value.ToString()).ToList(); + + // If index not present create it else skip. + if (!indexes.Any(i => i is not null && i.Equals("ExecutionStatsIndex"))) + { + // Create Index here + + var options = new CreateIndexOptions() + { + Name = "ExecutionStatsIndex" + }; + var model = new CreateIndexModel( + Builders.IndexKeys.Ascending(s => s.StartedUTC), + options + ); + + await TaskExecutionStatsCollection.Indexes.CreateOneAsync(model); + } + } + + public async Task CreateAsync(TaskDispatchEventInfo taskDispatchEventInfo) + { + Guard.Against.Null(taskDispatchEventInfo, "taskDispatchEventInfo"); + + try + { + var insertMe = new TaskExecutionStats(taskDispatchEventInfo); + + await _taskExecutionStatsCollection.ReplaceOneAsync(doc => + doc.ExecutionId == insertMe.ExecutionId, + insertMe, + new ReplaceOptions { IsUpsert = true } + ).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.DatabaseException(nameof(CreateAsync), e); + } + } + public async Task UpdateExecutionStatsAsync(TaskUpdateEvent taskUpdateEvent) + { + Guard.Against.Null(taskUpdateEvent, "taskUpdateEvent"); + + try + { + var updateMe = ExposeExecutionStats(new TaskExecutionStats(taskUpdateEvent), taskUpdateEvent); + var duration = updateMe.CompletedAtUTC == default ? 0 : (updateMe.CompletedAtUTC - updateMe.StartedUTC).TotalMilliseconds / 1000; + await _taskExecutionStatsCollection.UpdateOneAsync(o => + o.ExecutionId == updateMe.ExecutionId, + Builders.Update + .Set(w => w.Status, updateMe.Status) + .Set(w => w.LastUpdatedUTC, DateTime.UtcNow) + .Set(w => w.CompletedAtUTC, updateMe.CompletedAtUTC) + .Set(w => w.ExecutionTimeSeconds, updateMe.ExecutionTimeSeconds) + .Set(w => w.DurationSeconds, duration) + + , new UpdateOptions { IsUpsert = true }).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.DatabaseException(nameof(CreateAsync), e); + } + } + + public async Task> GetStatsAsync(DateTime startTime, DateTime endTime, int PageSize = 10, int PageNumber = 1, string workflowInstanceId = "", string taskId = "") + { + startTime = startTime.ToUniversalTime(); + + var workflowinstanceNull = string.IsNullOrWhiteSpace(workflowInstanceId); + var taskIdNull = string.IsNullOrWhiteSpace(taskId); + + var result = await _taskExecutionStatsCollection.Find(T => + T.StartedUTC >= startTime && + T.StartedUTC <= endTime.ToUniversalTime() && + (workflowinstanceNull || T.WorkflowInstanceId == workflowInstanceId) && + (taskIdNull || T.TaskId == taskId) && + ( + T.Status == TaskExecutionStatus.Succeeded.ToString() + || T.Status == TaskExecutionStatus.Failed.ToString() + || T.Status == TaskExecutionStatus.PartialFail.ToString() + ) + ) + .Limit(PageSize) + .Skip((PageNumber - 1) * PageSize) + .ToListAsync(); + return result; + } + + private static TaskExecutionStats ExposeExecutionStats(TaskExecutionStats taskExecutionStats, TaskUpdateEvent taskUpdateEvent) + { + if (taskUpdateEvent.ExecutionStats is not null) + { + if (taskUpdateEvent.ExecutionStats.ContainsKey("finishedAt") && + DateTime.TryParse(taskUpdateEvent.ExecutionStats["finishedAt"], out var finished)) + { + taskExecutionStats.CompletedAtUTC = finished; + taskExecutionStats.DurationSeconds = (taskExecutionStats.CompletedAtUTC - taskExecutionStats.StartedUTC).TotalMilliseconds / 1000; + } + + var statKeys = taskUpdateEvent.ExecutionStats.Keys.Where(v => v.StartsWith("podStartTime") || v.StartsWith("podFinishTime")); + if (statKeys.Any()) + { + var start = DateTime.Now; + var end = new DateTime(); + foreach (var statKey in statKeys) + { + if (statKey.Contains("StartTime") && DateTime.TryParse(taskUpdateEvent.ExecutionStats[statKey], out var startTime)) + { + start = (startTime < start ? startTime : start); + } + else if (DateTime.TryParse(taskUpdateEvent.ExecutionStats[statKey], out var endTime)) + { + end = (endTime > end ? endTime : start); + } + } + taskExecutionStats.ExecutionTimeSeconds = (end - start).TotalMilliseconds / 1000; + } + } + return taskExecutionStats; + } + + public async Task GetStatsCountAsync(DateTime startTime, DateTime endTime, string workflowInstanceId = "", string taskId = "") + { + var workflowinstanceNull = string.IsNullOrWhiteSpace(workflowInstanceId); + var taskIdNull = string.IsNullOrWhiteSpace(taskId); + + return await _taskExecutionStatsCollection.CountDocumentsAsync(T => + T.StartedUTC >= startTime.ToUniversalTime() && + T.StartedUTC <= endTime.ToUniversalTime() && + (workflowinstanceNull || T.WorkflowInstanceId == workflowInstanceId) && + (taskIdNull || T.TaskId == taskId) && + ( + T.Status == TaskExecutionStatus.Succeeded.ToString() || + T.Status == TaskExecutionStatus.Failed.ToString() || + T.Status == TaskExecutionStatus.PartialFail.ToString()) + ); + } + + public async Task GetStatsStatusFailedCountAsync(DateTime start, DateTime endTime, string workflowInstanceId = "", string taskId = "") + { + var workflowinstanceNull = string.IsNullOrWhiteSpace(workflowInstanceId); + var taskIdNull = string.IsNullOrWhiteSpace(taskId); + + return await _taskExecutionStatsCollection.CountDocumentsAsync(T => + T.StartedUTC >= start.ToUniversalTime() && + T.StartedUTC <= endTime.ToUniversalTime() && + (workflowinstanceNull || T.WorkflowInstanceId == workflowInstanceId) && + (taskIdNull || T.TaskId == taskId) && + ( + T.Status == TaskExecutionStatus.Failed.ToString() || + T.Status == TaskExecutionStatus.PartialFail.ToString() + )); + } + + public async Task<(double avgTotalExecution, double avgArgoExecution)> GetAverageStats(DateTime startTime, DateTime endTime, string workflowInstanceId = "", string taskId = "") + { + var workflowinstanceNull = string.IsNullOrWhiteSpace(workflowInstanceId); + var taskIdNull = string.IsNullOrWhiteSpace(taskId); + + var test = await _taskExecutionStatsCollection.Aggregate() + .Match(T => + T.StartedUTC >= startTime.ToUniversalTime() && + T.StartedUTC <= endTime.ToUniversalTime() && + (workflowinstanceNull || T.WorkflowInstanceId == workflowInstanceId) && + (taskIdNull || T.TaskId == taskId) && + T.Status == TaskExecutionStatus.Succeeded.ToString()) + .Group(g => new { g.Version }, r => new + { + avgTotalExecution = r.Average(x => (x.DurationSeconds)), + avgArgoExecution = r.Average(x => (x.ExecutionTimeSeconds)) + }).ToListAsync(); + + var firstResult = test.FirstOrDefault() ?? new { avgTotalExecution = 0.0, avgArgoExecution = 0.0 }; + return (firstResult.avgTotalExecution, firstResult.avgArgoExecution); + } + } +} diff --git a/src/TaskManager/Plug-ins/AideClinicalReview/packages.lock.json b/src/TaskManager/Plug-ins/AideClinicalReview/packages.lock.json index c2c905b83..471d94fae 100755 --- a/src/TaskManager/Plug-ins/AideClinicalReview/packages.lock.json +++ b/src/TaskManager/Plug-ins/AideClinicalReview/packages.lock.json @@ -706,7 +706,8 @@ "type": "Project", "dependencies": { "Ardalis.GuardClauses": "[4.0.1, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )" + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )", + "Monai.Deploy.WorkflowManager.Configuration": "[1.0.0, )" } }, "monai.deploy.workflowmanager.taskmanager.api": { diff --git a/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs b/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs index 1fadff59c..8c950665d 100755 --- a/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs +++ b/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs @@ -335,10 +335,22 @@ private Dictionary GetExecutuionStats(Workflow workflow) if (workflow.Status.Nodes is not null) { + var podcount = 0; + var preprend = ""; foreach (var item in workflow.Status.Nodes) { var json = JsonConvert.SerializeObject(item.Value, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); stats.Add($"nodes.{item.Key}", json); + + if (item.Value is not null && item.Value.Type == "Pod") + { + if (item.Value.Name.EndsWith(Strings.ExitHookTemplateSendTemplateName)) + { + preprend = Strings.ExitHookTemplateSendTemplateName; + } + stats.Add($"{preprend}podStartTime{podcount}", item.Value.StartedAt is not null ? item.Value.StartedAt.ToString() : ""); + stats.Add($"{preprend}podFinishTime{podcount++}", item.Value.FinishedAt is not null ? item.Value.FinishedAt.ToString() : ""); + } } } diff --git a/src/TaskManager/Plug-ins/Argo/packages.lock.json b/src/TaskManager/Plug-ins/Argo/packages.lock.json index 0afda0e92..56d978154 100755 --- a/src/TaskManager/Plug-ins/Argo/packages.lock.json +++ b/src/TaskManager/Plug-ins/Argo/packages.lock.json @@ -1181,7 +1181,8 @@ "type": "Project", "dependencies": { "Ardalis.GuardClauses": "[4.0.1, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )" + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )", + "Monai.Deploy.WorkflowManager.Configuration": "[1.0.0, )" } }, "monai.deploy.workflowmanager.taskmanager.api": { diff --git a/src/TaskManager/TaskManager/ApplicationPartsLogger.cs b/src/TaskManager/TaskManager/ApplicationPartsLogger.cs new file mode 100644 index 000000000..d5c6ce94a --- /dev/null +++ b/src/TaskManager/TaskManager/ApplicationPartsLogger.cs @@ -0,0 +1,57 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Monai.Deploy.WorkflowManager.TaskManager +{ + public class ApplicationPartsLogger : IHostedService + { + private readonly ILogger _logger; + private readonly ApplicationPartManager _partManager; + + public ApplicationPartsLogger(ILogger logger, ApplicationPartManager partManager) + { + _logger = logger; + _partManager = partManager; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // Get the names of all the application parts. This is the short assembly name for AssemblyParts + var applicationParts = _partManager.ApplicationParts.Select(x => x.Name); + + // Create a controller feature, and populate it from the application parts + var controllerFeature = new ControllerFeature(); + _partManager.PopulateFeature(controllerFeature); + + // Get the names of all of the controllers + var controllers = controllerFeature.Controllers.Select(x => x.Name); + + // Log the application parts and controllers + _logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'", + string.Join(", ", applicationParts), string.Join(", ", controllers)); + + return Task.CompletedTask; + } + + // Required by the interface + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/TaskManager/TaskManager/Controllers/TaskStatsController.cs b/src/TaskManager/TaskManager/Controllers/TaskStatsController.cs new file mode 100644 index 000000000..7bfab7112 --- /dev/null +++ b/src/TaskManager/TaskManager/Controllers/TaskStatsController.cs @@ -0,0 +1,165 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Monai.Deploy.WorkflowManager.Configuration; +using Monai.Deploy.WorkflowManager.ControllersShared; +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Services; +using Monai.Deploy.WorkflowManager.Shared.Wrappers; +using Monai.Deploy.WorkflowManager.TaskManager.API.Models; +using Monai.Deploy.WorkflowManager.TaskManager.Database; +using Monai.Deploy.WorkflowManager.TaskManager.Filter; +using Monai.Deploy.WorkflowManager.TaskManager.Logging; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Controllers +{ + /// + /// Execution stats endpoint. + /// + [ApiController] + [Route("tasks")] + public class TaskStatsController : ApiControllerBase + { + private readonly ILogger _logger; + private readonly IUriService _uriService; + private readonly ITaskExecutionStatsRepository _repository; + + /// + /// Initializes a new instance of the class. for retreiving execution stats. + /// + /// The options set, in this case for the pagination settings. + /// DI service for uri manipulation. + /// err, the logger. + /// the repository used to store the execution stats. + /// thrown if required arguments are null. + public TaskStatsController( + IOptions options, + IUriService uriService, + ILogger logger, + ITaskExecutionStatsRepository repository) + : base(options) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _uriService = uriService ?? throw new ArgumentNullException(nameof(uriService)); + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + [HttpGet("statsoverview")] + public async Task GetOverviewAsync([FromQuery] DateTime startTime, DateTime endTime) + { + if (endTime == default) + { + endTime = DateTime.Now; + } + + if (startTime == default) + { + startTime = new DateTime(2023, 1, 1); + } + + try + { + var fails = _repository.GetStatsStatusFailedCountAsync(startTime, endTime); + var rangeCount = _repository.GetStatsCountAsync(startTime, endTime); + var stats = _repository.GetAverageStats(startTime, endTime); + + await Task.WhenAll(fails, rangeCount, stats); + return Ok(new + { + PeriodStart = startTime, + PeriodEnd = endTime, + TotalExecutions = (int)rangeCount.Result, + TotalFailures = (int)fails.Result, + AverageTotalExecutionSeconds = Math.Round(stats.Result.avgTotalExecution, 2), + AverageArgoExecutionSeconds = Math.Round(stats.Result.avgArgoExecution, 2), + }); + } + catch (Exception e) + { + _logger.GetStatsOverviewAsyncError(e); + return Problem($"Unexpected error occurred: {e.Message}", $"tasks/statsoverview", InternalServerError); + } + } + + [ProducesResponseType(typeof(StatsPagedResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + [HttpGet("stats")] + public async Task GetStatsAsync([FromQuery] TimeFilter filter, string workflowId, string taskId) + { + + if ((string.IsNullOrWhiteSpace(workflowId) && string.IsNullOrWhiteSpace(taskId)) is false + && (string.IsNullOrWhiteSpace(workflowId) || string.IsNullOrWhiteSpace(taskId))) + { + // both not empty but one is ! + _logger.LogDebug($"{nameof(GetStatsAsync)} - Failed to validate WorkflowId or TaskId"); + return Problem($"Failed to validate ids, not a valid guid", $"tasks/stats/", BadRequest); + } + + if (filter.EndTime == default) + { + filter.EndTime = DateTime.Now; + } + + if (filter.StartTime == default) + { + filter.StartTime = new DateTime(2023, 1, 1); + } + + var route = Request?.Path.Value ?? string.Empty; + var pageSize = filter.PageSize ?? Options.Value.EndpointSettings?.DefaultPageSize ?? 10; + var max = Options.Value.EndpointSettings?.MaxPageSize ?? 20; + var validFilter = new PaginationFilter(filter.PageNumber, pageSize, max); + + try + { + var allStats = _repository.GetStatsAsync(filter.StartTime, filter.EndTime, pageSize, filter.PageNumber, workflowId, taskId); + var fails = _repository.GetStatsStatusFailedCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId); + var rangeCount = _repository.GetStatsCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId); + var stats = _repository.GetAverageStats(filter.StartTime, filter.EndTime, workflowId, taskId); + + await Task.WhenAll(allStats, fails, rangeCount, stats); + + ExecutionStatDTO[] statsDto; + + statsDto = allStats.Result + .OrderBy(a => a.StartedUTC) + .Select(s => new ExecutionStatDTO(s)) + .ToArray(); + + var res = CreateStatsPagedReponse(statsDto, validFilter, rangeCount.Result, _uriService, route); + + res.PeriodStart = filter.StartTime; + res.PeriodEnd = filter.EndTime; + res.TotalExecutions = rangeCount.Result; + res.TotalFailures = fails.Result; + res.AverageTotalExecutionSeconds = Math.Round(stats.Result.avgTotalExecution, 2); + res.AverageArgoExecutionSeconds = Math.Round(stats.Result.avgArgoExecution, 2); + return Ok(res); + } + catch (Exception e) + { + _logger.GetStatsAsyncError(e); + return Problem($"Unexpected error occurred: {e.Message}", $"tasks/stats", InternalServerError); + } + + } + } +} diff --git a/src/TaskManager/TaskManager/Filter/TimeFilter.cs b/src/TaskManager/TaskManager/Filter/TimeFilter.cs new file mode 100644 index 000000000..0ef3a6642 --- /dev/null +++ b/src/TaskManager/TaskManager/Filter/TimeFilter.cs @@ -0,0 +1,26 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using Monai.Deploy.WorkflowManager.Shared.Filter; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Filter +{ + public class TimeFilter : PaginationFilter + { + public DateTime StartTime { get; set; } + + public DateTime EndTime { get; set; } + } +} diff --git a/src/TaskManager/TaskManager/Logging/Log.cs b/src/TaskManager/TaskManager/Logging/Log.cs index 0edd98d22..d504199f9 100644 --- a/src/TaskManager/TaskManager/Logging/Log.cs +++ b/src/TaskManager/TaskManager/Logging/Log.cs @@ -122,5 +122,11 @@ public static partial class Log [LoggerMessage(EventId = 120, Level = LogLevel.Error, Message = "Recovering connection to storage service: {reason}.")] public static partial void MessagingServiceErrorRecover(this ILogger logger, string reason); + + [LoggerMessage(EventId = 121, Level = LogLevel.Error, Message = "Unexpected error occurred in GET tasks/statsoverview API.")] + public static partial void GetStatsOverviewAsyncError(this ILogger logger, Exception ex); + + [LoggerMessage(EventId = 122, Level = LogLevel.Error, Message = "Unexpected error occurred in GET tasks/stats API.")] + public static partial void GetStatsAsyncError(this ILogger logger, Exception ex); } } diff --git a/src/TaskManager/TaskManager/Program.cs b/src/TaskManager/TaskManager/Program.cs index d50038454..df80f596d 100755 --- a/src/TaskManager/TaskManager/Program.cs +++ b/src/TaskManager/TaskManager/Program.cs @@ -37,10 +37,14 @@ using NLog; using NLog.LayoutRenderers; using NLog.Web; +using Microsoft.AspNetCore.Http; +using Monai.Deploy.WorkflowManager.Shared.Services; +using Microsoft.AspNetCore.Builder; + namespace Monai.Deploy.WorkflowManager.TaskManager { - internal class Program + public class Program { protected Program() { } @@ -62,6 +66,11 @@ private static void Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.CaptureStartupErrors(true); + webBuilder.UseStartup(); + }) .ConfigureHostConfiguration(configHost => { configHost.SetBasePath(Directory.GetCurrentDirectory()); @@ -84,11 +93,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) => { ConfigureServices(hostContext, services); }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.CaptureStartupErrors(true); - webBuilder.UseStartup(); - }) + .UseNLog(); private static void ConfigureServices(HostBuilderContext hostContext, IServiceCollection services) @@ -107,8 +112,10 @@ private static void ConfigureServices(HostBuilderContext hostContext, IServiceCo // Mongo DB (Workflow Manager) services.Configure(hostContext.Configuration.GetSection("WorkloadManagerDatabase")); + services.Configure(hostContext.Configuration.GetSection("WorkloadManagerDatabase")); services.AddSingleton(s => new MongoClient(hostContext.Configuration["WorkloadManagerDatabase:ConnectionString"])); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddMigration(new MongoMigrationSettings { @@ -119,6 +126,17 @@ private static void ConfigureServices(HostBuilderContext hostContext, IServiceCo services.AddTransient(); services.AddTaskManager(hostContext); + services.AddHostedService(); + + services.AddHttpContextAccessor(); + services.AddSingleton(p => + { + var accessor = p.GetRequiredService(); + var request = accessor?.HttpContext?.Request; + var uri = string.Concat(request?.Scheme, "://", request?.Host.ToUriComponent()); + var newUri = new Uri(uri); + return new UriService(newUri); + }); } private static Logger ConfigureNLog(string assemblyVersionNumber) diff --git a/src/TaskManager/TaskManager/TaskManager.cs b/src/TaskManager/TaskManager/TaskManager.cs index 3faa31ec5..b14461415 100755 --- a/src/TaskManager/TaskManager/TaskManager.cs +++ b/src/TaskManager/TaskManager/TaskManager.cs @@ -31,6 +31,7 @@ using Monai.Deploy.WorkflowManager.TaskManager.API; using Monai.Deploy.WorkflowManager.TaskManager.API.Extensions; using Monai.Deploy.WorkflowManager.TaskManager.API.Models; +using Monai.Deploy.WorkflowManager.TaskManager.Database; using Monai.Deploy.WorkflowManager.TaskManager.Logging; namespace Monai.Deploy.WorkflowManager.TaskManager @@ -46,6 +47,7 @@ public class TaskManager : IHostedService, IDisposable, IMonaiService private readonly IServiceScope _scope; private readonly CancellationTokenSource _cancellationTokenSource; private readonly IStorageService _storageService; + private readonly ITaskExecutionStatsRepository _taskExecutionStatsRepository; private readonly ITaskDispatchEventService _taskDispatchEventService; private CancellationToken _cancellationToken; private IMessageBrokerPublisherService? _messageBrokerPublisherService; @@ -74,6 +76,7 @@ public TaskManager( _storageAdminService = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IStorageAdminService)); _storageService = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(IStorageService)); _taskDispatchEventService = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(ITaskDispatchEventService)); + _taskExecutionStatsRepository = _scope.ServiceProvider.GetService() ?? throw new ServiceNotFoundException(nameof(ITaskExecutionStatsRepository)); _messageBrokerPublisherService = null; _messageBrokerSubscriberService = null; _activeJobs = 0; @@ -357,6 +360,7 @@ private async Task HandleDispatchTask(JsonMessage message) await _taskDispatchEventService.CreateAsync(eventInfo).ConfigureAwait(false); message.Body.Validate(); pluginAssembly = _options.Value.TaskManager.PluginAssemblyMappings[message.Body.TaskPluginType]; + await _taskExecutionStatsRepository.CreateAsync(eventInfo); } catch (MessageValidationException ex) { @@ -539,6 +543,7 @@ private async Task SendUpdateEvent(JsonMessage message) try { + await _taskExecutionStatsRepository.UpdateExecutionStatsAsync(message.Body); _logger.SendingTaskUpdateMessage(_options.Value.Messaging.Topics.TaskUpdateRequest, message.Body.Reason); await _messageBrokerPublisherService!.Publish(_options.Value.Messaging.Topics.TaskUpdateRequest, message.ToMessage()).ConfigureAwait(false); _logger.TaskUpdateMessageSent(_options.Value.Messaging.Topics.TaskUpdateRequest); diff --git a/src/TaskManager/TaskManager/appsettings.json b/src/TaskManager/TaskManager/appsettings.json index 737db1b3a..60074c561 100755 --- a/src/TaskManager/TaskManager/appsettings.json +++ b/src/TaskManager/TaskManager/appsettings.json @@ -60,7 +60,7 @@ "messageSenderContainerCpuLimit": "1", "messageSenderContainerMemoryLimit": "500Mi" }, - "argoExitHookSendMessageContainerImage": "ghcr.io/jandelgado/rabtap:latest" + "argoExitHookSendMessageContainerImage": "ghcr.io/project-monai/monai-deploy-task-manager-callback:0.2.0-beta.211" }, "messaging": { "retries": { diff --git a/src/TaskManager/TaskManager/packages.lock.json b/src/TaskManager/TaskManager/packages.lock.json index 09f473980..aa07fad17 100755 --- a/src/TaskManager/TaskManager/packages.lock.json +++ b/src/TaskManager/TaskManager/packages.lock.json @@ -1894,7 +1894,8 @@ "type": "Project", "dependencies": { "Ardalis.GuardClauses": "[4.0.1, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )" + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )", + "Monai.Deploy.WorkflowManager.Configuration": "[1.0.0, )" } }, "monai.deploy.workflowmanager.taskmanager.aideclinicalreview": { diff --git a/src/TaskManager/TaskManager/stylecop.json b/src/TaskManager/TaskManager/stylecop.json new file mode 100644 index 000000000..42fb1f8ea --- /dev/null +++ b/src/TaskManager/TaskManager/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "PlaceholderCompany" + } + } +} diff --git a/src/WorkflowManager/PayloadListener/packages.lock.json b/src/WorkflowManager/PayloadListener/packages.lock.json index 9273967c4..0ff85d6ec 100755 --- a/src/WorkflowManager/PayloadListener/packages.lock.json +++ b/src/WorkflowManager/PayloadListener/packages.lock.json @@ -775,7 +775,8 @@ "type": "Project", "dependencies": { "Ardalis.GuardClauses": "[4.0.1, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )" + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )", + "Monai.Deploy.WorkflowManager.Configuration": "[1.0.0, )" } }, "monai.deploy.workflowmanager.storage": { diff --git a/src/WorkflowManager/WorkflowExecuter/packages.lock.json b/src/WorkflowManager/WorkflowExecuter/packages.lock.json index aa299d4c8..822d65044 100755 --- a/src/WorkflowManager/WorkflowExecuter/packages.lock.json +++ b/src/WorkflowManager/WorkflowExecuter/packages.lock.json @@ -775,7 +775,8 @@ "type": "Project", "dependencies": { "Ardalis.GuardClauses": "[4.0.1, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )" + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )", + "Monai.Deploy.WorkflowManager.Configuration": "[1.0.0, )" } }, "monai.deploy.workflowmanager.storage": { diff --git a/src/WorkflowManager/WorkflowManager/Controllers/AuthenticatedApiControllerBase.cs b/src/WorkflowManager/WorkflowManager/Controllers/AuthenticatedApiControllerBase.cs index e5a586169..29a1a31ec 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/AuthenticatedApiControllerBase.cs +++ b/src/WorkflowManager/WorkflowManager/Controllers/AuthenticatedApiControllerBase.cs @@ -18,13 +18,13 @@ using Microsoft.Extensions.Options; using Monai.Deploy.WorkflowManager.Configuration; -namespace Monai.Deploy.WorkflowManager.Controllers +namespace Monai.Deploy.WorkflowManager.ControllersShared { /// /// Base authenticated api controller base. /// [Authorize] - public class AuthenticatedApiControllerBase : ApiControllerBase + public class AuthenticatedApiControllerBase : WFMApiControllerBase { /// /// Initializes a new instance of the class. diff --git a/src/WorkflowManager/WorkflowManager/Controllers/PayloadsController.cs b/src/WorkflowManager/WorkflowManager/Controllers/PayloadsController.cs index 4ec618b7f..c4316fcd1 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/PayloadsController.cs +++ b/src/WorkflowManager/WorkflowManager/Controllers/PayloadsController.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,12 @@ using Monai.Deploy.WorkflowManager.Common.Interfaces; using Monai.Deploy.WorkflowManager.Configuration; using Monai.Deploy.WorkflowManager.Contracts.Models; -using Monai.Deploy.WorkflowManager.Filter; using Monai.Deploy.WorkflowManager.Logging; -using Monai.Deploy.WorkflowManager.Services; -using Monai.Deploy.WorkflowManager.Wrappers; +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Services; +using Monai.Deploy.WorkflowManager.Shared.Wrappers; -namespace Monai.Deploy.WorkflowManager.Controllers +namespace Monai.Deploy.WorkflowManager.ControllersShared { /// /// Payloads Controller. diff --git a/src/WorkflowManager/WorkflowManager/Controllers/TasksController.cs b/src/WorkflowManager/WorkflowManager/Controllers/TasksController.cs index 5ad951cec..bab1a2a81 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/TasksController.cs +++ b/src/WorkflowManager/WorkflowManager/Controllers/TasksController.cs @@ -25,13 +25,13 @@ using Monai.Deploy.WorkflowManager.Common.Interfaces; using Monai.Deploy.WorkflowManager.Configuration; using Monai.Deploy.WorkflowManager.Contracts.Models; -using Monai.Deploy.WorkflowManager.Filter; using Monai.Deploy.WorkflowManager.Logging; using Monai.Deploy.WorkflowManager.Models; -using Monai.Deploy.WorkflowManager.Services; -using Monai.Deploy.WorkflowManager.Wrappers; +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Services; +using Monai.Deploy.WorkflowManager.Shared.Wrappers; -namespace Monai.Deploy.WorkflowManager.Controllers +namespace Monai.Deploy.WorkflowManager.ControllersShared { /// /// Tasks Api endpoint controller. diff --git a/src/WorkflowManager/WorkflowManager/Controllers/WFMApiControllerBase.cs b/src/WorkflowManager/WorkflowManager/Controllers/WFMApiControllerBase.cs new file mode 100644 index 000000000..065e4daab --- /dev/null +++ b/src/WorkflowManager/WorkflowManager/Controllers/WFMApiControllerBase.cs @@ -0,0 +1,42 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Monai.Deploy.WorkflowManager.Configuration; + +namespace Monai.Deploy.WorkflowManager.ControllersShared +{ + /// + /// Base Api Controller. + /// + [ApiController] + public class WFMApiControllerBase : ApiControllerBase + { + private readonly IOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// Workflow manager options. + public WFMApiControllerBase(IOptions options) + : base(options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + } +} diff --git a/src/WorkflowManager/WorkflowManager/Controllers/WorkflowInstanceController.cs b/src/WorkflowManager/WorkflowManager/Controllers/WorkflowInstanceController.cs index 94a71b09f..cb5ec3ed7 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/WorkflowInstanceController.cs +++ b/src/WorkflowManager/WorkflowManager/Controllers/WorkflowInstanceController.cs @@ -26,11 +26,11 @@ using Monai.Deploy.WorkflowManager.Common.Interfaces; using Monai.Deploy.WorkflowManager.Configuration; using Monai.Deploy.WorkflowManager.Contracts.Models; -using Monai.Deploy.WorkflowManager.Filter; using Monai.Deploy.WorkflowManager.Logging; -using Monai.Deploy.WorkflowManager.Services; +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Services; -namespace Monai.Deploy.WorkflowManager.Controllers +namespace Monai.Deploy.WorkflowManager.ControllersShared { /// /// Workflow Instances Controller. diff --git a/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs b/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs index f982ffe19..c16d3cbc9 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs +++ b/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,13 @@ using Monai.Deploy.WorkflowManager.Configuration; using Monai.Deploy.WorkflowManager.Contracts.Models; using Monai.Deploy.WorkflowManager.Contracts.Responses; -using Monai.Deploy.WorkflowManager.Filter; using Monai.Deploy.WorkflowManager.Logging; -using Monai.Deploy.WorkflowManager.Services; +using Monai.Deploy.WorkflowManager.Shared.Filter; +using Monai.Deploy.WorkflowManager.Shared.Services; +using Monai.Deploy.WorkflowManager.Shared.Wrappers; using Monai.Deploy.WorkflowManager.Validators; -using Monai.Deploy.WorkflowManager.Wrappers; -namespace Monai.Deploy.WorkflowManager.Controllers +namespace Monai.Deploy.WorkflowManager.ControllersShared { /// /// Workflows Controller. diff --git a/src/WorkflowManager/WorkflowManager/Program.cs b/src/WorkflowManager/WorkflowManager/Program.cs index 61dbb4317..59b8e7475 100755 --- a/src/WorkflowManager/WorkflowManager/Program.cs +++ b/src/WorkflowManager/WorkflowManager/Program.cs @@ -1,5 +1,5 @@ /* - * Copyright 2022 MONAI Consortium + * Copyright 2023 MONAI Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ using Monai.Deploy.WorkflowManager.Services; using Monai.Deploy.WorkflowManager.Services.DataRetentionService; using Monai.Deploy.WorkflowManager.Services.Http; +using Monai.Deploy.WorkflowManager.Shared.Services; using Monai.Deploy.WorkflowManager.Validators; using Mongo.Migration.Startup; using Mongo.Migration.Startup.DotNetCore; diff --git a/src/WorkflowManager/WorkflowManager/packages.lock.json b/src/WorkflowManager/WorkflowManager/packages.lock.json index d4f70f5a8..8f73ef6cd 100755 --- a/src/WorkflowManager/WorkflowManager/packages.lock.json +++ b/src/WorkflowManager/WorkflowManager/packages.lock.json @@ -1556,7 +1556,8 @@ "type": "Project", "dependencies": { "Ardalis.GuardClauses": "[4.0.1, )", - "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )" + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "[6.0.15, )", + "Monai.Deploy.WorkflowManager.Configuration": "[1.0.0, )" } }, "monai.deploy.workflowmanager.storage": { diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/ExecutionStats.feature b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/ExecutionStats.feature new file mode 100644 index 000000000..1fbb6cddd --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/ExecutionStats.feature @@ -0,0 +1,70 @@ +# Copyright 2022 MONAI Consortium +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@IntegrationTests +Feature: ExecutionStats + +Execution stats are returned for Tasks + +@ExecutionStats +Scenario: Execution Stats table is populated when after consuming a TaskDispatchEvent + Given A Task Dispatch event is published Task_Dispatch_Accepted + And the Execution Stats table is populated correctly for a TaskDispatchEvent + +@ExecutionStats @ignore +Scenario: Execution Stats table is updated after consuming a TaskCallbackEvent + Given A Task Dispatch event is published Task_Dispatch_Execution_Stats + And the Execution Stats table is populated correctly for a TaskDispatchEvent + When A Task Callback event is published Task_Callback_Execution_Stats + Then the Execution Stats table is populated correctly for a TaskCallbackEvent + +@ExecutionStats +Scenario Outline: Summary of Execution Stats are returned + Given Execution Stats table is populated + And I have a TaskManager endpoint /tasks/statsoverview + And I set the start time to be UTC + When I send a GET request + Then I will get a 200 response + And I can see expected summary execution stats are returned + Examples: + | startTime | + | -61 | + | -31 | + +@ExecutionStats +Scenario Outline: Execution Stats for a Task are returned + Given Execution Stats table is populated + And I have a TaskManager endpoint /tasks/stats + And I set WorkflowId as and TaskId as + When I send a GET request + Then I will get a 200 response + And I can see expected execution stats are returned + Examples: + | workflowId | taskId | + | Workflow_1 | Task_1 | + | Workflow_1 | Task_2 | + +@ExecutionStats +Scenario Outline: Execution Stats are not returned if Workflow or Task is not found + Given Execution Stats table is populated + And I have a TaskManager endpoint /tasks/stats + And I set WorkflowId as and TaskId as + When I send a GET request + Then I will get a 200 response + And I can see expected execution stats are returned + Examples: + | workflowId | taskId | + | Workflow_2 | Task_1 | + | Workflow_1 | Task_3 | + diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/HealthApi.feature b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/HealthApi.feature index b449c7e68..b1c7138c2 100644 --- a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/HealthApi.feature +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/HealthApi.feature @@ -21,5 +21,5 @@ Health check API for Task Manager. Scenario: Get Health status of Task Manager Given I have a TaskManager endpoint /health When I send a GET request - Then I will get a 200 response - And I will get a health check response status message Healthy \ No newline at end of file + Then I will get a 503 response + And I will get a health check response message diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_1_Task_3_.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_1_Task_3_.snap new file mode 100644 index 000000000..c76cb2097 --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_1_Task_3_.snap @@ -0,0 +1,36 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "periodStart": "2023-01-01T00:00:00", + "periodEnd": "2023-04-11T10:13:29.9717784+01:00", + "totalExecutions": 0, + "totalFailures": 0, + "averageTotalExecutionSeconds": 0.0, + "averageArgoExecutionSeconds": 0.0, + "pageNumber": 1, + "pageSize": 10, + "firstPage": "/tasks/stats?pageNumber=1&pageSize=10", + "lastPage": "/tasks/stats?pageNumber=1&pageSize=10", + "totalPages": 0, + "totalRecords": 0, + "nextPage": null, + "previousPage": null, + "data": [], + "succeeded": true, + "errors": null, + "message": null +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_2_Task_1_.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_2_Task_1_.snap new file mode 100644 index 000000000..ac7c6710f --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsAreNotReturnedIfWorkflowOrTaskIsNotFound_Workflow_2_Task_1_.snap @@ -0,0 +1,36 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "periodStart": "2023-01-01T00:00:00", + "periodEnd": "2023-04-11T10:13:26.6294812+01:00", + "totalExecutions": 0, + "totalFailures": 0, + "averageTotalExecutionSeconds": 0.0, + "averageArgoExecutionSeconds": 0.0, + "pageNumber": 1, + "pageSize": 10, + "firstPage": "/tasks/stats?pageNumber=1&pageSize=10", + "lastPage": "/tasks/stats?pageNumber=1&pageSize=10", + "totalPages": 0, + "totalRecords": 0, + "nextPage": null, + "previousPage": null, + "data": [], + "succeeded": true, + "errors": null, + "message": null +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_Workflow_1_Task_2_.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_Workflow_1_Task_2_.snap new file mode 100644 index 000000000..c6f8a7fd3 --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_Workflow_1_Task_2_.snap @@ -0,0 +1,44 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "periodStart": "2023-01-01T00:00:00", + "periodEnd": "2023-04-11T10:13:30.5584818+01:00", + "totalExecutions": 1, + "totalFailures": 0, + "averageTotalExecutionSeconds": 30.0, + "averageArgoExecutionSeconds": 30.0, + "pageNumber": 1, + "pageSize": 10, + "firstPage": "/tasks/stats?pageNumber=1&pageSize=10", + "lastPage": "/tasks/stats?pageNumber=1&pageSize=10", + "totalPages": 1, + "totalRecords": 1, + "nextPage": null, + "previousPage": null, + "data": [ + { + "executionId": "73127fd4-20cb-42c1-8138-01db8b0d35d6", + "startedAt": "2023-02-10T09:13:26.544Z", + "finishedAt": "2023-02-10T10:12:56.544Z", + "executionDurationSeconds": 30.0, + "status": "Succeeded" + } + ], + "succeeded": true, + "errors": null, + "message": null +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_workflow_1_task_1_.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_workflow_1_task_1_.snap new file mode 100644 index 000000000..e5aca7f67 --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.ExecutionStatsForATaskAreReturned_workflow_1_task_1_.snap @@ -0,0 +1,65 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "periodStart": "2023-01-01T00:00:00", + "periodEnd": "2023-04-11T10:13:30.2789792+01:00", + "totalExecutions": 4, + "totalFailures": 1, + "averageTotalExecutionSeconds": 30.0, + "averageArgoExecutionSeconds": 30.0, + "pageNumber": 1, + "pageSize": 10, + "firstPage": "/tasks/stats?pageNumber=1&pageSize=10", + "lastPage": "/tasks/stats?pageNumber=1&pageSize=10", + "totalPages": 1, + "totalRecords": 4, + "nextPage": null, + "previousPage": null, + "data": [ + { + "executionId": "9f545b20-abf6-4c70-b64e-52c68ece8e37", + "startedAt": "2023-02-10T09:13:26.544Z", + "finishedAt": "2023-02-10T10:12:56.544Z", + "executionDurationSeconds": 30.0, + "status": "Succeeded" + }, + { + "executionId": "83c734a4-a511-46ad-9384-7dbf54a2c696", + "startedAt": "2023-02-10T09:13:26.544Z", + "finishedAt": "2023-02-10T10:12:56.544Z", + "executionDurationSeconds": 30.0, + "status": "Succeeded" + }, + { + "executionId": "73127fd4-20cb-42c1-8138-01db8b0d35d6", + "startedAt": "2023-02-10T09:13:26.544Z", + "finishedAt": "2023-02-10T10:12:56.544Z", + "executionDurationSeconds": 30.0, + "status": "Failed" + }, + { + "executionId": "83c734a4-a511-46ad-9384-7dbf54a2c696", + "startedAt": "2023-03-12T09:13:26.544Z", + "finishedAt": "2023-03-12T10:12:56.544Z", + "executionDurationSeconds": 30.0, + "status": "Succeeded" + } + ], + "succeeded": true, + "errors": null, + "message": null +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-31_.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-31_.snap new file mode 100644 index 000000000..151ff899a --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-31_.snap @@ -0,0 +1,24 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "periodStart": "2023-03-11T08:19:51Z", + "periodEnd": "2023-03-21T08:19:51Z", + "totalExecutions": 1, + "totalFailures": 0, + "averageTotalExecutionSeconds": 30.0, + "averageArgoExecutionSeconds": 30.0 +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-61_.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-61_.snap new file mode 100644 index 000000000..f4aae17e6 --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/ExecutionStatsFeature.SummaryOfExecutionStatsAreReturned_-61_.snap @@ -0,0 +1,24 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "periodStart": "2023-02-09T08:19:51Z", + "periodEnd": "2023-02-19T08:19:51Z", + "totalExecutions": 4, + "totalFailures": 1, + "averageTotalExecutionSeconds": 30.0, + "averageArgoExecutionSeconds": 30.0 +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/HealthApiFeature.GetHealthStatusOfTaskManager.snap b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/HealthApiFeature.GetHealthStatusOfTaskManager.snap new file mode 100644 index 000000000..86e0bb4f2 --- /dev/null +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Features/__snapshots__/HealthApiFeature.GetHealthStatusOfTaskManager.snap @@ -0,0 +1,45 @@ +/* + * Copyright 2023 MONAI Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "status": "Unhealthy", + "checks": [ + { + "check": "Task Manager Services", + "result": "Healthy" + }, + { + "check": "mongodb", + "result": "Healthy" + }, + { + "check": "minio", + "result": "Healthy" + }, + { + "check": "minio-admin", + "result": "Unhealthy" + }, + { + "check": "Rabbit MQ Publisher", + "result": "Healthy" + }, + { + "check": "Rabbit MQ Subscriber", + "result": "Healthy" + } + ] +} diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Hooks.cs b/tests/IntegrationTests/TaskManager.IntegrationTests/Hooks.cs index 26c996bf2..4a9885476 100755 --- a/tests/IntegrationTests/TaskManager.IntegrationTests/Hooks.cs +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Hooks.cs @@ -70,10 +70,12 @@ public static void Init() TestExecutionConfig.RabbitConfig.TaskCallbackQueue = "md.tasks.callback"; TestExecutionConfig.RabbitConfig.TaskUpdateQueue = "md.tasks.update"; TestExecutionConfig.RabbitConfig.ClinicalReviewQueue = "aide.clinical_review.request"; + TestExecutionConfig.RabbitConfig.TaskCancellationQueue = "md.tasks.cancellation"; TestExecutionConfig.MongoConfig.ConnectionString = config.GetValue("WorkloadManagerDatabase:ConnectionString"); TestExecutionConfig.MongoConfig.Database = config.GetValue("WorkloadManagerDatabase:DatabaseName"); TestExecutionConfig.MongoConfig.TaskDispatchEventCollection = "TaskDispatchEvents"; + TestExecutionConfig.MongoConfig.ExecutionStatsCollection = "ExecutionStats"; TestExecutionConfig.MinioConfig.Endpoint = config.GetValue("WorkflowManager:storage:settings:endpoint"); TestExecutionConfig.MinioConfig.AccessKey = config.GetValue("WorkflowManager:storage:settings:accessKey"); @@ -102,6 +104,8 @@ public static void ClearTestData() RabbitConnectionFactory.PurgeAllQueues(); MongoClient?.DeleteAllTaskDispatch(); + + MongoClient?.DeleteAllExecutionStats(); } // @@ -159,6 +163,7 @@ public void SetUp(ScenarioContext scenarioContext, ISpecFlowOutputHelper outputH [AfterTestRun(Order = 1)] public static void TearDownRabbit() { + RabbitConnectionFactory.DeleteAllQueues(); Host?.StopAsync(); } } diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Monai.Deploy.WorkflowManager.TaskManager.IntegrationTests.csproj b/tests/IntegrationTests/TaskManager.IntegrationTests/Monai.Deploy.WorkflowManager.TaskManager.IntegrationTests.csproj index 767313dd8..b6d0d494e 100755 --- a/tests/IntegrationTests/TaskManager.IntegrationTests/Monai.Deploy.WorkflowManager.TaskManager.IntegrationTests.csproj +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Monai.Deploy.WorkflowManager.TaskManager.IntegrationTests.csproj @@ -1,4 +1,4 @@ -