Skip to content

Commit 9e98a79

Browse files
committed
gh-188 Stop accepting DICOMweb, HL7 & FHIR when disk space is low
Signed-off-by: Victor Chang <[email protected]>
1 parent 126ee70 commit 9e98a79

23 files changed

+182
-31
lines changed

docs/api/rest/dicomweb-stow.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ Response Content Type: `JSON`
5656
| 202 | [DicomDataset](https://github.com/fo-dicom/fo-dicom/blob/development/FO-DICOM.Core/DicomDataset.cs) | All instances are received and stored with warnings (e.g. for a mismatched StudyInstanceUID. |
5757
| 204 | `none` | No data is provided. |
5858
| 400 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Request contains invalid values. |
59-
| 415 | `none` | Unsupported media type |
60-
| 500 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Server error |
59+
| 415 | `none` | Unsupported media typ. |
60+
| 500 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Server error. |
61+
| 507 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Insufficient storage. |
6162

6263
---
6364

@@ -97,3 +98,4 @@ Response Content Type: `JSON`
9798
| 400 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Request contains invalid values. |
9899
| 415 | `none` | Unsupported media type |
99100
| 500 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Server error |
101+
| 507 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807) | Insufficient storage. |

docs/api/rest/fhir.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ Depending on the `Accept` header or the original document, the response supports
5555

5656
If the `Accept` header is missing or a none supported value exists, the service will return the same type as the posted document.
5757

58-
| Code | Data Type | Description |
59-
| ---- | ------------------------------------------------------------- | --------------------------------------------------------------------- |
60-
| 201 | Original JSON or XML document. | Resource created & stored successfully. |
61-
| 400 | [OperationOutcome](http://hl7.org/fhir/operationoutcome.html) | Unable to parse the resource or mismatching resource type specified.. |
62-
| 415 | `none` | Unsupported media type |
63-
| 500 | [OperationOutcome](http://hl7.org/fhir/operationoutcome.html) | Server error. |
58+
| Code | Data Type | Description |
59+
| ---- | --------------------------------------------------------------- | --------------------------------------------------------------------- |
60+
| 201 | Original JSON or XML document. | Resource created & stored successfully. |
61+
| 400 | [OperationOutcome](http://hl7.org/fhir/operationoutcome.html) | Unable to parse the resource or mismatching resource type specified.. |
62+
| 415 | `none` | Unsupported media type |
63+
| 500 | [OperationOutcome](http://hl7.org/fhir/operationoutcome.html) | Server error. |
64+
| 507 | [Problem details](https://datatracker.ietf.org/doc/html/rfc7807)| Insufficient storage. |

src/Configuration/Test/ConfigurationValidatorTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public void StorageWithInaccessbleDirectory()
141141

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

144-
var validationMessages = new[] { $"Directory `/blabla` specified in `InformaticsGateway>storage>bufferRootPath` is not accessible: error." };
144+
var validationMessages = new[] { $"Directory `/blabla` specified in `InformaticsGateway>storage>localTemporaryStoragePath` is not accessible: error." };
145145
Assert.Equal(string.Join(Environment.NewLine, validationMessages), valid.FailureMessage);
146146
foreach (var message in validationMessages)
147147
{

src/DicomWebClient/API/IDicomWebClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ public interface IDicomWebClient
7474
/// </summary>
7575
/// <param name="serviceType"><c>ServiceType</c> to be configured</param>
7676
/// <param name="urlPrefix">Url prefix</param>
77-
#pragma warning disable CA1054
77+
#pragma warning disable CA1054
7878
void ConfigureServicePrefix(DicomWebServiceType serviceType, string urlPrefix);
79-
#pragma warning restore CA1054
79+
#pragma warning restore CA1054
8080
/// <summary>
8181
/// Configures the authentication header for the DICOMweb client.
8282
/// </summary>

src/DicomWebClient/API/IServiceBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ namespace Monai.Deploy.InformaticsGateway.DicomWeb.Client.API
1919
{
2020
public interface IServiceBase
2121
{
22-
#pragma warning disable CA1054
22+
#pragma warning disable CA1054
2323
bool TryConfigureServiceUriPrefix(string uriPrefix);
24-
#pragma warning restore CA1054
24+
#pragma warning restore CA1054
2525
}
2626
}

src/DicomWebClient/DicomWebClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ public void ConfigureServiceUris(Uri uriRoot)
7878
}
7979

8080
/// <inheritdoc/>
81-
#pragma warning disable CA1054
81+
#pragma warning disable CA1054
8282
public void ConfigureServicePrefix(DicomWebServiceType serviceType, string urlPrefix)
83-
#pragma warning restore CA1054
83+
#pragma warning restore CA1054
8484
{
8585
Guard.Against.NullOrWhiteSpace(urlPrefix, nameof(urlPrefix));
8686

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Microsoft.Extensions.Logging;
18+
19+
namespace Monai.Deploy.InformaticsGateway.Logging
20+
{
21+
public static partial class Log
22+
{
23+
// StorageInfoProvider
24+
[LoggerMessage(EventId = 300, Level = LogLevel.Information, Message = "Temporary Storage Path={path}. Storage Size: {totalSize:N0}. Reserved: {reservedSpace:N0}.")]
25+
public static partial void StorageInfoProviderStartup(this ILogger logger, string path, long totalSize, long reservedSpace);
26+
27+
[LoggerMessage(EventId = 301, Level = LogLevel.Information, Message = "Storage Size: {totalSize:N0}. Reserved: {reservedSpace:N0}. Available: {freeSpace:N0}.")]
28+
public static partial void CurrentStorageSize(this ILogger logger, long totalSize, long reservedSpace, long freeSpace);
29+
}
30+
}

src/InformaticsGateway/Logging/Log.800.Hl7Service.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,8 @@ public static partial class Log
6262

6363
[LoggerMessage(EventId = 813, Level = LogLevel.Debug, Message = "Waiting for HL7 message.")]
6464
public static partial void HL7ReadingMessage(this ILogger logger);
65+
66+
[LoggerMessage(EventId = 814, Level = LogLevel.Warning, Message = "HL7 service paused due to insufficient storage space. Available storage space: {availableFreeSpace:D}.")]
67+
public static partial void Hl7DisconnectedDueToLowStorageSpace(this ILogger logger, long availableFreeSpace);
6568
}
6669
}

src/InformaticsGateway/Logging/Log.900.ScuService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* limitations under the License.
1515
*/
1616

17-
using System;
1817
using Microsoft.Extensions.Logging;
1918

2019
namespace Monai.Deploy.InformaticsGateway.Logging

src/InformaticsGateway/Services/Common/ITcpClientAdapter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ internal interface ITcpClientAdapter : IDisposable
2424
EndPoint RemoteEndPoint { get; }
2525

2626
INetworkStream GetStream();
27+
28+
void Close();
2729
}
2830
}

src/InformaticsGateway/Services/Common/TcpClientAdapter.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@ public EndPoint RemoteEndPoint
3535
public TcpClientAdapter(System.Net.Sockets.TcpClient tcpClient)
3636
=> _tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient));
3737

