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 @@
-