Skip to content

Commit

Permalink
Merge branch 'stu3/master' into r4/master
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethmyhra committed Aug 13, 2020
2 parents b012248 + 46da0ca commit 0d0d668
Show file tree
Hide file tree
Showing 32 changed files with 904 additions and 75 deletions.
8 changes: 6 additions & 2 deletions .docker/docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
spark:
container_name: spark
restart: always
image: sparkfhir/spark:r4-latest-develop
image: sparkfhir/spark:r4-latest
environment:
- StoreSettings__ConnectionString=mongodb://root:CosmicTopSecret@mongodb:27017/spark?authSource=admin
- SparkSettings__Endpoint=http://localhost:5555/fhir
Expand All @@ -14,9 +14,13 @@ services:
- mongodb
mongodb:
container_name: mongodb
image: sparkfhir/mongo:r4-latest-develop
image: sparkfhir/mongo:r4-latest
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: CosmicTopSecret
ports:
- "17017:27017"
volumes:
- r4-latest-develop-data-volume:/data/db
volumes:
r4-latest-develop-data-volume:
133 changes: 133 additions & 0 deletions src/Spark.Engine.Test/Maintenance/MaintenanceModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Spark.Engine.Maintenance;
using System.Linq;
using System.Net;
using Xunit;

