I am experimenting with gRPC for long-lived streaming session as I need to guarantee message ordering from server to client.
I have the following .proto:
service Subscriber {
rpc Subscribe(SubscriptionRequest) returns (stream SubscriberEvent);
}
My current service (hosted in ASP.NET / .NET 5.0) looks like this:
public class SubscriberService : Subscriber.SubscriberBase
{
private readonly ILogger<SubscriberService> _logger;
private readonly ConcurrentDictionary<string, IServerStreamWriter<SubscriberEvent>> _subscriptions = new();
private int _messageCount = 0;
private Timer _timer;
public SubscriberService(ILogger<SubscriberService> logger)
{
_logger = logger;
_timer = new Timer(o => TimerCallback(), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
private void TimerCallback()
{
Broadcast($"Current time is {DateTime.UtcNow}");
}
public override Task Subscribe(SubscriptionRequest request, IServerStreamWriter<SubscriberEvent> responseStream, ServerCallContext context)
{
_subscriptions.TryAdd(request.ClientId, responseStream);
return responseStream.WriteAsync(new SubscriberEvent() {Id = 0, Message = "Subscribe successful"});
}
public void Broadcast(string message)
{
var count = ++_messageCount;
foreach (var sub in _subscriptions.Values)
{
sub.WriteAsync(new SubscriberEvent() { Id = count, Message = message });
}
_logger.LogInformation($"Broadcast message #{count}: {message}");
}
}
My client only receives the initial 'Subscribe Successful' message, but never those triggered by the timer. Not do I get any exceptions when calling WriteAsync.
Am I trying to use gRPC for something it was never designed to do (a SignalR/WebSocket substitute), or am I merely missing something obvious?
For a long-running gRPC streaming, you have to wait for a client to say the connection is closed. Something like this:
while (!context.CancellationToken.IsCancellationRequested)
{
// event-based action
responseStream.WriteAsync(new SubscriberEvent() {Id = 0, Message = "Subscribe successful"});
}
I think the previous answer is doing busy-wait. So I want to show you an async version of it.
public override Task Subscribe(RequestMessage, IServerStreamWriter<ReplyMessage> responseStream, ServerCallContext context)
{
// your event-based code here
var tcs = new TaskCompletionSource();
context.CancellationToken.Register(() => tcs.TrySetCanceled(), false);
return tcs.Task;
}
BTW, I think I have been doing a project just like yours. And I have used Observables, Subjects, and ReplaySubjects from Rx.Net. These are very helpful for event-based code.
Related
I am trying to implement a request response pattern in C# with the ArtemisNetClient, but having a bit of trouble finding out how to do so in a more generic way in a real solution.
I was able to do something like this in two console applications based on some Java examples:
Sender
static async System.Threading.Tasks.Task Main(string[] args)
{
var connectionFactory = new ConnectionFactory();
var endpoint = Endpoint.Create("localhost", 5672, "guest", "guest");
var connection = await connectionFactory.CreateAsync(endpoint);
string guid = new Guid().ToString();
var requestAddress = "TRADE REQ1";
var responseAddress = "TRADE RESP";
Message message = new Message("BUY AMD 1000 SHARES");
message.SetCorrelationId(guid);
message.ReplyTo = responseAddress;
var producer = await connection.CreateProducerAsync(requestAddress, RoutingType.Anycast);
await producer.SendAsync(message);
var consumer = await connection.CreateConsumerAsync(responseAddress, RoutingType.Anycast);
var responseMessage = await consumer.ReceiveAsync();
Console.WriteLine(responseMessage.GetBody<string>());
}
Receiver
static async System.Threading.Tasks.Task Main(string[] args)
{
// Create connection
var connectionFactory = new ConnectionFactory();
var endpoint = Endpoint.Create("localhost", 5672, "guest", "guest");
var connection = await connectionFactory.CreateAsync(endpoint);
var requestAddress = "TRADE REQ1";
// Create consumer to receive trade request messages
var consumer = await connection.CreateConsumerAsync(requestAddress, RoutingType.Anycast);
var message = await consumer.ReceiveAsync();
Console.WriteLine($"Received message: {message.GetBody<string>()}");
// Confirm trade request and ssend response message
if (!string.IsNullOrEmpty(message.ReplyTo))
{
Message responseMessage = new Message("Confirmed trade request");
responseMessage.SetCorrelationId(message.CorrelationId);
var producer = await connection.CreateProducerAsync(message.ReplyTo);
await producer.SendAsync(responseMessage);
}
}
This worked as expected, but I'd like to have something more down the line of what is described in this article, except it doesn't have any examples of a request response pattern.
To elaborate, I currently have two services that I want to communicate across.
In service 1 I want to create and publish a message and then wait for a response to enrich the instance object and save it to a database. I currently have this, but it lacks the await response message.
public async Task<Instance> CreateInstance(Instance instance)
{
await _instanceCollection.InsertOneAsync(instance);
var #event = new InstanceCreated
{
Id = instance.Id,
SiteUrl = instance.SiteUrl
};
await _messageProducer.PublishAsync(#event);
return instance;
}
I figured I might need to setup a temporary queue/connection or something in the PublishAsync() and change it to e.g. Task<Message> to support returning a response message. But how would I go about doing that? Would I have to do a new connectionfactory + CreateConsumerAsync etc. like in the console app example?
public class MessageProducer
{
private readonly IAnonymousProducer _producer;
public MessageProducer(IAnonymousProducer producer)
{
_producer = producer;
}
public async Task PublishAsync<T>(T message, string replyTo = null, string correlationId = null)
{
var serialized = JsonSerializer.Serialize(message);
var address = typeof(T).Name;
var msg = new Message(serialized);
if (replyTo != null && correlationId != null)
{
msg.CorrelationId = correlationId;
msg.ReplyTo = replyTo;
}
await _producer.SendAsync(address, msg);
}
public async Task PublishAsync<T>(T message, string routeName, string replyTo = null, string correlationId = null)
{
var serialized = JsonSerializer.Serialize(message);
var address = routeName;
var msg = new Message(serialized);
if(replyTo != null && correlationId != null)
{
msg.CorrelationId = correlationId;
msg.ReplyTo = replyTo;
}
await _producer.SendAsync(address, msg);
}
}
In Service 2 I have a InstanceCreatedConsumer which receives messages, but again it lacks a way to return response messages.
public class InstanceCreatedConsumer : ITypedConsumer<InstanceCreated>
{
private readonly MessageProducer _messageProducer;
public InstanceCreatedConsumer(MessageProducer messageProducer)
{
_messageProducer = messageProducer;
}
public async Task ConsumeAsync(InstanceCreated message, CancellationToken cancellationToken)
{
// consume message and return response
}
}
I figured I might be able to extend the ActiveMqExtensions class with a ConsumeAsync and HandleMessage that handles the response message with a return value, but I haven't gotten as far yet.
public static IActiveMqBuilder AddTypedConsumer<TMessage, TConsumer>(this IActiveMqBuilder builder,
RoutingType routingType)
where TConsumer : class, ITypedConsumer<TMessage>
{
builder.Services.TryAddScoped<TConsumer>();
builder.AddConsumer(typeof(TMessage).Name, routingType, HandleMessage<TMessage, TConsumer>);
return builder;
}
private static async Task HandleMessage<TMessage, TConsumer>(Message message, IConsumer consumer, IServiceProvider serviceProvider, CancellationToken token)
where TConsumer : class, ITypedConsumer<TMessage>
{
try
{
var msg = JsonConvert.DeserializeObject<TMessage>(message.GetBody<string>());
using var scope = serviceProvider.CreateScope();
var typedConsumer = scope.ServiceProvider.GetService<TConsumer>();
await typedConsumer.ConsumeAsync(msg, token);
await consumer.AcceptAsync(message);
}
catch(Exception ex)
{
// todo
}
}
Am I totally wrong in what I am trying to achieve here, or is it just not possible with the ArtemisNetClient?
Maybe someone has an example or can confirm whether I am down the right path, or maybe I should be using a different framework.
I am new to this kind of communication through messages like ActiveMQ Artemis, so any guidance is appreciated.
I don't see anything in the ArtemisNetClient that would simplify the request/response pattern from your application's point of view. One might expect something akin to JMS' QueueRequestor, but I don't see anything like that in the code, and I don't see anything like that listed in the documentation.
I recommend you simply do in your application what you did in your example (i.e. manually create the consumer & producer to deal with the responses on each end respectively). The only change I would recommend is to re-use connections so you create as few as possible. A connection pool would be ideal here.
For what it's worth, it looks to me like the first release of ArtemisNetClient was just 3 months ago and according to GitHub all but 2 of the commits to the code-base came from one developer. ArtemisNetClient may grow into a very successful C# client implementation, but at this point it seems relatively immature. Even if the existing code is high quality if there isn't a solid community around the client then chances are it won't have the support necessary to get timely bug fixes, new features, etc. Only time will tell.
With version 2.7.0 ArtemisNetClient introduces IRequestReplyClient interface that can be used to implement a request-response messaging pattern. With ArtemisNetClient.Extensions.DependencyInjection this may look as follows:
Client Side:
First you need to register your typed request-reply client in DI:
public void ConfigureServices(IServiceCollection services)
{
/*...*/
var endpoints = new[] { Endpoint.Create(host: "localhost", port: 5672, "guest", "guest") };
services.AddActiveMq("bookstore-cluster", endpoints)
.AddRequestReplyClient<MyRequestReplyClient>();
/*...*/
}
MyRequestReplyClient is your custom class that expects the IRequestReplyClient to be injected via the constructor. Once you have your custom class, you can either expose the IRequestReplyClient directly or encapsulate sending logic inside of it:
public class MyRequestReplyClient
{
private readonly IRequestReplyClient _requestReplyClient;
public MyRequestReplyClient(IRequestReplyClient requestReplyClient)
{
_requestReplyClient = requestReplyClient;
}
public async Task<TResponse> SendAsync<TRequest, TResponse>(TRequest request, CancellationToken cancellationToken)
{
var serialized = JsonSerializer.Serialize(request);
var address = typeof(TRequest).Name;
var msg = new Message(serialized);
var response = await _requestReplyClient.SendAsync(address, msg, cancellationToken);
return JsonSerializer.Deserialize<TResponse>(response.GetBody<string>());
}
}
That's it regarding the client-side.
Worker side
To implement the worker side you can (as you suggested), change the ITypedConsumer interface to return the message that would be sent back, or you can provide the additional data (ReplyTo and CorrelationId headers) so you can send the response back as part of your consumer logic. I prefer the latter as it's a more flexible option in my opinion.
Modified ITypedConsumer might look like that:
public interface ITypedConsumer<in T>
{
public Task ConsumeAsync(T message, MessageContext context, CancellationToken cancellationToken);
}
Where MessageContext is just a simple dto:
public class MessageContext
{
public string ReplyTo { get; init; }
public string CorrelationId { get; init; }
}
HandleMessage extension method:
private static async Task HandleMessage<TMessage, TConsumer>(Message message, IConsumer consumer, IServiceProvider serviceProvider, CancellationToken token)
where TConsumer : class, ITypedConsumer<TMessage>
{
var msg = JsonSerializer.Deserialize<TMessage>(message.GetBody<string>());
using var scope = serviceProvider.CreateScope();
var typedConsumer = scope.ServiceProvider.GetService<TConsumer>();
var messageContext = new MessageContext
{
ReplyTo = message.ReplyTo,
CorrelationId = message.CorrelationId
};
await typedConsumer.ConsumeAsync(msg, messageContext, token);
await consumer.AcceptAsync(message);
}
MessageProducer has to be slightly changed as well, so you can explicitly pass address and CorrelationId:
public class MessageProducer
{
private readonly IAnonymousProducer _producer;
public MessageProducer(IAnonymousProducer producer)
{
_producer = producer;
}
public async Task PublishAsync<T>(string address, T message, MessageContext context, CancellationToken cancellationToken)
{
var serialized = JsonSerializer.Serialize(message);
var msg = new Message(serialized);
if (!string.IsNullOrEmpty(context.CorrelationId))
{
msg.CorrelationId = context.CorrelationId;
}
await _producer.SendAsync(address, msg, cancellationToken);
}
}
And finally, the exemplary consumer could work like that:
public class CreateBookConsumer : ITypedConsumer<CreateBook>
{
private readonly MessageProducer _messageProducer;
public CreateBookConsumer(MessageProducer messageProducer)
{
_messageProducer = messageProducer;
}
public async Task ConsumeAsync(CreateBook message, MessageContext context, CancellationToken cancellationToken)
{
var #event = new BookCreated
{
Id = Guid.NewGuid(),
Title = message.Title,
Author = message.Author,
Cost = message.Cost,
InventoryAmount = message.InventoryAmount,
UserId = message.UserId,
Timestamp = DateTime.UtcNow
};
await _messageProducer.PublishAsync(context.ReplyTo, #event, new MessageContext
{
CorrelationId = context.CorrelationId
}, cancellationToken);
}
}
At runtime I get the following error:
System.InvalidOperationException: 'Cannot write message after request
is complete.'
I'm using Grpc and reactive X to get data.
public class SensorService: Protos.Vehicule.VehiculeBase
{
private readonly ILogger<SensorService> _logger;
private DataProvider _dataProvider;
private CarSim.Vehicule _vehicule;
public SensorService(ILogger<SensorService> logger)
{
_dataProvider = new DataProvider(new CarSim.Vehicule());
_vehicule = _dataProvider.getVehicule();
_logger = logger;
_dataProvider.Start();
}
public System.IObserver<CarSim.Vehicule> GetData { get; private set; }
public override async Task Status(All request, IServerStreamWriter<StatusVehicule> responseStream, ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
_vehicule.VehiculeChangedState.Subscribe(onNext: new Action<CarSim.Vehicule>(async (t) =>
{
await responseStream.WriteAsync(new StatusVehicule()
{
Camera = t.Camera,
FuelLevel = t.FuelLevel,
GunStatus = true,
Light = t.Light,
OilLevel = t.OilLevel,
Peed = t.Speed,
Tempature = t.Tempature
}
);
; }));
}
}
}
This is likely not a bug. The exception means that you're trying to send a response after the RPC has actually finished. Usually this happens when the RPC deadline is exceeded (at which point the RPC is automatically cancelled) or when it was cancelled by the client. Both of these situations can happen at any time (from the server side handler's perspective) and they are basically an inherent race condition (the RPC could have been cancelled just before you decide to send the response).
The exception is just gRPC's way of informing you that the response could not be sent (and there is really no way to send a response AFTER the RPC has finished).
You must register SensorService in your gRpc Server code:
like this:
app.UseEndpoints(endpoints => { endpoints.MapGrpcService<SensorService >(); .....
I have found a solution that works. But i still have the same error.
public override async Task Status(All request, IServerStreamWriter<StatusVehicule> responseStream, ServerCallContext context){
try
{
var eventLoop = new EventLoopScheduler();
await _vehicule.VehiculeChangedState.ObserveOn(eventLoop).ForEachAsync<CarSim.Vehicule>(t =>
{
try
{
responseStream.WriteAsync(new StatusVehicule()
{
Camera = t.Camera,
FuelLevel = t.FuelLevel,
GunStatus = true,
Light = t.Light,
OilLevel = t.OilLevel,
Peed = t.Speed,
Status = t.GetState().ToString(),
StatusDoors = t.StatusDoors.ToString(),
Tempature = t.Tempature
}
).Wait();
}
catch (Exception)
{
// ignored
}
},
context.CancellationToken);
}
catch (Exception e)
{
_logger.LogError(e.Message);
}
}
How to write to the database on a timer in the background. For example, check mail and add new letters to the database. In the example, I simplified the code just before writing to the database.
The class names from the example in Microsoft.
The recording class itself:
namespace EmailNews.Services
{
internal interface IScopedProcessingService
{
void DoWork();
}
internal class ScopedProcessingService : IScopedProcessingService
{
private readonly ApplicationDbContext _context;
public ScopedProcessingService(ApplicationDbContext context)
{
_context = context;
}
public void DoWork()
{
Mail mail = new Mail();
mail.Date = DateTime.Now;
mail.Note = "lala";
mail.Tema = "lala";
mail.Email = "lala";
_context.Add(mail);
_context.SaveChangesAsync();
}
}
}
Timer class:
namespace EmailNews.Services
{
#region snippet1
internal class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
public TimedHostedService(IServiceProvider services, ILogger<TimedHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromMinutes(1));
return Task.CompletedTask;
}
private void DoWork(object state)
{
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
scopedProcessingService.DoWork();
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
#endregion
}
Startup:
services.AddHostedService<TimedHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
It seems everything is done as in the example, but nothing is added to the database, which is not so?
This is a rather interesting question, that boils down to "How do you correctly handle an async timer callback?"
The immediate problem is that SaveChangesAsync isn't getting awaited. The DbContext almost certainly gets disposed before SaveChangesAsync has a chance to run. To await it, DoWork must become an async Task method (never async void) :
internal interface IScheduledTask
{
Task DoWorkAsync();
}
internal class MailTask : IScheduledTask
{
private readonly ApplicationDbContext _context;
public MailTask(ApplicationDbContext context)
{
_context = context;
}
public async Task DoWorkAsync()
{
var mail = new Mail
{ Date = DateTime.Now,
Note = "lala",
Tema = "lala",
Email = "lala" };
_context.Add(mail);
await _context.SaveChangesAsync();
}
}
The problem now is how to call DoWorkAsync from the timer callback. If we just call it without awaiting, we'll get the same problem we had in the first place. A timer callback can't handle methods that return Task. We can't make it async void either, because this would result in the same problem - the method will return before any async operation has a chance to finish.
David Fowler explains how to properly handle asynchronous timer callbacks in the Timer Callbacks section of his Async Guidance
article :
private readonly Timer _timer;
private readonly HttpClient _client;
public Pinger(HttpClient client)
{
_client = new HttpClient();
_timer = new Timer(Heartbeat, null, 1000, 1000);
}
public void Heartbeat(object state)
{
// Discard the result
_ = DoAsyncPing();
}
private async Task DoAsyncPing()
{
await _client.GetAsync("http://mybackend/api/ping");
}
The actual method should be async Task but the returned task only has to be assigned, not awaited, in order for it to work properly.
Applying this to the question leads to something like this :
public Task StartAsync(CancellationToken cancellationToken)
{
...
_timer = new Timer(HeartBeat, null, TimeSpan.Zero,
TimeSpan.FromMinutes(1));
return Task.CompletedTask;
}
private void Heartbeat(object state)
{
_ = DoWorkAsync();
}
private async Task DoWorkAsync()
{
using (var scope = Services.CreateScope())
{
var schedTask = scope.ServiceProvider
.GetRequiredService<IScheduledTask>();
await schedTask.DoWorkAsync();
}
}
David Fowler explains why async void is ALWAY BAD in ASP.NET Core - it's not only that async actions won't be awaited, exceptions will crash the application.
He also explains why we can't use Timer(async state=>DoWorkAsync(state)) - that's an async void delegate.
I'm trying to implement messanging through the RabbitMQ. I always heard that it's crazily fast so I gave it a try. But for some reason, It's very slow.
I have one queue called blockchain. It has several messages, message size is about ~1kb. Then I create a queue listener which basically just write a constant text in stdout:
public async Task RunAsync(CancellationToken cancellationToken)
{
// using EasyNetQ in this example.
using (var bus = RabbitHutch.CreateBus(_queueSettings.RabbitConnection)).Advanced)
{
using (var _ = await ConsumeBus(bus, _queueSettings.QueueName))
{
Console.WriteLine("Listening for messages. Hit <return> to quit.");
cancellationToken.WaitHandle.WaitOne();
}
}
}
private async Task<IDisposable> ConsumeBus(IAdvancedBus bus, string queueName)
{
var queue = await bus.QueueDeclareAsync(queueName, true).ConfigureAwait(false);
return bus.Consume(queue,
(body, properties, info) => Console.WriteLine("Got a message!"));
}
But when I watch at the queue I see that message consumption speed is only ~40 msg/sec. Comparing to 50k msg/sec written in articles over the net it seems to be very slow. When I go to the management page I see that queue is almost completely utilized:
Connection string seems to be ok too, we take large batches and process them:
"QueueSettings": {
"RabbitConnection":
"host=myhost:5672;username=reader;password=readerpass5;requestedHeartbeat=20;prefetchcount=100;timeout=1000;publisherConfirms=true",
"QueueName": "blockchain"
},
What's wrong here? How could I get this amazing number of thousands messages per second per queue? Should I deploy 1000 consumers and expect that they would have 40*1000 msg/sec?
This version is slighly faster, however, I still unable to get more than 50 msg/sec
public async Task RunAsync(CancellationToken cancellationToken)
{
var factory = GetConnectionFactory()
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
using (var _ = ConsumeBus(channel, _queueSettings.QueueName))
{
_contextlessLogger.Information("Listening for messages. Hit <return> to quit.");
cancellationToken.WaitHandle.WaitOne();
}
}
}
private IDisposable ConsumeBus(IModel bus, string queueName)
{
return new Consumer(bus, queueName, async bytes => Console.WriteLine("Got a message!"));
}
public class Consumer : IDisposable
{
private readonly IModel _channel;
private readonly Func<byte[], Task> _action;
private readonly EventingBasicConsumer _consumer;
public Consumer(IModel channel, string queueName, Func<byte[], Task> action)
{
_channel = channel;
_action = action;
_consumer = new EventingBasicConsumer(channel);
_consumer.Received += OnReceived;
channel.BasicConsume(queueName, false, _consumer);
}
private async void OnReceived(object model, BasicDeliverEventArgs ea)
{
try
{
await _action(ea.Body);
_channel.BasicAck(ea.DeliveryTag, false);
}
catch
{
_channel.BasicNack(ea.DeliveryTag, false, true);
}
}
public void Dispose()
{
_consumer.Received -= OnReceived;
}
}
Here's the entry point:
public class Program
{
private static readonly CancellationTokenSource TokenSource = new CancellationTokenSource();
public static void Main(string[] args)
{
// start the app
new Bootstrap()
.RunAsync(TokenSource.Token)
.Wait();
Console.CancelKeyPress += (sender, eventArgs) =>
{
TokenSource.CancelAfter(500);
};
}
}
Here's the bootstrap:
public class Bootstrap : IBootstrap
{
private readonly IServer server;
private readonly ILogger logger;
public Bootstrap(
IServer server,
ILogger logger)
{
this.server = server;
this.logger = logger;
}
public async Task RunAsync(CancellationToken token)
{
this.logger.Info("Application Starting...");
await this.server.StartAsync(token);
}
}
Here's the server:
public abstract class BaseServer : IServer
{
private readonly IPAddress ipAddress;
private readonly int port;
private readonly ILogger logger;
protected BaseServer(
string ipAddress,
int port,
ILogger logger)
{
this.ipAddress = IPAddress.Parse(ipAddress);
this.port = port;
this.logger = logger;
}
public async Task StartAsync(CancellationToken token)
{
this.logger.Debug("[{0}] Listening for connections using: {1}:{2}", this.GetType().Name, this.ipAddress.ToString(), this.port);
var tcpListener = new TcpListener(this.ipAddress, this.port);
tcpListener.Start();
while (!token.IsCancellationRequested)
{
await this.ServerProcessorAsync(tcpListener, token);
}
tcpListener.Stop();
Console.WriteLine("Stopped Listener");
}
public abstract Task ServerProcessorAsync(TcpListener listener, CancellationToken token);
}
Here's the Server Processor:
public class Server : BaseServer
{
private readonly ILogger logger;
public Server(
IAppConfiguration configuration,
ILogger logger)
: base(configuration.IpAddress, configuration.Port, logger)
{
this.logger = logger;
}
public override async Task ServerProcessorAsync(TcpListener listener, CancellationToken token)
{
this.logger.Debug("[{0}] Waiting for connection...", this.GetType().Name);
var client = await listener.AcceptTcpClientAsync();
this.logger.Debug("[{0}] Spawning Thread for Connection...", this.GetType().Name);
Parallel.Invoke(new ParallelOptions
{
CancellationToken = token,
MaxDegreeOfParallelism = 10000,
TaskScheduler = TaskScheduler.Current
}, () => this.ListenToClient(client));
}
private void ListenToClient(TcpClient client)
{
var threadName = Thread.CurrentThread.Name;
var bytes = new byte[2048];
var stream = client.GetStream();
int i;
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
{
var timeString = DateTime.Now.ToLongTimeString();
var currentRes = Encoding.UTF8.GetString(bytes);
var received = $"Recieved [{threadName}] [{timeString}]: {currentRes}";
this.logger.Info(received);
var responseData = Encoding.UTF8.GetBytes(received);
stream.Write(responseData, 0, responseData.Length);
}
client.Close();
}
}
Will this correctly shut the app down when ctrl+c is pressed?
Is there a way to debug this, or to know that the resources have been released properly.
I assume that the while (!token.IsCancellationRequested) will break when ctrl+c. I also assume that that if there are any threads created by Parallel.Invoke they will disposed of when the Cancel is called.
If in the case that:
Console.CancelKeyPress += (sender, eventArgs) =>
{
TokenSource.CancelAfter(500);
};
doesn't wait for things to be cleared up, is there a better way than a timeout to make sure that everything is properly cleared up?
First, you Wait on RunAsync before subscribing to Console.CancelKeyPress event, so you will subscribe to it when it's too late.
Second, cancellation token won't work anyway in your case. This line:
var client = await listener.AcceptTcpClientAsync();
Will block until new client connects, and because AcceptTcpClientAsync does not has overload which accepts CancellationToken - usage of CancellationTokens in the whole program becomes not needed. What you should do instead is stop your listener instead of cancellation. This will throw exception on the line above, which you should catch and gracefully end the task.
If you really want to continue with CancellationToken, even if it's not really needed here, consider this approach to make it work with AcceptTcpClientAsync: https://stackoverflow.com/a/14524565/5311735. This might also be good idea if you use CancellationToken to cancel many different operations not shown in your question.