Async call exception behavior varies
mcmbsee opened this issue · 5 comments
All of these tests were performed with the PLC disconnected. I have 4 tests, and they all show different behavior. I reviewed #327, but I believe these are different issues.
var myTag = new Tag<DintPlcMapper, int>()
{
Name = $"MY_DINT_ARRAY_1000[0]",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
Timeout = TimeSpan.FromMilliseconds(1000),
};
This throws an exception. Based on documentation I think that's expected.
await myTag.ReadAsync();
This does not throw an exception. It waits for one second, then continues on.
Task.WaitAll(new[] { myTag.ReadAsync() }, 1000);
This code never times out and is stuck here forever. I would expect it to adhere to the Timeout property defined in myTag.
Task.WaitAll(new[] { myTag.ReadAsync() });
This code provides unpredictable results. Sometimes it completes with no exceptions. Other times an exception is thrown and caught in the catch block. However, when the exception is thrown seems to be random. Sometimes an exception is thrown that can only be caught by: AppDomain.CurrentDomain.UnhandledException. For the last exception type, I have attached the stacktrace. Once this exception happens the first time, it happens much more frequently afterward.
int loopCount = 0;
try
{
for (int i = 0; i < 100; i++)
{
loopCount = i;
Task.WaitAll(new[] { myTag.ReadAsync() }, 1000);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Exception on loop {loopCount}");
}
Can someone provide guidance on which of these is expected behavior, and which is a bug? I began investigating since my program crashed when the PLC was unplugged.
Hi @mcmbsee - thanks for this high quality issue report.
I'd like to first understand what issue you're seeing.
I'm running the code examples below in a brand new .NET 7 Console Application, with a Program.cs file contents as below.
I believe you're running a .NET Framework application due to the AppDomain exception - are you able to provide more details on the exact setup you've got (.NET version, application type (WPF/Console/UWP/etc)).
Scenario 1
using libplctag;
using libplctag.DataTypes;
var myTag = new Tag<DintPlcMapper, int>()
{
Name = $"MY_DINT_ARRAY_1000[0]",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
Timeout = TimeSpan.FromMilliseconds(1000),
};
await myTag.ReadAsync();
Console.WriteLine("Hello World!");
Output
Unhandled exception. System.Threading.Tasks.TaskCanceledException: A task was canceled.
at libplctag.NativeTagWrapper.InitializeAsync(CancellationToken token)
at libplctag.NativeTagWrapper.ReadAsync(CancellationToken token)
at libplctag.Tag`2.ReadAsync(CancellationToken token)
at Program.<Main>$(String[] args) in C:\Users\timyhac\proj\343\343\Program.cs:line 14
at Program.<Main>(String[] args)
Scenario 2
using libplctag;
using libplctag.DataTypes;
var myTag = new Tag<DintPlcMapper, int>()
{
Name = $"MY_DINT_ARRAY_1000[0]",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
Timeout = TimeSpan.FromMilliseconds(1000),
};
Task.WaitAll(new[] { myTag.ReadAsync() }, 1000);
Console.WriteLine("Hello World!");
Output
Unhandled exception. System.AggregateException: One or more errors occurred. (A task was canceled.)
---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Threading.Tasks.Task.GetExceptions(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.WaitAll(Task[] tasks, Int32 millisecondsTimeout)
at Program.<Main>$(String[] args) in C:\Users\timyhac\proj\343\343\Program.cs:line 14
--- End of stack trace from previous location ---
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.WaitAll(Task[] tasks, Int32 millisecondsTimeout)
at Program.<Main>$(String[] args) in C:\Users\timyhac\proj\343\343\Program.cs:line 14
Scenario 3
using libplctag;
using libplctag.DataTypes;
var myTag = new Tag<DintPlcMapper, int>()
{
Name = $"MY_DINT_ARRAY_1000[0]",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
Timeout = TimeSpan.FromMilliseconds(1000),
};
Task.WaitAll(new[] { myTag.ReadAsync() });
Console.WriteLine("Hello World!");
Output
Unhandled exception. System.AggregateException: One or more errors occurred. (A task was canceled.)
---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Threading.Tasks.Task.GetExceptions(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
at Program.<Main>$(String[] args) in C:\Users\timyhac\proj\343\343\Program.cs:line 14
--- End of stack trace from previous location ---
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.WaitAllCore(Task[] tasks, Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.WaitAll(Task[] tasks)
at Program.<Main>$(String[] args) in C:\Users\timyhac\proj\343\343\Program.cs:line 14
Scenario 4
using libplctag;
using libplctag.DataTypes;
var myTag = new Tag<DintPlcMapper, int>()
{
Name = $"MY_DINT_ARRAY_1000[0]",
Gateway = "10.10.10.10",
Path = "1,0",
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
Timeout = TimeSpan.FromMilliseconds(1000),
};
int loopCount = 0;
try
{
for (int i = 0; i < 100; i++)
{
loopCount = i;
Task.WaitAll(new[] { myTag.ReadAsync() }, 1000);
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception on loop {loopCount}");
}
Console.WriteLine("Hello World!");
Output
I ran the above code 4 times with the following output every time:
Exception on loop 0
Hello World!
I'm using a .NET Framework 4.8 Windows Forms application. I also tested using a .NET 7 Winform project and this gave the same results as my initial tests. However when I use a console application I get the same results you posted above. That doesn't make any sense to me.
That said, you can probably disregard scenario 4. I found the issue with random exceptions was due to Task.WaitAll and libplctag Timeout having the same values. If I make the Task timeout longer than the tag timeout the random exceptions stop occurring.
This might be one where it is around the details of the Task machinery. e.g. where the continuations are run, whether its an async context.
Maybe when using Task.WaitAll in the context of a WinForms app you need to explicitly call Task.Run to get the task started.
var task = Task.Run(() => myTag.ReadAsync());
Task.WaitAll(new[] { task }, 2000);
I think that explains it. I didn't realize .NET console applications are async when Top Level statements are enabled. For some reason that's different than when top level statements are disabled. So for LibPlc code, If the async functions are called from an async context and awaited, the exceptions are thrown. If they are called from a non async context, or not awaited, the exceptions are not thrown.
I suppose this makes sense, and this isn't a bug. I was under the mistaken impression that calling an async function with Task.WaitAll from a synchronous context was equivalent. Since there's no easy way to make my code synchronous, I will continue to use Task.WaitAll with a timeout.
Thanks