Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
39861f7
Create dependabot.yml
mocsharp Aug 30, 2022
9334da2
Bump actions/cache from 2.1.7 to 3.0.8 (#123)
dependabot[bot] Sep 2, 2022
9a94d2c
Bump actions/setup-dotnet from 1 to 2 (#121)
dependabot[bot] Sep 2, 2022
536e2c5
Bump anchore/scan-action from 3.2.0 to 3.2.5 (#120)
dependabot[bot] Sep 2, 2022
0746d1d
Bump actions/setup-java from 1 to 3 (#122)
dependabot[bot] Sep 2, 2022
3e85792
Bump docker/login-action from 1.12.0 to 2.0.0 (#126)
dependabot[bot] Sep 2, 2022
64264fc
Implement FHIR server (#118)
mocsharp Sep 6, 2022
df6ecad
Integrate MS Health Check Service (#130)
mocsharp Sep 6, 2022
336ff33
Bump codecov/codecov-action from 2 to 3 (#131)
dependabot[bot] Sep 6, 2022
4aeab01
Bump jungwinter/split from 1 to 2 (#136)
dependabot[bot] Sep 6, 2022
5fbf6ae
Bump actions/download-artifact from 2 to 3 (#139)
dependabot[bot] Sep 6, 2022
63f5ab7
Bump actions/upload-artifact from 2.3.1 to 3.1.0 (#133)
dependabot[bot] Sep 6, 2022
f6745f6
Bump crazy-max/ghaction-chocolatey from 1 to 2 (#132)
dependabot[bot] Sep 6, 2022
c5356e0
Bump docker/metadata-action from 3.6.2 to 4.0.1 (#135)
dependabot[bot] Sep 6, 2022
0f2c714
Bump actions/checkout from 2 to 3 (#143)
dependabot[bot] Sep 6, 2022
b39eb74
Bump gittools/actions from 0.9.11 to 0.9.13 (#142)
dependabot[bot] Sep 6, 2022
424d029
Bump docker/build-push-action from 2.9.0 to 3.1.1 (#140)
dependabot[bot] Sep 6, 2022
084c94d
Merge Release/0.3.0 into develop (#150)
mocsharp Sep 13, 2022
1579f20
Merge main to develop (#151)
mocsharp Sep 14, 2022
82eb310
Bump Docker.DotNet from 3.125.10 to 3.125.11 (#147)
dependabot[bot] Sep 14, 2022
c758629
Bump System.IO.Abstractions from 17.1.1 to 17.2.1 (#146)
dependabot[bot] Sep 14, 2022
d469873
Bump System.IO.Abstractions.TestingHelpers from 17.1.1 to 17.2.1 (#145)
dependabot[bot] Sep 14, 2022
1932b08
Release/0.3.0 (#152)
mocsharp Sep 14, 2022
1ff3bad
Update .gitversion.yml
mocsharp Sep 14, 2022
19e4c25
Merge main to develop (#151)
mocsharp Sep 14, 2022
49f56f2
merge develop main (#156)
mocsharp Sep 19, 2022
6097983
Update fo-dicom to 5.0.3 (#164)
mocsharp Sep 20, 2022
321abbb
Bump anchore/scan-action from 3.2.5 to 3.3.0 (#158)
dependabot[bot] Sep 20, 2022
5b05935
Update default SCU AET to MONAISCU (#157)
mocsharp Sep 21, 2022
09887b1
Ability to switch temporary storage to use either memory or disk (#166)
mocsharp Sep 23, 2022
dc898fe
Revert "Ability to switch temporary storage to use either memory or d…
mocsharp Sep 23, 2022
148c1e4
rebased changes (#170)
neildsouth Sep 26, 2022
34974f1
Ability to switch temporary storage to use either memory or disk (#169)
mocsharp Sep 26, 2022
6487fa3
Bump Microsoft.NET.Test.Sdk from 17.3.1 to 17.3.2
dependabot[bot] Sep 26, 2022
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Ability to switch temporary storage to use either memory or disk (#166)
* Ability to switch temporary storage to use either memory or disk Signed-off-by: Victor Chang <vicchang@nvidia.com> * Fix configuration name for temp data storage. Signed-off-by: Victor Chang <vicchang@nvidia.com> * Validate storage configurations based on temp storage location Signed-off-by: Victor Chang <vicchang@nvidia.com> * Log time for a payload to complete end-to-end within the service. Signed-off-by: Victor Chang <vicchang@nvidia.com> Signed-off-by: Victor Chang <vicchang@nvidia.com>
  • Loading branch information
mocsharp authored Sep 23, 2022
commit 09887b1bff6ec7d77e69e0256edc76bac1ec6a82
2 changes: 2 additions & 0 deletions src/Api/Storage/Payload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public enum PayloadState

public bool HasTimedOut { get => ElapsedTime().TotalSeconds >= Timeout; }

public TimeSpan Elapsed { get { return DateTime.UtcNow.Subtract(DateTimeCreated); } }

public string CallingAeTitle { get => Files.OfType<DicomFileStorageMetadata>().Select(p => p.CallingAeTitle).FirstOrDefault(); }

public string CalledAeTitle { get => Files.OfType<DicomFileStorageMetadata>().Select(p => p.CalledAeTitle).FirstOrDefault(); }
Expand Down
25 changes: 22 additions & 3 deletions src/Api/Storage/StorageObjectMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

using System;
using System.IO;
using System.Runtime;
using System.Text.Json.Serialization;
using Ardalis.GuardClauses;

Expand Down Expand Up @@ -123,9 +124,27 @@ public void SetUploaded(string bucketName)

if (Data is not null && Data.CanSeek)
{
Data.Close();
Data.Dispose();
Data = null;
if (Data is FileStream fileStream)
{
var filename = fileStream.Name;
Data.Close();
Data.Dispose();
Data = null;
System.IO.File.Delete(filename);
}
else // MemoryStream
{
Data.Close();
Data.Dispose();
Data = null;

// When IG stores all received/downloaded data in-memory using MemoryStream, LOH grows tremendously and thus impacts the performance and
// memory usage. The following makes sure LOH is compacted after the data is uploaded.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
#pragma warning disable S1215 // "GC.Collect" should not be called
GC.Collect();
#pragma warning restore S1215 // "GC.Collect" should not be called
}
}
}

Expand Down
38 changes: 37 additions & 1 deletion src/Configuration/ConfigurationValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -29,16 +31,18 @@ namespace Monai.Deploy.InformaticsGateway.Configuration
public class ConfigurationValidator : IValidateOptions<InformaticsGatewayConfiguration>
{
private readonly ILogger<ConfigurationValidator> _logger;
private readonly IFileSystem _fileSystem;
private readonly List<string> _validationErrors;

/// <summary>
/// Initializes an instance of the <see cref="ConfigurationValidator"/> class.
/// </summary>
/// <param name="configuration">InformaticsGatewayConfiguration to be validated</param>
/// <param name="logger">Logger to be used by ConfigurationValidator</param>
public ConfigurationValidator(ILogger<ConfigurationValidator> logger)
public ConfigurationValidator(ILogger<ConfigurationValidator> logger, IFileSystem fileSystem)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_validationErrors = new List<string>();
}

Expand Down Expand Up @@ -82,6 +86,38 @@ private bool IsStorageValid(StorageConfiguration storage)
valid &= IsValueInRange("InformaticsGateway>storage>reserveSpaceGB", 1, 999, storage.ReserveSpaceGB);
valid &= IsValueInRange("InformaticsGateway>storage>payloadProcessThreads", 1, 128, storage.PayloadProcessThreads);
valid &= IsValueInRange("InformaticsGateway>storage>concurrentUploads", 1, 128, storage.ConcurrentUploads);
valid &= IsValueInRange("InformaticsGateway>storage>memoryThreshold", 1, int.MaxValue, storage.MemoryThreshold);

if (storage.TemporaryDataStorage == TemporaryDataStorageLocation.Disk)
{
valid &= IsNotNullOrWhiteSpace("InformaticsGateway>storage>bufferRootPath", storage.BufferStorageRootPath);
valid &= IsValidDirectory("InformaticsGateway>storage>bufferRootPath", storage.BufferStorageRootPath);
valid &= IsValueInRange("InformaticsGateway>storage>bufferSize", 1, int.MaxValue, storage.BufferSize);
}

return valid;
}

private bool IsValidDirectory(string source, string directory)
{
var valid = true;
try
{
if (!_fileSystem.Directory.Exists(directory))
{
valid = false;
_validationErrors.Add($"Directory `{directory}` specified in `{source}` does not exist.");
}
else
{
using var _ = _fileSystem.File.Create(Path.Combine(directory, Path.GetRandomFileName()), 1, FileOptions.DeleteOnClose);
}
}
catch (Exception ex)
{
valid = false;
_validationErrors.Add($"Directory `{directory}` specified in `{source}` is not accessible: {ex.Message}.");
}
return valid;
}

Expand Down
15 changes: 15 additions & 0 deletions src/Configuration/StorageConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,28 @@ namespace Monai.Deploy.InformaticsGateway.Configuration
{
public class StorageConfiguration : StorageServiceConfiguration
{
/// <summary>
/// Gets or sets whether to store temporary data in <c>Memory</c> or on <c>Disk</c>.
/// Defaults to <c>Memory</c>.
/// </summary>
[ConfigurationKeyName("tempStorageLocation")]
public TemporaryDataStorageLocation TemporaryDataStorage { get; set; } = TemporaryDataStorageLocation.Memory;

/// <summary>
/// Gets or sets the path used for buffering incoming data.
/// Defaults to <c>./temp</c>.
/// </summary>
[ConfigurationKeyName("bufferRootPath")]
public string BufferStorageRootPath { get; set; } = "./temp";

/// <summary>
/// Gets or sets the number of bytes buffered for reads and writes to the temporary file.
/// Defaults to <c>128000</c>.
/// </summary>
[ConfigurationKeyName("bufferSize")]
public int BufferSize { get; set; } = 128000;


/// <summary>
/// Gets or set the maximum memory buffer size in bytes with default to 30MiB.
/// </summary>
Expand Down
24 changes: 24 additions & 0 deletions src/Configuration/TemporaryDataStorageLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.
*/

namespace Monai.Deploy.InformaticsGateway.Configuration
{
public enum TemporaryDataStorageLocation
{
Memory,
Disk
}
}
38 changes: 31 additions & 7 deletions src/Configuration/Test/ConfigurationValidatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/

using System;
using System.IO;
using System.IO.Abstractions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Monai.Deploy.InformaticsGateway.SharedTest;
Expand All @@ -26,17 +28,19 @@ namespace Monai.Deploy.InformaticsGateway.Configuration.Test
public class ConfigurationValidatorTest
{
private readonly Mock<ILogger<ConfigurationValidator>> _logger;
private readonly Mock<IFileSystem> _fileSystem;

public ConfigurationValidatorTest()
{
_logger = new Mock<ILogger<ConfigurationValidator>>();
_fileSystem = new Mock<IFileSystem>();
}

[Fact(DisplayName = "ConfigurationValidator test with all valid settings")]
public void AllValid()
{
var config = MockValidConfiguration();
var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);
Assert.True(valid == ValidateOptionsResult.Success);
}

Expand All @@ -46,7 +50,7 @@ public void InvalidScpPort()
var config = MockValidConfiguration();
config.Dicom.Scp.Port = Int32.MaxValue;

var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessage = $"Invalid port number '{Int32.MaxValue}' specified for InformaticsGateway>dicom>scp>port.";
Assert.Equal(validationMessage, valid.FailureMessage);
Expand All @@ -59,7 +63,7 @@ public void InvalidScpMaxAssociations()
var config = MockValidConfiguration();
config.Dicom.Scp.MaximumNumberOfAssociations = 0;

var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessage = $"Value of InformaticsGateway>dicom>scp>max-associations must be between {1} and {1000}.";
Assert.Equal(validationMessage, valid.FailureMessage);
Expand All @@ -72,7 +76,7 @@ public void StorageWithInvalidWatermark()
var config = MockValidConfiguration();
config.Storage.Watermark = 1000;

var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessage = "Value of InformaticsGateway>storage>watermark must be between 1 and 100.";
Assert.Equal(validationMessage, valid.FailureMessage);
Expand All @@ -85,7 +89,7 @@ public void StorageWithInvalidReservedSpace()
var config = MockValidConfiguration();
config.Storage.ReserveSpaceGB = 9999;

var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessage = "Value of InformaticsGateway>storage>reserveSpaceGB must be between 1 and 999.";
Assert.Equal(validationMessage, valid.FailureMessage);
Expand All @@ -98,7 +102,7 @@ public void StorageWithInvalidTemporaryBucketName()
var config = MockValidConfiguration();
config.Storage.TemporaryStorageBucket = " ";

var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessages = new[] { "Value for InformaticsGateway>storage>temporaryBucketName is required.", "Value for InformaticsGateway>storage>temporaryBucketName does not conform to Amazon S3 bucket naming requirements." };
Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage);
Expand All @@ -114,7 +118,7 @@ public void StorageWithInvalidBucketName()
var config = MockValidConfiguration();
config.Storage.StorageServiceBucketName = "";

var valid = new ConfigurationValidator(_logger.Object).Validate("", config);
var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessages = new[] { "Value for InformaticsGateway>storage>bucketName is required.", "Value for InformaticsGateway>storage>bucketName does not conform to Amazon S3 bucket naming requirements." };
Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage);
Expand All @@ -124,6 +128,26 @@ public void StorageWithInvalidBucketName()
}
}

[Fact(DisplayName = "ConfigurationValidator test with inaccessible directory")]
public void StorageWithInaccessbleDirectory()
{
_fileSystem.Setup(p => p.Directory.Exists(It.IsAny<string>())).Returns(true);
_fileSystem.Setup(p => p.File.Create(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<FileOptions>())).Throws(new UnauthorizedAccessException("error"));

var config = MockValidConfiguration();
config.Storage.TemporaryDataStorage = TemporaryDataStorageLocation.Disk;
config.Storage.BufferStorageRootPath = "/blabla";

var valid = new ConfigurationValidator(_logger.Object, _fileSystem.Object).Validate("", config);

var validationMessages = new[] { $"Directory `/blabla` specified in `InformaticsGateway>storage>bufferRootPath` is not accessible: error." };
Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage);
foreach (var message in validationMessages)
{
_logger.VerifyLogging(message, LogLevel.Error, Times.Once());
}
}

private static InformaticsGatewayConfiguration MockValidConfiguration()
{
var config = new InformaticsGatewayConfiguration();
Expand Down
1 change: 1 addition & 0 deletions src/Database/PayloadConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public void Configure(EntityTypeBuilder<Payload> builder)
builder.Ignore(j => j.CalledAeTitle);
builder.Ignore(j => j.CallingAeTitle);
builder.Ignore(j => j.HasTimedOut);
builder.Ignore(j => j.Elapsed);
builder.Ignore(j => j.Count);
}
}
Expand Down
75 changes: 63 additions & 12 deletions src/InformaticsGateway/Common/FileStorageMetadataExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,41 +14,92 @@
* limitations under the License.
*/

using System.IO;
using System.IO.Abstractions;
using System.Text;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using FellowOakDicom;
using Monai.Deploy.InformaticsGateway.Api.Storage;
using Monai.Deploy.InformaticsGateway.Configuration;

namespace Monai.Deploy.InformaticsGateway.Common
{
internal static class FileStorageMetadataExtensions
{
public static async Task SetDataStreams(this DicomFileStorageMetadata dicomFileStorageMetadata, DicomFile dicomFile, string dicomJson)
public static async Task SetDataStreams(
this DicomFileStorageMetadata dicomFileStorageMetadata,
DicomFile dicomFile,
string dicomJson,
TemporaryDataStorageLocation storageLocation,
IFileSystem fileSystem = null,
string temporaryStoragePath = "")
{
Guard.Against.Null(dicomFile, nameof(dicomFile));
Guard.Against.Null(dicomJson, nameof(dicomJson)); // allow empty here

dicomFileStorageMetadata.File.Data = new MemoryStream();
switch (storageLocation)
{
case TemporaryDataStorageLocation.Disk:
Guard.Against.Null(fileSystem, nameof(fileSystem));
Guard.Against.NullOrWhiteSpace(temporaryStoragePath, nameof(temporaryStoragePath));

var tempFile = fileSystem.Path.Combine(temporaryStoragePath, $@"{System.DateTime.UtcNow.Ticks}.tmp");
dicomFileStorageMetadata.File.Data = fileSystem.File.Create(tempFile);
break;
default:
dicomFileStorageMetadata.File.Data = new System.IO.MemoryStream();
break;
}

await dicomFile.SaveAsync(dicomFileStorageMetadata.File.Data).ConfigureAwait(false);
dicomFileStorageMetadata.File.Data.Seek(0, SeekOrigin.Begin);
dicomFileStorageMetadata.File.Data.Seek(0, System.IO.SeekOrigin.Begin);

SetTextStream(dicomFileStorageMetadata.JsonFile, dicomJson);
await SetTextStream(dicomFileStorageMetadata.JsonFile, dicomJson, storageLocation, fileSystem, temporaryStoragePath);
}

public static void SetDataStream(this FhirFileStorageMetadata fhirFileStorageMetadata, string json)
=> SetTextStream(fhirFileStorageMetadata.File, json);
public static async Task SetDataStream(
this FhirFileStorageMetadata fhirFileStorageMetadata,
string json,
TemporaryDataStorageLocation storageLocation,
IFileSystem fileSystem = null,
string temporaryStoragePath = "")
=> await SetTextStream(fhirFileStorageMetadata.File, json, storageLocation, fileSystem, temporaryStoragePath);

public static void SetDataStream(this Hl7FileStorageMetadata hl7FileStorageMetadata, string message)
=> SetTextStream(hl7FileStorageMetadata.File, message);
public static async Task SetDataStream(
this Hl7FileStorageMetadata hl7FileStorageMetadata,
string message,
TemporaryDataStorageLocation storageLocation,
IFileSystem fileSystem = null,
string temporaryStoragePath = "")
=> await SetTextStream(hl7FileStorageMetadata.File, message, storageLocation, fileSystem, temporaryStoragePath);

private static void SetTextStream(StorageObjectMetadata storageObjectMetadata, string message)
private static async Task SetTextStream(
StorageObjectMetadata storageObjectMetadata,
string message,
TemporaryDataStorageLocation storageLocation,
IFileSystem fileSystem = null,
string temporaryStoragePath = "")
{
Guard.Against.Null(message, nameof(message)); // allow empty here

storageObjectMetadata.Data = new MemoryStream(Encoding.UTF8.GetBytes(message));
storageObjectMetadata.Data.Seek(0, SeekOrigin.Begin);
switch (storageLocation)
{
case TemporaryDataStorageLocation.Disk:
Guard.Against.Null(fileSystem, nameof(fileSystem));
Guard.Against.NullOrWhiteSpace(temporaryStoragePath, nameof(temporaryStoragePath));

var tempFile = fileSystem.Path.Combine(temporaryStoragePath, $@"{System.DateTime.UtcNow.Ticks}.tmp");
var stream = fileSystem.File.Create(tempFile);
var data = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(data, 0, data.Length);
storageObjectMetadata.Data = stream;
break;
default:
storageObjectMetadata.Data = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(message));
break;
}

storageObjectMetadata.Data.Seek(0, System.IO.SeekOrigin.Begin);
}
}
}
Loading