Skip to content

Commit 3c8ae8a

Browse files
committed
add Starting/Started/Stopping/Stopped async methods
1 parent 7c09ac0 commit 3c8ae8a

File tree

2 files changed

+279
-2
lines changed

2 files changed

+279
-2
lines changed

src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/NodeBase.cs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ async ValueTask StartAsyncCore()
228228
if (Logger is not null)
229229
LogStartingNode(Logger, null);
230230

231+
await StartingAsync(cancellationToken).ConfigureAwait(false);
232+
233+
cancellationToken.ThrowIfCancellationRequested();
234+
231235
listener = await listenerFactory.CreateAsync(
232236
endPoint: GetLocalEndPointToBind(),
233237
node: this,
@@ -246,13 +250,47 @@ async ValueTask StartAsyncCore()
246250
cancellationToken: cancellationToken
247251
).ConfigureAwait(false);
248252

253+
sessionCountdownEvent.Reset();
254+
255+
await StartedAsync(cancellationToken).ConfigureAwait(false);
256+
249257
if (Logger is not null)
250258
LogStartedNode(Logger, HostName, listener.EndPoint, null);
251-
252-
sessionCountdownEvent.Reset();
253259
}
254260
}
255261

262+
/// <summary>
263+
/// Provides an extension point for starting the <c>Munin-Node</c> instance.
264+
/// If overridden in a derived class, it is invoked before the <c>Munin-Node</c> is started
265+
/// by a call to the <see cref="StartAsync"/> method.
266+
/// </summary>
267+
/// <param name="cancellationToken">
268+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
269+
/// </param>
270+
/// <returns>
271+
/// The <see cref="ValueTask"/> that represents the asynchronous operation.
272+
/// </returns>
273+
/// <seealso cref="StartAsync"/>
274+
/// <seealso cref="StartedAsync"/>
275+
protected virtual ValueTask StartingAsync(CancellationToken cancellationToken)
276+
=> default; // nothing to do in this class
277+
278+
/// <summary>
279+
/// Provides an extension point for starting the <c>Munin-Node</c> instance.
280+
/// If overridden in a derived class, it is invoked after the <c>Munin-Node</c> is started
281+
/// by a call to the <see cref="StartAsync"/> method.
282+
/// </summary>
283+
/// <param name="cancellationToken">
284+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
285+
/// </param>
286+
/// <returns>
287+
/// The <see cref="ValueTask"/> that represents the asynchronous operation.
288+
/// </returns>
289+
/// <seealso cref="StartAsync"/>
290+
/// <seealso cref="StartingAsync"/>
291+
protected virtual ValueTask StartedAsync(CancellationToken cancellationToken)
292+
=> default; // nothing to do in this class
293+
256294
/// <summary>
257295
/// Stops accepting connections from clients at the <c>Munin-Node</c> currently running.
258296
/// </summary>
@@ -277,9 +315,15 @@ public ValueTask StopAsync(CancellationToken cancellationToken = default)
277315

278316
async ValueTask StopAsyncCore()
279317
{
318+
cancellationToken.ThrowIfCancellationRequested();
319+
280320
if (Logger is not null)
281321
LogStoppingNode(Logger, HostName, null);
282322

323+
await StoppingAsync(cancellationToken).ConfigureAwait(false);
324+
325+
cancellationToken.ThrowIfCancellationRequested();
326+
283327
// decrement by the initial value of 1 (re)set in Start()/StartAsync()
284328
sessionCountdownEvent.Signal();
285329

@@ -307,11 +351,45 @@ async ValueTask StopAsyncCore()
307351
protocolHandler = null;
308352
}
309353

354+
await StoppedAsync(cancellationToken).ConfigureAwait(false);
355+
310356
if (Logger is not null)
311357
LogStoppedNode(Logger, HostName, null);
312358
}
313359
}
314360

361+
/// <summary>
362+
/// Provides an extension point for stopping the <c>Munin-Node</c> instance.
363+
/// If overridden in a derived class, it is invoked before the <c>Munin-Node</c> is stopped
364+
/// by a call to the <see cref="StopAsync"/> method.
365+
/// </summary>
366+
/// <param name="cancellationToken">
367+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
368+
/// </param>
369+
/// <returns>
370+
/// The <see cref="ValueTask"/> that represents the asynchronous operation.
371+
/// </returns>
372+
/// <seealso cref="StopAsync"/>
373+
/// <seealso cref="StoppedAsync"/>
374+
protected virtual ValueTask StoppingAsync(CancellationToken cancellationToken)
375+
=> default; // nothing to do in this class
376+
377+
/// <summary>
378+
/// Provides an extension point for stopping the <c>Munin-Node</c> instance.
379+
/// If overridden in a derived class, it is invoked after the <c>Munin-Node</c> is stopped
380+
/// by a call to the <see cref="StopAsync"/> method.
381+
/// </summary>
382+
/// <param name="cancellationToken">
383+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
384+
/// </param>
385+
/// <returns>
386+
/// The <see cref="ValueTask"/> that represents the asynchronous operation.
387+
/// </returns>
388+
/// <seealso cref="StopAsync"/>
389+
/// <seealso cref="StoppingAsync"/>
390+
protected virtual ValueTask StoppedAsync(CancellationToken cancellationToken)
391+
=> default; // nothing to do in this class
392+
315393
/// <inheritdoc cref="IMuninNode.RunAsync(CancellationToken)"/>
316394
/// <seealso cref="IMuninNode.RunAsync(CancellationToken)"/>
317395
/// <seealso cref="StartAsync(CancellationToken)"/>

tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode/NodeBase.cs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
using NUnit.Framework;
1414

15+
using Smdn.Net.MuninNode.Transport;
1516
using Smdn.Net.MuninPlugin;
1617

1718
namespace Smdn.Net.MuninNode;
@@ -69,6 +70,63 @@ protected override EndPoint GetLocalEndPointToBind()
6970
);
7071
}
7172

73+
private class TestLifecycleLocalNode : NodeBase {
74+
public EventHandler? NodeStarting = null;
75+
public EventHandler? NodeStarted = null;
76+
public EventHandler? NodeStopping = null;
77+
public EventHandler? NodeStopped = null;
78+
79+
private class NullPluginProvider : IPluginProvider {
80+
public IReadOnlyCollection<IPlugin> Plugins { get; } = [];
81+
public INodeSessionCallback? SessionCallback => null;
82+
}
83+
84+
public override IPluginProvider PluginProvider { get; } = new NullPluginProvider();
85+
public override string HostName => "test.munin-node.localhost";
86+
87+
public TestLifecycleLocalNode()
88+
#pragma warning disable CS0618
89+
: base(
90+
listenerFactory: new PseudoMuninNodeListenerFactory(),
91+
accessRule: null,
92+
logger: null
93+
)
94+
#pragma warning restore CS0618
95+
{
96+
}
97+
98+
protected override EndPoint GetLocalEndPointToBind()
99+
=> new IPEndPoint(IPAddress.Any, port: 0);
100+
101+
protected override ValueTask StartingAsync(CancellationToken cancellationToken)
102+
{
103+
NodeStarting?.Invoke(this, EventArgs.Empty);
104+
105+
return default;
106+
}
107+
108+
protected override ValueTask StartedAsync(CancellationToken cancellationToken)
109+
{
110+
NodeStarted?.Invoke(this, EventArgs.Empty);
111+
112+
return default;
113+
}
114+
115+
protected override ValueTask StoppingAsync(CancellationToken cancellationToken)
116+
{
117+
NodeStopping?.Invoke(this, EventArgs.Empty);
118+
119+
return default;
120+
}
121+
122+
protected override ValueTask StoppedAsync(CancellationToken cancellationToken)
123+
{
124+
NodeStopped?.Invoke(this, EventArgs.Empty);
125+
126+
return default;
127+
}
128+
}
129+
72130
private static NodeBase CreateNode()
73131
=> CreateNode(accessRule: null, plugins: Array.Empty<IPlugin>());
74132

@@ -217,6 +275,72 @@ public async Task StartAsync_Restart()
217275
#pragma warning restore CS0618
218276
}
219277

