Skip to content

Commit c288697

Browse files
Merge pull request #5622 from Particular/bp-startuptask-order
Start FeatureStartupTasks in the feature dependency order
2 parents b7c9bf5 + 6d37e60 commit c288697

File tree

3 files changed

+249
-3
lines changed

3 files changed

+249
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
namespace NServiceBus.AcceptanceTests.Core.Feature
2+
{
3+
using System;
4+
using System.Threading.Tasks;
5+
using AcceptanceTesting;
6+
using EndpointTemplates;
7+
using Features;
8+
using NUnit.Framework;
9+
10+
public class When_depending_on_feature : NServiceBusAcceptanceTest
11+
{
12+
[Test]
13+
public async Task Should_start_startup_tasks_in_order_of_dependency()
14+
{
15+
var context = await Scenario.Define<Context>()
16+
.WithEndpoint<EndpointWithFeatures>(b => b.CustomConfig(c =>
17+
{
18+
c.EnableFeature<DependencyFeature>();
19+
c.EnableFeature<TypedDependentFeature>();
20+
}))
21+
.Done(c => c.EndpointsStarted)
22+
.Run();
23+
24+
Assert.That(context.StartCalled, Is.True);
25+
Assert.That(context.StopCalled, Is.True);
26+
}
27+
28+
class Context : ScenarioContext
29+
{
30+
public bool StartCalled { get; set; }
31+
public bool StopCalled { get; set; }
32+
public bool InitializeCalled { get; set; }
33+
}
34+
35+
public class EndpointWithFeatures : EndpointConfigurationBuilder
36+
{
37+
public EndpointWithFeatures()
38+
{
39+
EndpointSetup<DefaultServer>();
40+
}
41+
}
42+
43+
public class TypedDependentFeature : Feature
44+
{
45+
public TypedDependentFeature()
46+
{
47+
DependsOn<DependencyFeature>();
48+
}
49+
50+
protected override void Setup(FeatureConfigurationContext context)
51+
{
52+
context.Container.ConfigureComponent<Runner>(DependencyLifecycle.SingleInstance);
53+
context.RegisterStartupTask(b => b.Build<Runner>());
54+
}
55+
56+
class Runner : FeatureStartupTask
57+
{
58+
Dependency dependency;
59+
60+
public Runner(Dependency dependency)
61+
{
62+
this.dependency = dependency;
63+
}
64+
protected override Task OnStart(IMessageSession session)
65+
{
66+
dependency.Start();
67+
return Task.FromResult(0);
68+
}
69+
70+
protected override Task OnStop(IMessageSession session)
71+
{
72+
dependency.Stop();
73+
return Task.FromResult(0);
74+
}
75+
}
76+
}
77+
78+
public class DependencyFeature : Feature
79+
{
80+
protected override void Setup(FeatureConfigurationContext context)
81+
{
82+
context.Container.ConfigureComponent<Dependency>(DependencyLifecycle.SingleInstance);
83+
84+
context.Container.ConfigureComponent<Runner>(DependencyLifecycle.SingleInstance);
85+
context.RegisterStartupTask(b => b.Build<Runner>());
86+
}
87+
88+
class Runner : FeatureStartupTask
89+
{
90+
Dependency dependency;
91+
92+
public Runner(Dependency dependency)
93+
{
94+
this.dependency = dependency;
95+
}
96+
protected override Task OnStart(IMessageSession session)
97+
{
98+
dependency.Initialize();
99+
return Task.FromResult(0);
100+
}
101+
102+
protected override Task OnStop(IMessageSession session)
103+
{
104+
return Task.FromResult(0);
105+
}
106+
}
107+
}
108+
109+
class Dependency
110+
{
111+
Context context;
112+
113+
public Dependency(Context context)
114+
{
115+
this.context = context;
116+
}
117+
118+
public void Start()
119+
{
120+
if (!context.InitializeCalled)
121+
{
122+
throw new InvalidOperationException("Not initialized");
123+
}
124+
context.StartCalled = true;
125+
}
126+
127+
public void Stop()
128+
{
129+
if (!context.InitializeCalled)
130+
{
131+
throw new InvalidOperationException("Not initialized");
132+
}
133+
134+
context.StopCalled = true;
135+
}
136+
137+
public void Initialize()
138+
{
139+
context.InitializeCalled = true;
140+
}
141+
}
142+
}
143+
}

src/NServiceBus.Core.Tests/Features/FeatureStartupTests.cs

+102
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Text;
56
using System.Threading.Tasks;
67
using NServiceBus.Features;
78
using NUnit.Framework;
@@ -34,6 +35,28 @@ public async Task Should_start_and_stop_features()
3435
Assert.True(feature.TaskStopped);
3536
}
3637