38-
public INetworkStream GetStream()
39-
{
40-
return new NetworkStreamAdapter(_tcpClient.GetStream());
41-
}
38+
public INetworkStream GetStream() => new NetworkStreamAdapter(_tcpClient.GetStream());
39+
40+
public void Close() => _tcpClient.Close();
4241

4342
protected virtual void Dispose(bool disposing)
4443
{

src/InformaticsGateway/Services/Connectors/DataRetrievalService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ public DataRetrievalService(
8282
_dicomToolkit = _rootScope.ServiceProvider.GetService<IDicomToolkit>() ?? throw new ServiceNotFoundException(nameof(IDicomToolkit));
8383
_fileSystem = _rootScope.ServiceProvider.GetService<IFileSystem>() ?? throw new ServiceNotFoundException(nameof(IFileSystem));
8484
_storageInfoProvider = _rootScope.ServiceProvider.GetService<IStorageInfoProvider>() ?? throw new ServiceNotFoundException(nameof(IStorageInfoProvider));
85-
8685
}
8786

8887
public Task StartAsync(CancellationToken cancellationToken)

src/InformaticsGateway/Services/HealthLevel7/MllpClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ private void Dispose(bool disposing)
226226
{
227227
if (disposing)
228228
{
229+
_client.Close();
229230
_client.Dispose();
230231
_loggerScope.Dispose();
231232
}

src/InformaticsGateway/Services/HealthLevel7/MllpService.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal sealed class MllpService : IHostedService, IDisposable, IMonaiService
4949
private readonly ILoggerFactory _logginFactory;
5050
private readonly ILogger<MllpService> _logger;
5151
private readonly IOptions<InformaticsGatewayConfiguration> _configuration;
52+
private readonly IStorageInfoProvider _storageInfoProvider;
5253
private readonly ConcurrentDictionary<Guid, IMllpClient> _activeTasks;
5354

5455
public int ActiveConnections
@@ -82,6 +83,7 @@ public MllpService(IServiceScopeFactory serviceScopeFactory,
8283
_uploadQueue = _serviceScope.ServiceProvider.GetService<IObjectUploadQueue>() ?? throw new ServiceNotFoundException(nameof(IObjectUploadQueue));
8384
_payloadAssembler = _serviceScope.ServiceProvider.GetService<IPayloadAssembler>() ?? throw new ServiceNotFoundException(nameof(IPayloadAssembler));
8485
_fileSystem = _serviceScope.ServiceProvider.GetService<IFileSystem>() ?? throw new ServiceNotFoundException(nameof(IFileSystem));
86+
_storageInfoProvider = _serviceScope.ServiceProvider.GetService<IStorageInfoProvider>() ?? throw new ServiceNotFoundException(nameof(IStorageInfoProvider));
8587
_activeTasks = new ConcurrentDictionary<Guid, IMllpClient>();
8688
}
8789

@@ -121,6 +123,14 @@ private async Task BackgroundProcessing(CancellationToken cancellationToken)
121123
var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
122124
_logger.ClientConnected();
123125

126+
if (!_storageInfoProvider.HasSpaceAvailableToStore)
127+
{
128+
_logger.Hl7DisconnectedDueToLowStorageSpace(_storageInfoProvider.AvailableFreeSpace);
129+
client.Close();
130+
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
131+
continue;
132+
}
133+
124134
mllpClient = _mllpClientFactory.CreateClient(client, _configuration.Value.Hl7, _logginFactory.CreateLogger<MllpClient>());
125135
_ = mllpClient.Start(OnDisconnect, cancellationToken);
126136
_activeTasks.TryAdd(mllpClient.ClientId, mllpClient);
@@ -170,6 +180,10 @@ private async Task OnDisconnect(IMllpClient client, MllpClientResult result)
170180
{
171181
_logger.ErrorHandlingHl7Results(ex);
172182
}
183+
finally
184+
{
185+
client.Dispose();
186+
}
173187
}
174188

175189
private void WaitUntilAvailable(int maximumNumberOfConnections)

src/InformaticsGateway/Services/Http/DicomWeb/StowController.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using Monai.Deploy.InformaticsGateway.Common;
2626
using Monai.Deploy.InformaticsGateway.Logging;
2727
using Monai.Deploy.InformaticsGateway.Services.DicomWeb;
28+
using Monai.Deploy.InformaticsGateway.Services.Storage;
2829

2930
namespace Monai.Deploy.InformaticsGateway.Services.Http.DicomWeb
3031
{
@@ -34,6 +35,7 @@ public class StowController : ControllerBase
3435
{
3536
private readonly IStowService _stowService;
3637
private readonly ILogger<StowController> _logger;
38+
private readonly IStorageInfoProvider _storageInfoProvider;
3739

3840
public StowController(IServiceScopeFactory serviceScopeFactory)
3941
{
@@ -45,6 +47,7 @@ public StowController(IServiceScopeFactory serviceScopeFactory)
4547

4648
_stowService = scope.ServiceProvider.GetService<IStowService>() ?? throw new ServiceNotFoundException(nameof(IStowService));
4749
_logger = scope.ServiceProvider.GetService<ILogger<StowController>>() ?? throw new ServiceNotFoundException(nameof(ILogger<StowController>));
50+
_storageInfoProvider = scope.ServiceProvider.GetService<IStorageInfoProvider>() ?? throw new ServiceNotFoundException(nameof(IStorageInfoProvider));
4851
}
4952

5053
[HttpPost("studies")]
@@ -58,6 +61,7 @@ public StowController(IServiceScopeFactory serviceScopeFactory)
5861
[ProducesResponseType(typeof(DicomDataset), StatusCodes.Status409Conflict)]
5962
[ProducesResponseType(typeof(string), StatusCodes.Status415UnsupportedMediaType)]
6063
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
64+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status507InsufficientStorage)]
6165
public async Task<IActionResult> StoreInstances(string workflowName = "")
6266
{
6367
return await StoreInstances(string.Empty, workflowName).ConfigureAwait(false);
@@ -74,6 +78,7 @@ public async Task<IActionResult> StoreInstances(string workflowName = "")
7478
[ProducesResponseType(typeof(DicomDataset), StatusCodes.Status409Conflict)]
7579
[ProducesResponseType(typeof(string), StatusCodes.Status415UnsupportedMediaType)]
7680
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
81+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status507InsufficientStorage)]
7782
public async Task<IActionResult> StoreInstancesToStudy(string studyInstanceUid, string workflowName = "")
7883
{
7984
return await StoreInstances(studyInstanceUid, workflowName).ConfigureAwait(false);
@@ -84,6 +89,13 @@ private async Task<IActionResult> StoreInstances(string studyInstanceUid, string
8489
var correlationId = Guid.NewGuid().ToString();
8590
using var logger = _logger.BeginScope(new LoggingDataDictionary<string, object> { { "CorrelationId", correlationId }, { "StudyInstanceUID", studyInstanceUid }, { "Workflow", workflowName } });
8691

92+
if (!_storageInfoProvider.HasSpaceAvailableToStore)
93+
{
94+
return StatusCode(
95+
StatusCodes.Status507InsufficientStorage,
96+
Problem(title: $"Insufficient Storage", statusCode: StatusCodes.Status507InsufficientStorage));
97+
}
98+
8799
try
88100
{
89101
var result = await _stowService.StoreAsync(Request, studyInstanceUid, workflowName, correlationId, HttpContext.RequestAborted).ConfigureAwait(false);

src/InformaticsGateway/Services/Http/Fhir/FhirController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using Monai.Deploy.InformaticsGateway.Common;
2525
using Monai.Deploy.InformaticsGateway.Logging;
2626
using Monai.Deploy.InformaticsGateway.Services.Fhir;
27+
using Monai.Deploy.InformaticsGateway.Services.Storage;
2728

2829
namespace Monai.Deploy.InformaticsGateway.Services.Http.Fhir
2930
{
@@ -34,6 +35,7 @@ public class FhirController : ControllerBase, IDisposable
3435
private readonly IServiceScope _scope;
3536
private readonly IFhirService _fhirService;
3637
private readonly ILogger<FhirController> _logger;
38+
private readonly IStorageInfoProvider _storageInfoProvider;
3739
private bool _disposedValue;
3840

3941
public FhirController(IServiceScopeFactory serviceScopeFactory)
@@ -46,13 +48,15 @@ public FhirController(IServiceScopeFactory serviceScopeFactory)
4648
_scope = serviceScopeFactory.CreateScope();
4749
_fhirService = _scope.ServiceProvider.GetService<IFhirService>() ?? throw new ServiceNotFoundException(nameof(IFhirService));
4850
_logger = _scope.ServiceProvider.GetService<ILogger<FhirController>>() ?? throw new ServiceNotFoundException(nameof(ILogger<FhirController>));
51+
_storageInfoProvider = _scope.ServiceProvider.GetService<IStorageInfoProvider>() ?? throw new ServiceNotFoundException(nameof(IStorageInfoProvider));
4952
}
5053

5154
[HttpPost]
5255
[Consumes(ContentTypes.ApplicationFhirJson, ContentTypes.ApplicationFhirXml)]
5356
[ProducesResponseType(StatusCodes.Status200OK)]
5457
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status400BadRequest)]
5558
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
59+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status507InsufficientStorage)]
5660
[Produces(ContentTypes.ApplicationFhirJson, ContentTypes.ApplicationFhirXml)]
5761
public async Task<IActionResult> Create()
5862
{
@@ -65,12 +69,19 @@ public async Task<IActionResult> Create()
6569
[ProducesResponseType(StatusCodes.Status201Created)]
6670
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status400BadRequest)]
6771
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status500InternalServerError)]
72+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status507InsufficientStorage)]
6873
[Produces(ContentTypes.ApplicationFhirJson, ContentTypes.ApplicationFhirXml)]
6974
public async Task<IActionResult> Create(string resourceType)
7075
{
7176
var correlationId = Guid.NewGuid().ToString();
7277
using var logger = _logger.BeginScope(new LoggingDataDictionary<string, object> { { "CorrelationId", correlationId } });
7378

79+
if (!_storageInfoProvider.HasSpaceAvailableToStore)
80+
{
81+
return StatusCode(
82+
StatusCodes.Status507InsufficientStorage,
83+
Problem(title: $"Insufficient Storage", statusCode: StatusCodes.Status507InsufficientStorage));
84+
}
7485
try
7586
{
7687
var result = await _fhirService.StoreAsync(Request, correlationId, resourceType, HttpContext.RequestAborted).ConfigureAwait(false);

0 commit comments

Comments
 (0)