Skip to content

Commit 416c312

Browse files
committed
add StopAsync()
1 parent b77b8d6 commit 416c312

File tree

4 files changed

+203
-0
lines changed

4 files changed

+203
-0
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ partial class NodeBase {
2121
eventId: default, // TODO
2222
formatString: "Started munin-node '{HostName}' on '{EndPoint}'."
2323
);
24+
private static readonly Action<ILogger, string, Exception?> LogStoppingNode = LoggerMessage.Define<string>(
25+
LogLevel.Debug,
26+
eventId: default, // TODO
27+
formatString: "Stopping munin-node '{HostName}'."
28+
);
29+
private static readonly Action<ILogger, string, Exception?> LogStoppedNode = LoggerMessage.Define<string>(
30+
LogLevel.Information,
31+
eventId: default, // TODO
32+
formatString: "Stopped munin-node '{HostName}'."
33+
);
2434
private static readonly Action<ILogger, Exception?> LogStartedAcceptingConnections = LoggerMessage.Define(
2535
LogLevel.Information,
2636
eventId: default, // TODO

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ public void Start()
112112
? createHandlerValueTask.Result
113113
: createHandlerValueTask.AsTask().GetAwaiter().GetResult();
114114

115+
sessionCountdownEvent.Reset();
116+
115117
Logger?.LogInformation("started (end point: {EndPoint})", listener.EndPoint);
116118
}
117119
}

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public EndPoint EndPoint {
7878
}
7979
}
8080

81+
private CountdownEvent sessionCountdownEvent = new(initialCount: 1);
8182
private bool disposed;
8283

8384
protected NodeBase(
@@ -135,6 +136,9 @@ protected virtual async ValueTask DisposeAsyncCore()
135136

136137
protocolHandler = null;
137138

139+
sessionCountdownEvent?.Dispose();
140+
sessionCountdownEvent = null!;
141+
138142
disposed = true;
139143
}
140144

@@ -151,6 +155,9 @@ protected virtual void Dispose(bool disposing)
151155

152156
protocolHandler = null;
153157

158+
sessionCountdownEvent?.Dispose();
159+
sessionCountdownEvent = null!;
160+
154161
disposed = true;
155162
}
156163

@@ -240,6 +247,67 @@ async ValueTask StartAsyncCore()
240247

241248
if (Logger is not null)
242249
LogStartedNode(Logger, HostName, listener.EndPoint, null);
250+
251+
sessionCountdownEvent.Reset();
252+
}
253+
}
254+
255+
/// <summary>
256+
/// Stops accepting connections from clients at the <c>Munin-Node</c> currently running.
257+
/// </summary>
258+
/// <param name="cancellationToken">
259+
/// The <see cref="CancellationToken"/> to monitor for cancellation requests.
260+
/// </param>
261+
/// <returns>
262+
/// The <see cref="ValueTask"/> that represents the asynchronous operation,
263+
/// starting the <c>Munin-Node</c> instance.
264+
/// </returns>
265+
/// <remarks>
266+
/// If current <c>Munin-Node</c> has already stopped, this method does nothing and returns the result.
267+
/// </remarks>
268+
public ValueTask StopAsync(CancellationToken cancellationToken)
269+
{
270+
ThrowIfDisposed();
271+
272+
if (listener is null)
273+
return default; // already stopped
274+
275+
return StopAsyncCore();
276+
277+
async ValueTask StopAsyncCore()
278+
{
279+
if (Logger is not null)
280+
LogStoppingNode(Logger, HostName, null);
281+
282+
// decrement by the initial value of 1 (re)set in Start()/StartAsync()
283+
sessionCountdownEvent.Signal();
284+
285+
try {
286+
// wait for all sessions to complete
287+
sessionCountdownEvent.Wait(cancellationToken);
288+
}
289+
catch (OperationCanceledException ex) when (cancellationToken.Equals(ex.CancellationToken)) {
290+
// revert decremented counter value
291+
sessionCountdownEvent.AddCount();
292+
293+
throw;
294+
}
295+
296+
await listener.DisposeAsync().ConfigureAwait(false);
297+
298+
listener = null;
299+
300+
if (protocolHandler is not null) {
301+
if (protocolHandler is IAsyncDisposable asyncDisposableProtocolHandler)
302+
await asyncDisposableProtocolHandler.DisposeAsync().ConfigureAwait(false);
303+
else if (protocolHandler is IDisposable disposableProtocolHandler)
304+
disposableProtocolHandler.Dispose();
305+
306+
protocolHandler = null;
307+
}
308+
309+
if (Logger is not null)
310+
LogStoppedNode(Logger, HostName, null);
243311
}
244312
}
245313

