Skip to content

Commit

Permalink
Merge pull request #5623 from Particular/startup-task-order
Browse files Browse the repository at this point in the history
Start FeatureStartupTasks in the feature dependency order
  • Loading branch information
danielmarbach committed Apr 20, 2020
2 parents 90dc26f + 420c29c commit 3759bb1
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 3 deletions.
@@ -0,0 +1,143 @@
namespace NServiceBus.AcceptanceTests.Core.Feature
{
using System;
using System.Threading.Tasks;
using AcceptanceTesting;
using EndpointTemplates;
using Features;
using NUnit.Framework;

public class When_depending_on_feature : NServiceBusAcceptanceTest
{
[Test]
public async Task Should_start_startup_tasks_in_order_of_dependency()
{
var context = await Scenario.Define<Context>()
.WithEndpoint<EndpointWithFeatures>(b => b.CustomConfig(c =>
{
c.EnableFeature<DependencyFeature>();
c.EnableFeature<TypedDependentFeature>();
}))
.Done(c => c.EndpointsStarted)
.Run();

Assert.That(context.StartCalled, Is.True);
Assert.That(context.StopCalled, Is.True);
}

class Context : ScenarioContext
{
public bool StartCalled { get; set; }
public bool StopCalled { get; set; }
public bool InitializeCalled { get; set; }
}

public class EndpointWithFeatures : EndpointConfigurationBuilder
{
public EndpointWithFeatures()
{
EndpointSetup<DefaultServer>();
}
}

public class TypedDependentFeature : Feature
{
public TypedDependentFeature()
{
DependsOn<DependencyFeature>();
}

protected override void Setup(FeatureConfigurationContext context)
{
context.Container.ConfigureComponent<Runner>(DependencyLifecycle.SingleInstance);
context.RegisterStartupTask(b => b.Build<Runner>());
}

class Runner : FeatureStartupTask
{
Dependency dependency;

public Runner(Dependency dependency)
{
this.dependency = dependency;
}
protected override Task OnStart(IMessageSession session)
{
dependency.Start();
return Task.FromResult(0);
}

protected override Task OnStop(IMessageSession session)
{
dependency.Stop();
return Task.FromResult(0);
}
}
}

public class DependencyFeature : Feature
{
protected override void Setup(FeatureConfigurationContext context)
{
context.Container.ConfigureComponent<Dependency>(DependencyLifecycle.SingleInstance);

context.Container.ConfigureComponent<Runner>(DependencyLifecycle.SingleInstance);
context.RegisterStartupTask(b => b.Build<Runner>());
}

class Runner : FeatureStartupTask
{
Dependency dependency;

public Runner(Dependency dependency)
{
this.dependency = dependency;
}
protected override Task OnStart(IMessageSession session)
{
dependency.Initialize();
return Task.FromResult(0);
}

protected override Task OnStop(IMessageSession session)
{
return Task.FromResult(0);
}
}
}

class Dependency
{
Context context;

public Dependency(Context context)
{
this.context = context;
}

public void Start()
{
if (!context.InitializeCalled)
{
throw new InvalidOperationException("Not initialized");
}
context.StartCalled = true;
}

public void Stop()
{
if (!context.InitializeCalled)
{
throw new InvalidOperationException("Not initialized");
}

context.StopCalled = true;
}

public void Initialize()
{
context.InitializeCalled = true;
}
}
}
}
102 changes: 102 additions & 0 deletions src/NServiceBus.Core.Tests/Features/FeatureStartupTests.cs
Expand Up @@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using NServiceBus.Features;
using NUnit.Framework;
Expand Down Expand Up @@ -36,6 +37,28 @@ public async Task Should_start_and_stop_features()
Assert.True(feature.TaskStopped);
}

[Test]
public async Task Should_start_and_stop_features_in_dependency_order()
{
var orderBuilder = new StringBuilder();

featureSettings.Add(new FeatureWithStartupTaskWithDependency(orderBuilder));
featureSettings.Add(new FeatureWithStartupThatAnotherFeatureDependsOn(orderBuilder));

featureSettings.SetupFeatures(null, null, null);

await featureSettings.StartFeatures(null, null);
await featureSettings.StopFeatures(null);

var expectedOrderBuilder = new StringBuilder();
expectedOrderBuilder.AppendLine("FeatureWithStartupThatAnotherFeatureDependsOn.Start");
expectedOrderBuilder.AppendLine("FeatureWithStartupTaskWithDependency.Start");
expectedOrderBuilder.AppendLine("FeatureWithStartupThatAnotherFeatureDependsOn.Stop");
expectedOrderBuilder.AppendLine("FeatureWithStartupTaskWithDependency.Stop");

Assert.AreEqual(expectedOrderBuilder.ToString(), orderBuilder.ToString());
}

