I have a console app that queries a database and then posts some records to a REST API in a loop (the api does not support batch posting, so I have to loop through each record and post individually, if its relevant). The database access is fast and no issue and so is the api post loop according to the timer I've put in place, however the app itself takes a long time to exit after the work is done.
This started happening after I introduced Parallel.Foreach
to speed up the posting. Before using a non-parallel loop, posting 1000 records took on average ~10mins, but the app would return and exit immediately when it was done (as expected). With the parallel loop in place, this, according to the Stopwatch
timer I'm using, is reduced to an average of ~44secs, however the app doesn't exit until around 2 minutes have passed - ~1min15sec after all work has completed.
The app isn't doing anything 'extra'. It enters main
, main
calls a method to retrieve some records from a database (1-2 secs), forwards 1000 of those records to another method that loops through them and posts each to the api, then exits. Except, it doesn't exit immediately in this case, for some reason.
I put a stopwatch
timer in main
immediately before the call to the posting method and log the time immediately after the method returns, and the timer aligns with the timer inside the method, averaging ~46 seconds. So the delay is happening after the posting method has returned but before the main
function exits, but there is nothing defined for it to do at this point. Debugging didn't show anything out of the ordinary. Is this a de-allocation issue related to all the objects spawned by the parallel loop that are 'hanging around'?
This happens regardless of whether I am running with a debugger attached or executing the binary directly when built for release (so not a detaching delay issue). I've looked at other SO questions like this but their approaches have not made a difference. Any input would be appreciated.
Code of the posting function:
public ProcessingState PostClockingRecordBatchParallel(List<ClockingEvent> batch, int tokenExpiryTolerance)
{
log.Info($"Attempting to post batch of {batch.Count.ToString()} clocking records to API with an auth token expiry tolerance of {tokenExpiryTolerance} seconds");
try
{
ProcessingState state = new ProcessingState() { PendingRecords = batch };
List<ClockingEvent> successfulRecords = new List<ClockingEvent>();
Stopwatch timer = new Stopwatch();
ServicePointManager.UseNagleAlgorithm = false; //Performance optimization related to RestSharp lib
authToken = Authenticate();
timer.Start();
Parallel.ForEach(state.PendingRecords, pr =>
{
successfulRecords.Add(PostClockingRecord(pr, tokenExpiryTolerance));
});
//Prior non-parallel version
//state.PendingRecords.ForEach(pr =>
//{
// successfulRecords.Add(PostClockingRecord(pr, tokenExpiryTolerance));
//});
state.PendingRecords = state.PendingRecords.Except(successfulRecords).ToList();
state.LastSuccessfulRecord = successfulRecords.OrderBy(r => r.EventID).Last().EventID;
log.Info($"PostClockingRecordBatchParallel - Time elapsed: {new TimeSpan(timer.ElapsedTicks).ToString()}");
return state;
}
catch (Exception ex)
{
log.Fatal($"Failed to post records to API (exception encountered: {ex}).");
throw;
}
}