namespace Spark.Engine.Test.Maintenance
{
public class MaintenanceModeTests
{
[Theory]
[MemberData(nameof(AllLockModes))]
internal void IsEnabled_Should_Return_False_If_Maintenance_Mode_Is_Not_Enabled(MaintenanceLockMode mode)
{
Assert.False(MaintenanceMode.IsEnabled(mode));
}

[Theory]
[MemberData(nameof(AllLockModes))]
internal void IsEnabled_Should_Not_Lock_If_Lock_None_Is_Provided(MaintenanceLockMode mode)
{
using (MaintenanceMode.Enable(MaintenanceLockMode.None))
{
Assert.False(MaintenanceMode.IsEnabled(mode));
}
}

[Fact]
internal void IsEnabled_Should_Not_Lock_Read_If_Write_Only_Mock_Mode_Is_Provided()
{
using (MaintenanceMode.Enable(MaintenanceLockMode.Write))
{
Assert.True(MaintenanceMode.IsEnabled(MaintenanceLockMode.None));
Assert.True(MaintenanceMode.IsEnabled(MaintenanceLockMode.Write));
Assert.False(MaintenanceMode.IsEnabled(MaintenanceLockMode.Full));
}
}

[Fact]
internal void IsEnabled_Full_Lock_Mode_Should_Lock_Read_And_Write()
{
using (MaintenanceMode.Enable(MaintenanceLockMode.Full))
{
Assert.True(MaintenanceMode.IsEnabled(MaintenanceLockMode.None));
Assert.True(MaintenanceMode.IsEnabled(MaintenanceLockMode.Write));
Assert.True(MaintenanceMode.IsEnabled(MaintenanceLockMode.Full));
}
}

[Theory]
[MemberData(nameof(ActualLockModes))]
internal void Should_Not_Allow_To_Lock_Twice(MaintenanceLockMode mode)
{
using (MaintenanceMode.Enable(mode))
{
var e = Assert.Throws<MaintenanceModeEnabledException>(() => MaintenanceMode.Enable(mode));
Assert.Equal(HttpStatusCode.ServiceUnavailable, e.StatusCode);
}
}

[Theory]
[MemberData(nameof(AllHttpMethods))]
internal void IsEnabledForHttpMethod_Should_Return_False_If_Maintenance_Mode_Is_Not_Enabled(string method)
{
Assert.False(MaintenanceMode.IsEnabledForHttpMethod(method));
}

[Theory]
[MemberData(nameof(ReadHttpMethods))]
internal void IsEnabledForHttpMethod_Should_Return_False_For_Read_Methods_Write_Only_Lock(string method)
{
using (MaintenanceMode.Enable(MaintenanceLockMode.Write))
{
Assert.False(MaintenanceMode.IsEnabledForHttpMethod(method));
}
}

[Theory]
[MemberData(nameof(ReadHttpMethods))]
internal void IsEnabledForHttpMethod_Should_Return_True_For_Read_Methods_Full_Lock(string method)
{
using (MaintenanceMode.Enable(MaintenanceLockMode.Full))
{
Assert.True(MaintenanceMode.IsEnabledForHttpMethod(method));
}
}

[Theory]
[MemberData(nameof(WriteHttpMethods))]
internal void IsEnabledForHttpMethod_Should_Return_True_For_Update_Methods_Write_Only_Lock(string method)
{
using (MaintenanceMode.Enable(MaintenanceLockMode.Write))
{
Assert.True(MaintenanceMode.IsEnabledForHttpMethod(method));
}
}

[Theory]
[MemberData(nameof(WriteHttpMethods))]
internal void IsEnabledForHttpMethod_Should_Return_True_For_Update_Methods_Full_Lock(string method)
{
using (MaintenanceMode.Enable(MaintenanceLockMode.Full))
{
Assert.True(MaintenanceMode.IsEnabledForHttpMethod(method));
}
}

public static object[][] ReadHttpMethods => new[]
{
new object[] {"GET"},
new object[] {"HEAD"},
new object[] {"OPTIONS"}
};

public static object[][] WriteHttpMethods => new[]
{
new object[] {"POST"},
new object[] {"PUT"},
new object[] {"PATCH"},
new object[] {"DELETE"}
};

public static object[][] AllHttpMethods => ReadHttpMethods.Concat(WriteHttpMethods).ToArray();

public static object[][] AllLockModes => ((MaintenanceLockMode[])typeof(MaintenanceLockMode).GetEnumValues())
.Select(v => new object[] { v })
.ToArray();

public static object[][] ActualLockModes => ((MaintenanceLockMode[])typeof(MaintenanceLockMode).GetEnumValues())
.Where(m => m != MaintenanceLockMode.None)
.Select(v => new object[] { v })
.ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
using System.Web.Http;
using System.Web.Http.Validation;
using System.Net.Http.Formatting;
using System.Web.Http. ExceptionHandling;
using System.Web.Http.ExceptionHandling;
using Spark.Filters;
using Spark.Handlers;
using Spark.Formatters;
using Spark.Engine.Maintenance;
using Spark.Core;
using Spark.Engine.ExceptionHandling;
using Hl7.Fhir.Model;
Expand Down Expand Up @@ -48,6 +49,7 @@ public static void AddFhirMessageHandlers(this HttpConfiguration config)
config.MessageHandlers.Add(new FhirMediaTypeHandler());
config.MessageHandlers.Add(new FhirResponseHandler());
config.MessageHandlers.Add(new FhirErrorMessageHandler());
config.MessageHandlers.Add(new MaintenanceModeNetHttpHandler());
}

public static void AddCustomSearchParameters(this HttpConfiguration configuration, IEnumerable<SearchParamDefinition> searchParameters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Spark.Engine.ExceptionHandling;
using Spark.Engine.Handlers.NetCore;
using System;
using Spark.Engine.Maintenance;

namespace Spark.Engine.Extensions
{
Expand All @@ -13,6 +14,7 @@ public static void UseFhir(this IApplicationBuilder app, Action<IRouteBuilder> c
{
app.UseMiddleware<ErrorHandler>();
app.UseMiddleware<FormatTypeHandler>();
app.UseMiddleware<MaintenanceModeHandler>();

if (configureRoutes == null)
app.UseMvc();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public static IMvcCoreBuilder AddFhir(this IServiceCollection services, SparkSet
services.TryAddTransient((provider) => new IFhirResponseInterceptor[] { provider.GetRequiredService<ConditionalHeaderFhirResponseInterceptor>() });
services.TryAddTransient<IFhirResponseInterceptorRunner, FhirResponseInterceptorRunner>();
services.TryAddTransient<IFhirResponseFactory, FhirResponseFactory.FhirResponseFactory>();
services.TryAddTransient<IIndexRebuildService, IndexRebuildService>();
services.TryAddTransient<ISearchService, SearchService>();
services.TryAddTransient<ISnapshotPaginationProvider, SnapshotPaginationProvider>();
services.TryAddTransient<ISnapshotPaginationCalculator, SnapshotPaginationCalculator>();
Expand Down
26 changes: 26 additions & 0 deletions src/Spark.Engine/Maintenance/MaintenanceLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Spark.Engine.Maintenance
{
internal class MaintenanceLock : IDisposable
{
public MaintenanceLockMode Mode { get; private set; }

public bool IsLocked => Mode > MaintenanceLockMode.None;

public MaintenanceLock(MaintenanceLockMode mode)
{
Mode = mode;
}

public void Unlock()
{
Mode = MaintenanceLockMode.None;
}

public void Dispose()
{
Unlock();
}
}
}
9 changes: 9 additions & 0 deletions src/Spark.Engine/Maintenance/MaintenanceLockMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Spark.Engine.Maintenance
{
internal enum MaintenanceLockMode
{
None = 0,
Write = 1,
Full = 2
}
}
58 changes: 58 additions & 0 deletions src/Spark.Engine/Maintenance/MaintenanceMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;

namespace Spark.Engine.Maintenance
{
internal static class MaintenanceMode
{
private static readonly object _mutex = new object();
private static volatile MaintenanceLock _lock;

/// <summary>
/// Whether maintenance mode is enabled. If <code>true</code>
/// then all the data modifying requests should be responded
/// with <code>503</code> HTTP status code.
/// </summary>
public static bool IsEnabled(MaintenanceLockMode mode) => _lock?.IsLocked == true && _lock.Mode >= mode;

/// <summary>
/// Sets maintenance mode ON. The returned lock handle should be used
/// to reset the maintenance state.
/// </summary>
/// <param name="mode">Lock mode, write only, or read and write</param>
/// <exception cref="MaintenanceModeEnabledException">Maintenance mode already enabled somewhere else.</exception>
public static MaintenanceLock Enable(MaintenanceLockMode mode)
{
if (_lock?.IsLocked != true)
{
lock (_mutex)
{
if (_lock?.IsLocked != true)
{
_lock = new MaintenanceLock(mode);
return _lock;
}
}
}
throw new MaintenanceModeEnabledException();
}

public static bool IsEnabledForHttpMethod(string method)
{
if (string.IsNullOrWhiteSpace(method))
{
throw new ArgumentException(nameof(method));
}

switch (method.ToUpper())
{
case "GET":
case "HEAD":
case "OPTIONS":
return IsEnabled(MaintenanceLockMode.Full);

default: // PUT, POST, PATCH, DELETE ETC
return IsEnabled(MaintenanceLockMode.Write);
}
}
}
}
12 changes: 12 additions & 0 deletions src/Spark.Engine/Maintenance/MaintenanceModeEnabledException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Spark.Engine.Core;
using System.Net;

namespace Spark.Engine.Maintenance
{
internal class MaintenanceModeEnabledException : SparkException
{
public MaintenanceModeEnabledException() : base(HttpStatusCode.ServiceUnavailable)
{
}
}
}
27 changes: 27 additions & 0 deletions src/Spark.Engine/Maintenance/MaintenanceModeHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if NETSTANDARD2_0
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Spark.Engine.Maintenance
{
public class MaintenanceModeHandler
{
private readonly RequestDelegate _next;

public MaintenanceModeHandler(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
if (MaintenanceMode.IsEnabledForHttpMethod(context.Request.Method))
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
return;
}
await _next(context);
}
}
}
#endif
21 changes: 21 additions & 0 deletions src/Spark.Engine/Maintenance/MaintenanceModeNetHttpHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#if NET461
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Spark.Engine.Maintenance
{
public class MaintenanceModeNetHttpHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (MaintenanceMode.IsEnabledForHttpMethod(request.Method.Method))
{
return request.CreateResponse(HttpStatusCode.ServiceUnavailable);
}
return await base.SendAsync(request, cancellationToken);
}
}
}
#endif
11 changes: 11 additions & 0 deletions src/Spark.Engine/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Spark.Engine.Test")]

namespace Spark.Engine.Properties
{

internal class AssemblyInfo
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;

namespace Spark.Engine.Service.FhirServiceExtensions
{
public interface IIndexBuildProgressReporter
{
Task ReportProgressAsync(int progress, string message);

Task ReportErrorAsync(string message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading.Tasks;

namespace Spark.Engine.Service.FhirServiceExtensions
{
public interface IIndexRebuildService
{
Task RebuildIndexAsync(IIndexBuildProgressReporter reporter = null);
}
}
Loading

0 comments on commit 0d0d668

Please sign in to comment.