I have a C# console application that uses Nancy and Nancy.Hosting.Self.
The idea is that it will serve an API via Nancy, and the main application will routinely poll a number of connections to various application + obtain data from those connections when request via the API (via Nancy).
So I will have 2 running processes, the constant polling and the HTTP server.
My Program.cs contains the following snippets.
Task pollTask = null;
try {
pollTask = Task.Run(async () => {
while (processTask) {
connectionPool.PollEvents();
await Task.Delay(configLoader.config.connectionPollDelay, wtoken.Token);
}
keepRunning = false;
}, wtoken.Token);
}
catch (AggregateException ex) {
Console.WriteLine(ex);
}
catch (System.Threading.Tasks.TaskCanceledException ex) {
Console.WriteLine("Task Cancelled");
Console.WriteLine(ex);
}
And later...
using (var host = new Nancy.Hosting.Self.NancyHost(hostConfigs, new Uri(serveUrl))) {
host.Start();
// ...
// routinely checking console for a keypress to quit which then sets
// processTask to false, which would stop the polling task, which
// in turn sets keepRunning to false which stops the application entirely.
}
The polling task seems to just die/stop without any output to the console to indicate why it stopped. In the check for the console input keypress, I also query the pollTask.Status, and it eventually details "Faulted". But I've no idea why. I'm also questioning the reliability of a long/forever running Task.
To prevent this being vague, I have one primary question. Is Task suitable for a forever running task in the manner presented above. If it is not, what should I be using instead to achieve 2 parallel proceses one of which being Nancy.
UPDATE (17/07/2018):
After taking on board the suggestions and answers thus far, I have been able to ascertain the exception that is eventually occuring and killing the process:
PollEvents process appears to be throwing an exception...
System.AggregateException: One or more errors occurred. ---> System.InvalidOperationException: There were not enough free threads in the ThreadPool to complete the operation.
at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
at System.Net.Http.HttpClientHandler.StartGettingRequestStream(RequestState state)
at System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(RequestState state)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ApiProxy.ServiceA.Connection.<>c__DisplayClass22_0.<<Heartbeat>b__0>d.MoveNext() in \api-proxy\src\ServiceA\Connection.cs:line 281
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at ApiProxy.ServiceA.Connection.Heartbeat() in \api-proxy\src\ServiceA\Connection.cs:line 274
at ApiProxy.ServiceA.Connection.PollEvents(Nullable`1 sinceEventId) in \api-proxy\src\ServiceA\Connection.cs:line 313
at ApiProxy.ConnectionPool.PollEvents() in \api-proxy\src\ConnectionPool.cs:line 50
at ApiProxy.Program.<>c.<<Main>b__5_0>d.MoveNext() in \api-proxy\Program.cs:line 172
---> (Inner Exception #0) System.InvalidOperationException: There were not enough free threads in the ThreadPool to complete the operation.
at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
at System.Net.Http.HttpClientHandler.StartGettingRequestStream(RequestState state)
at System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(RequestState state)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ApiProxy.ServiceA.Connection.<>c__DisplayClass22_0.<<Heartbeat>b__0>d.MoveNext() in \api-proxy\src\ServiceA\Connection.cs:line 281<---
Because the try...catch
is now inside the Task, it means while this error happens repeatedly and at speed, eventually it appears to rectify itself. However it would be ideal to be able to query the availability of the ThreadPool before demanding more of it. It also looks like the ThreadPool issue isn't related to my code, but instead of Nancy.
After looking up the source of the error, I've identified it happening in the following:
public bool Heartbeat() {
if (connectionConfig.events.heartbeatUrl == "") {
return false;
}
var val = false;
var task = Task.Run(async () => {
var heartbeatRequest = new HeartbeatRequest();
heartbeatRequest.host = connectionConfig.host;
heartbeatRequest.name = connectionConfig.name;
heartbeatRequest.eventId = lastEventId;
var prettyJson = JToken.Parse(JsonConvert.SerializeObject(heartbeatRequest)).ToString(Formatting.Indented);
var response = await client.PostAsync(connectionConfig.events.heartbeatUrl, new StringContent(prettyJson, Encoding.UTF8, "application/json"));
// todo: create a heartbeatResponse extending a base response type
PingResponse heartbeatResponse = JsonConvert.DeserializeObject<PingResponse>(await response.Content.ReadAsStringAsync());
if (heartbeatResponse != null) {
Console.WriteLine("Heartbeat: " + heartbeatResponse.message);
val = heartbeatResponse.success;
}
else {
// todo: sentry?
}
});
task.Wait();
return val;
}
I'm wrapping the calls in a Task
because otherwise I end up with a sea of async
definitions everywhere. Is this the possible source of the ThreadPool starvation?
UPDATE 2
Corrected the above code by removing the Task.Run
that wraps the PostAsync
. The calling code would then call Heartbeat().Wait()
, and so the method would now look like:
public async Task<bool> Heartbeat() {
if (connectionConfig.events.heartbeatUrl == "") {
return false;
}
var val = false;
var heartbeatRequest = new HeartbeatRequest();
heartbeatRequest.host = connectionConfig.host;
heartbeatRequest.name = connectionConfig.name;
heartbeatRequest.eventId = lastEventId;
var prettyJson = JToken.Parse(JsonConvert.SerializeObject(heartbeatRequest)).ToString(Formatting.Indented);
var response = await client.PostAsync(connectionConfig.events.heartbeatUrl, new StringContent(prettyJson, Encoding.UTF8, "application/json"));
PingResponse heartbeatResponse = JsonConvert.DeserializeObject<PingResponse>(await response.Content.ReadAsStringAsync());
if (heartbeatResponse != null) {
Console.WriteLine("Heartbeat: " + heartbeatResponse.message);
val = heartbeatResponse.success;
}
else {
// todo: sentry?
}
return val;
}
Hope some of this experience of mine helps others. I'm not yet sure if the above changes (there were many like this) will prevent the Thread starvation.