278+
[Test]
279+
public async Task StartAsync_CancellationRequestedBeforeStarting()
280+
{
281+
await using var node = new TestLifecycleLocalNode();
282+
var cancellationToken = new CancellationToken(canceled: true);
283+
284+
var numberOfInvocationOfStartingAsync = 0;
285+
var numberOfInvocationOfStartedAsync = 0;
286+
287+
node.NodeStarting += (_, _) => numberOfInvocationOfStartingAsync++;
288+
node.NodeStarted += (_, _) => numberOfInvocationOfStartedAsync++;
289+
290+
Assert.That(async () => await node.StartAsync(cancellationToken), Throws.InstanceOf<OperationCanceledException>());
291+
292+
Assert.That(numberOfInvocationOfStartingAsync, Is.Zero);
293+
Assert.That(numberOfInvocationOfStartedAsync, Is.Zero);
294+
295+
Assert.That(() => _ = node.EndPoint, Throws.InvalidOperationException, "must not be started");
296+
}
297+
298+
[Test]
299+
public async Task StartAsync_CancellationRequestedWhileStarting()
300+
{
301+
await using var node = new TestLifecycleLocalNode();
302+
using var cts = new CancellationTokenSource();
303+
304+
var numberOfInvocationOfStartingAsync = 0;
305+
var numberOfInvocationOfStartedAsync = 0;
306+
307+
node.NodeStarting += (_, _) => {
308+
numberOfInvocationOfStartingAsync++;
309+
cts.Cancel();
310+
};
311+
node.NodeStarted += (_, _) => numberOfInvocationOfStartedAsync++;
312+
313+
Assert.That(async () => await node.StartAsync(cts.Token), Throws.InstanceOf<OperationCanceledException>());
314+
315+
Assert.That(numberOfInvocationOfStartingAsync, Is.EqualTo(1));
316+
Assert.That(numberOfInvocationOfStartedAsync, Is.Zero);
317+
318+
Assert.That(() => _ = node.EndPoint, Throws.InvalidOperationException, "must not be started");
319+
}
320+
321+
[Test]
322+
public async Task StartAsync_CancellationRequestedAfterStarted()
323+
{
324+
await using var node = new TestLifecycleLocalNode();
325+
using var cts = new CancellationTokenSource();
326+
327+
var numberOfInvocationOfStartingAsync = 0;
328+
var numberOfInvocationOfStartedAsync = 0;
329+
330+
node.NodeStarting += (_, _) => numberOfInvocationOfStartingAsync++;
331+
node.NodeStarted += (_, _) => {
332+
numberOfInvocationOfStartedAsync++;
333+
cts.Cancel();
334+
};
335+
336+
Assert.That(async () => await node.StartAsync(cts.Token), Throws.Nothing, "requested cancellation must be ignored");
337+
338+
Assert.That(numberOfInvocationOfStartingAsync, Is.EqualTo(1));
339+
Assert.That(numberOfInvocationOfStartedAsync, Is.EqualTo(1));
340+
341+
Assert.That(() => _ = node.EndPoint, Throws.Nothing, "must be started");
342+
}
343+
220344
[Test]
221345
public async Task StopAsync_StartedByStartAsync()
222346
{
@@ -298,4 +422,79 @@ public async Task StopAsync_WhileProcessingClient()
298422
Throws.Nothing
299423
);
300424
}
425+
426+
[Test]
427+
public async Task StopAsync_CancellationRequestedBeforeStopping()
428+
{
429+
await using var node = new TestLifecycleLocalNode();
430+
431+
await node.StartAsync();
432+
433+
var cancellationToken = new CancellationToken(canceled: true);
434+
435+
var numberOfInvocationOfStoppingAsync = 0;
436+
var numberOfInvocationOfStoppedAsync = 0;
437+
438+
node.NodeStopping += (_, _) => numberOfInvocationOfStoppingAsync++;
439+
node.NodeStopped += (_, _) => numberOfInvocationOfStoppedAsync++;
440+
441+
Assert.That(async () => await node.StopAsync(cancellationToken), Throws.InstanceOf<OperationCanceledException>());
442+
443+
Assert.That(numberOfInvocationOfStoppingAsync, Is.Zero);
444+
Assert.That(numberOfInvocationOfStoppedAsync, Is.Zero);
445+
446+
Assert.That(() => _ = node.EndPoint, Throws.Nothing, "must not be stopped");
447+
}
448+
449+
[Test]
450+
public async Task StopAsync_CancellationRequestedWhileStopping()
451+
{
452+
await using var node = new TestLifecycleLocalNode();
453+
454+
await node.StartAsync();
455+
456+
using var cts = new CancellationTokenSource();
457+
458+
var numberOfInvocationOfStoppingAsync = 0;
459+
var numberOfInvocationOfStoppedAsync = 0;
460+
461+
node.NodeStopping += (_, _) => {
462+
numberOfInvocationOfStoppingAsync++;
463+
cts.Cancel();
464+
};
465+
node.NodeStopped += (_, _) => numberOfInvocationOfStoppedAsync++;
466+
467+
Assert.That(async () => await node.StopAsync(cts.Token), Throws.InstanceOf<OperationCanceledException>());
468+
469+
Assert.That(numberOfInvocationOfStoppingAsync, Is.EqualTo(1));
470+
Assert.That(numberOfInvocationOfStoppedAsync, Is.Zero);
471+
472+
Assert.That(() => _ = node.EndPoint, Throws.Nothing, "must not be stopped");
473+
}
474+
475+
[Test]
476+
public async Task StopAsync_CancellationRequestedAfterStopped()
477+
{
478+
await using var node = new TestLifecycleLocalNode();
479+
480+
await node.StartAsync();
481+
482+
using var cts = new CancellationTokenSource();
483+
484+
var numberOfInvocationOfStoppingAsync = 0;
485+
var numberOfInvocationOfStoppedAsync = 0;
486+
487+
node.NodeStopping += (_, _) => numberOfInvocationOfStoppingAsync++;
488+
node.NodeStopped += (_, _) => {
489+
numberOfInvocationOfStoppedAsync++;
490+
cts.Cancel();
491+
};
492+
493+
Assert.That(async () => await node.StopAsync(cts.Token), Throws.Nothing, "requested cancellation must be ignored");
494+
495+
Assert.That(numberOfInvocationOfStoppingAsync, Is.EqualTo(1));
496+
Assert.That(numberOfInvocationOfStoppedAsync, Is.EqualTo(1));
497+
498+
Assert.That(() => _ = node.EndPoint, Throws.InvalidOperationException, "must be stopped");
499+
}
301500
}

0 commit comments

Comments
 (0)