[Test]
public async Task Should_dispose_feature_startup_tasks_when_they_implement_IDisposable()
{
Expand Down Expand Up @@ -101,6 +124,85 @@ public async Task Should_dispose_feature_task_even_when_stop_throws()
private FeatureActivator featureSettings;
private SettingsHolder settings;

class FeatureWithStartupTaskWithDependency : TestFeature
{
public FeatureWithStartupTaskWithDependency(StringBuilder orderBuilder)
{
EnableByDefault();
DependsOn<FeatureWithStartupThatAnotherFeatureDependsOn>();

this.orderBuilder = orderBuilder;
}

protected internal override void Setup(FeatureConfigurationContext context)
{
context.RegisterStartupTask(new Runner(orderBuilder));
}

class Runner : FeatureStartupTask
{
public Runner(StringBuilder orderBuilder)
{
this.orderBuilder = orderBuilder;
}

protected override Task OnStart(IMessageSession session)
{
orderBuilder.AppendLine($"{nameof(FeatureWithStartupTaskWithDependency)}.Start");
return TaskEx.CompletedTask;
}

protected override Task OnStop(IMessageSession session)
{
orderBuilder.AppendLine($"{nameof(FeatureWithStartupTaskWithDependency)}.Stop");
return TaskEx.CompletedTask;
}

StringBuilder orderBuilder;
}

readonly StringBuilder orderBuilder;
}

class FeatureWithStartupThatAnotherFeatureDependsOn : TestFeature
{
public FeatureWithStartupThatAnotherFeatureDependsOn(StringBuilder orderBuilder)
{
EnableByDefault();

this.orderBuilder = orderBuilder;
}

protected internal override void Setup(FeatureConfigurationContext context)
{
context.RegisterStartupTask(new Runner(orderBuilder));
}

class Runner : FeatureStartupTask
{
public Runner(StringBuilder orderBuilder)
{
this.orderBuilder = orderBuilder;
}

protected override Task OnStart(IMessageSession session)
{
orderBuilder.AppendLine($"{nameof(FeatureWithStartupThatAnotherFeatureDependsOn)}.Start");
return TaskEx.CompletedTask;
}

protected override Task OnStop(IMessageSession session)
{
orderBuilder.AppendLine($"{nameof(FeatureWithStartupThatAnotherFeatureDependsOn)}.Stop");
return TaskEx.CompletedTask;
}

StringBuilder orderBuilder;
}

readonly StringBuilder orderBuilder;
}

class FeatureWithStartupTask : TestFeature
{
public FeatureWithStartupTask()
Expand Down
7 changes: 4 additions & 3 deletions src/NServiceBus.Core/Features/FeatureActivator.cs
Expand Up @@ -41,7 +41,6 @@ public FeaturesReport SetupFeatures(IConfigureComponents container, PipelineSett
// featuresToActivate is enumerated twice because after setting defaults some new features might got activated.
var sourceFeatures = Sort(features);

var enabledFeatures = new List<FeatureInfo>();
while (true)
{
var featureToActivate = sourceFeatures.FirstOrDefault(x => settings.IsFeatureEnabled(x.Feature.GetType()));
Expand All @@ -66,7 +65,8 @@ public FeaturesReport SetupFeatures(IConfigureComponents container, PipelineSett

public async Task StartFeatures(IBuilder builder, IMessageSession session)
{
foreach (var feature in features.Where(f => f.Feature.IsActive))
// sequential starting of startup tasks is intended, introducing concurrency here could break a lot of features.
foreach (var feature in enabledFeatures.Where(f => f.Feature.IsActive))
{
foreach (var taskController in feature.TaskControllers)
{
Expand All @@ -77,7 +77,7 @@ public async Task StartFeatures(IBuilder builder, IMessageSession session)

public Task StopFeatures(IMessageSession session)
{
var featureStopTasks = features.Where(f => f.Feature.IsActive)
var featureStopTasks = enabledFeatures.Where(f => f.Feature.IsActive)
.SelectMany(f => f.TaskControllers)
.Select(task => task.Stop(session));

Expand Down Expand Up @@ -211,6 +211,7 @@ static bool HasAllPrerequisitesSatisfied(Feature feature, FeatureDiagnosticData
}

List<FeatureInfo> features = new List<FeatureInfo>();
List<FeatureInfo> enabledFeatures = new List<FeatureInfo>();
SettingsHolder settings;

class FeatureInfo
Expand Down

0 comments on commit 3759bb1

Please sign in to comment.