38+
[Test]
39+
public async Task Should_start_and_stop_features_in_dependency_order()
40+
{
41+
var orderBuilder = new StringBuilder();
42+
43+
featureSettings.Add(new FeatureWithStartupTaskWithDependency(orderBuilder));
44+
featureSettings.Add(new FeatureWithStartupThatAnotherFeatureDependsOn(orderBuilder));
45+
46+
featureSettings.SetupFeatures(null, null, null, null);
47+
48+
await featureSettings.StartFeatures(null, null);
49+
await featureSettings.StopFeatures(null);
50+
51+
var expectedOrderBuilder = new StringBuilder();
52+
expectedOrderBuilder.AppendLine("FeatureWithStartupThatAnotherFeatureDependsOn.Start");
53+
expectedOrderBuilder.AppendLine("FeatureWithStartupTaskWithDependency.Start");
54+
expectedOrderBuilder.AppendLine("FeatureWithStartupThatAnotherFeatureDependsOn.Stop");
55+
expectedOrderBuilder.AppendLine("FeatureWithStartupTaskWithDependency.Stop");
56+
57+
Assert.AreEqual(expectedOrderBuilder.ToString(), orderBuilder.ToString());
58+
}
59+
3760
[Test]
3861
public async Task Should_dispose_feature_startup_tasks_when_they_implement_IDisposable()
3962
{
@@ -99,6 +122,85 @@ public async Task Should_dispose_feature_task_even_when_stop_throws()
99122
private FeatureActivator featureSettings;
100123
private SettingsHolder settings;
101124

125+
class FeatureWithStartupTaskWithDependency : TestFeature
126+
{
127+
public FeatureWithStartupTaskWithDependency(StringBuilder orderBuilder)
128+
{
129+
EnableByDefault();
130+
DependsOn<FeatureWithStartupThatAnotherFeatureDependsOn>();
131+
132+
this.orderBuilder = orderBuilder;
133+
}
134+
135+
protected internal override void Setup(FeatureConfigurationContext context)
136+
{
137+
context.RegisterStartupTask(new Runner(orderBuilder));
138+
}
139+
140+
class Runner : FeatureStartupTask
141+
{
142+
public Runner(StringBuilder orderBuilder)
143+
{
144+
this.orderBuilder = orderBuilder;
145+
}
146+
147+
protected override Task OnStart(IMessageSession session)
148+
{
149+
orderBuilder.AppendLine($"{nameof(FeatureWithStartupTaskWithDependency)}.Start");
150+
return TaskEx.CompletedTask;
151+
}
152+
153+
protected override Task OnStop(IMessageSession session)
154+
{
155+
orderBuilder.AppendLine($"{nameof(FeatureWithStartupTaskWithDependency)}.Stop");
156+
return TaskEx.CompletedTask;
157+
}
158+
159+
StringBuilder orderBuilder;
160+
}
161+
162+
readonly StringBuilder orderBuilder;
163+
}
164+
165+
class FeatureWithStartupThatAnotherFeatureDependsOn : TestFeature
166+
{
167+
public FeatureWithStartupThatAnotherFeatureDependsOn(StringBuilder orderBuilder)
168+
{
169+
EnableByDefault();
170+
171+
this.orderBuilder = orderBuilder;
172+
}
173+
174+
protected internal override void Setup(FeatureConfigurationContext context)
175+
{
176+
context.RegisterStartupTask(new Runner(orderBuilder));
177+
}
178+
179+
class Runner : FeatureStartupTask
180+
{
181+
public Runner(StringBuilder orderBuilder)
182+
{
183+
this.orderBuilder = orderBuilder;
184+
}
185+
186+
protected override Task OnStart(IMessageSession session)
187+
{
188+
orderBuilder.AppendLine($"{nameof(FeatureWithStartupThatAnotherFeatureDependsOn)}.Start");
189+
return TaskEx.CompletedTask;
190+
}
191+
192+
protected override Task OnStop(IMessageSession session)
193+
{
194+
orderBuilder.AppendLine($"{nameof(FeatureWithStartupThatAnotherFeatureDependsOn)}.Stop");
195+
return TaskEx.CompletedTask;
196+
}
197+
198+
StringBuilder orderBuilder;
199+
}
200+
201+
readonly StringBuilder orderBuilder;
202+
}
203+
102204
class FeatureWithStartupTask : TestFeature
103205
{
104206
public FeatureWithStartupTask()

src/NServiceBus.Core/Features/FeatureActivator.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ public FeatureDiagnosticData[] SetupFeatures(IConfigureComponents container, Pip
4141
// featuresToActivate is enumerated twice because after setting defaults some new features might got activated.
4242
var sourceFeatures = Sort(features);
4343

44-
var enabledFeatures = new List<FeatureInfo>();
4544
while (true)
4645
{
4746
var featureToActivate = sourceFeatures.FirstOrDefault(x => settings.IsFeatureEnabled(x.Feature.GetType()));
@@ -66,7 +65,8 @@ public FeatureDiagnosticData[] SetupFeatures(IConfigureComponents container, Pip
6665

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

7878
public Task StopFeatures(IMessageSession session)
7979
{
80-
var featureStopTasks = features.Where(f => f.Feature.IsActive)
80+
var featureStopTasks = enabledFeatures.Where(f => f.Feature.IsActive)
8181
.SelectMany(f => f.TaskControllers)
8282
.Select(task => task.Stop(session));
8383

@@ -210,6 +210,7 @@ static bool HasAllPrerequisitesSatisfied(Feature feature, FeatureDiagnosticData
210210
}
211211

212212
List<FeatureInfo> features = new List<FeatureInfo>();
213+
List<FeatureInfo> enabledFeatures = new List<FeatureInfo>();
213214
SettingsHolder settings;
214215

215216
class FeatureInfo

0 commit comments

Comments
 (0)