@@ -335,6 +403,8 @@ public async ValueTask AcceptSingleSessionAsync(
335403
using var scope = Logger?.BeginScope(client.EndPoint);
336404

337405
try {
406+
sessionCountdownEvent.AddCount();
407+
338408
cancellationToken.ThrowIfCancellationRequested();
339409

340410
if (!CanAccept(client))
@@ -348,6 +418,8 @@ await ProcessSessionAsync(
348418
finally {
349419
await client.DisposeAsync().ConfigureAwait(false);
350420

421+
sessionCountdownEvent.Signal();
422+
351423
if (Logger is not null)
352424
LogAcceptedConnectionClosed(Logger, null);
353425
}

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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Net;
88
using System.Net.Sockets;
99
using System.Runtime.InteropServices;
10+
using System.Threading;
1011
using System.Threading.Tasks;
1112

1213
using NUnit.Framework;
@@ -161,6 +162,24 @@ public async Task Start()
161162
#pragma warning restore CS0618
162163
}
163164

165+
[Test]
166+
public async Task Start_Restart()
167+
{
168+
await using var node = CreateNode();
169+
170+
#pragma warning disable CS0618
171+
Assert.DoesNotThrow(node.Start);
172+
Assert.Throws<InvalidOperationException>(node.Start, "already started");
173+
#pragma warning restore CS0618
174+
175+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing);
176+
177+
#pragma warning disable CS0618
178+
Assert.DoesNotThrow(node.Start, "restart");
179+
Assert.Throws<InvalidOperationException>(node.Start, "already restarted");
180+
#pragma warning restore CS0618
181+
}
182+
164183
[Test]
165184
public async Task StartAsync()
166185
{
@@ -179,4 +198,104 @@ public async Task StartAsync()
179198
Assert.That(() => _ = node.LocalEndPoint, Throws.Nothing, $"{nameof(node.LocalEndPoint)} after start");
180199
#pragma warning restore CS0618
181200
}
201+
202+
[Test]
203+
public async Task StartAsync_Restart()
204+
{
205+
await using var node = CreateNode();
206+
207+
#pragma warning disable CS0618
208+
Assert.That(async () => await node.StartAsync(default), Throws.Nothing);
209+
Assert.That(async () => await node.StartAsync(default), Throws.InvalidOperationException, "already started");
210+
#pragma warning restore CS0618
211+
212+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing);
213+
214+
#pragma warning disable CS0618
215+
Assert.That(async () => await node.StartAsync(default), Throws.Nothing, "restart");
216+
Assert.That(async () => await node.StartAsync(default), Throws.InvalidOperationException, "already restarted");
217+
#pragma warning restore CS0618
218+
}
219+
220+
[Test]
221+
public async Task StopAsync_StartedByStartAsync()
222+
{
223+
await using var node = CreateNode();
224+
225+
Assert.That(async () => await node.StartAsync(default), Throws.Nothing);
226+
227+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing);
228+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing, "stop again");
229+
}
230+
231+
[Test]
232+
public async Task StopAsync_StartedByStart()
233+
{
234+
await using var node = CreateNode();
235+
236+
#pragma warning disable CS0618
237+
Assert.That(() => node.Start(), Throws.Nothing);
238+
#pragma warning restore CS0618
239+
240+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing);
241+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing, "stop again");
242+
}
243+
244+
[Test]
245+
public async Task StopAsync_NotStarted()
246+
{
247+
await using var node = CreateNode();
248+
249+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing);
250+
Assert.That(async () => await node.StopAsync(default), Throws.Nothing, "stop again");
251+
}
252+
253+
[Test]
254+
public async Task StopAsync_WhileProcessingClient()
255+
{
256+
await using var node = CreateNode();
257+
258+
await node.StartAsync();
259+
260+
using var cts = new CancellationTokenSource();
261+
262+
var taskAccept = Task.Run(async () => await node.AcceptAsync(throwIfCancellationRequested: false, cts.Token));
263+
264+
using var client = CreateClient((IPEndPoint)node.EndPoint, out var writer, out var reader);
265+
266+
reader.ReadLine(); // banner
267+
268+
// attempt stop while processing client
269+
const int MaxAttempt = 10;
270+
271+
for (var n = 0; n < MaxAttempt; n++) {
272+
using var ctsStopWhileProcessingClientTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(0.1));
273+
274+
Assert.That(
275+
async () => await node.StopAsync(ctsStopWhileProcessingClientTimeout.Token),
276+
Throws
277+
.InstanceOf<OperationCanceledException>()
278+
.With
279+
.Property(nameof(OperationCanceledException.CancellationToken))
280+
.EqualTo(ctsStopWhileProcessingClientTimeout.Token)
281+
);
282+
}
283+
284+
// close session, disconnect client
285+
writer.WriteLine(".");
286+
writer.Close();
287+
288+
// stop accepting task
289+
cts.Cancel();
290+
291+
Assert.DoesNotThrowAsync(async () => await taskAccept);
292+
293+
// attempt stop after processing client
294+
using var ctsStopTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(1));
295+
296+
Assert.That(
297+
async () => await node.StopAsync(ctsStopTimeout.Token),
298+
Throws.Nothing
299+
);
300+
}
182301
}

0 commit comments

Comments
 (0)