0

I have a method RenderReport which generates a PDF file (byte[]). This can sometimes hang indefinitely. It should take no more than 15 seconds to complete when successful. Hence, I'm using a TaskCompletionSource to be able to limit the execution time and throw a TimeoutException if it exceeds the timeout.

However, what I can't determine is: how do you provide the byte[] file returned by RenderReport to the SetResult in the following code? longRunningTask.Wait returns a boolean and not the file so where do you get the file from?

I don't want to use longRunningTask.Result as that can introduce deadlock issues. Here's my code:

public async Task RunLongRunningTaskAsync()
{
    Task<byte[]> longRunningTask = Task.Run(() => RenderReport());
    TaskCompletionSource<byte[]> tcs = new TaskCompletionSource<byte[]>();
    Task toBeAwaited = tcs.Task;

    new Thread(() => ThreadBody(longRunningTask, tcs, 15)).Start();

    await toBeAwaited;
}

private void ThreadBody(Task<byte[]> longRunningTask, TaskCompletionSource<byte[]> tcs, int seconds)
{
    bool completed = longRunningTask.Wait(TimeSpan.FromSeconds(seconds));

    if (completed)
        // The result is a placeholder. How do you get the return value of the RenderReport()?
        tcs.SetResult(new byte[100]);
    else
        tcs.SetException(new TimeoutException("error!"));
}

private byte[] RenderReport()
{
    using (var report = new Microsoft.Reporting.WinForms.LocalReport())
    {
        // Other logic here...
        
        var file = report.Render("PDF", null, out _, out _, out _, out _, out var warnings);

        if (warnings.Any())
        {
            // Log warnings...
        }

        return file; // How do I get this file?
    }
}
Alex
  • 34,699
  • 13
  • 75
  • 158
  • 2
    "*This can sometimes hang indefinitely*" -- that bit worries me. If it hands around indefinitely, your code will simply abandon the thread. But the thread will still exist, and if it hangs indefinitely it will never exit. So you'll leak a thread (and all of the associated resources) every time this happens – canton7 May 20 '22 at 13:01
  • Thanks, @canton7. Yeah, I wish there was a way to see what's happening inside the failed `Render` call. It doesn't throw any exceptions; it just goes off into never-never land. – Alex May 20 '22 at 13:13
  • 3
    I think I'd be looking to move it to a separate process in that case: that gives you a clean, supported way to kill it and make sure that it doesn't leak anything. – canton7 May 20 '22 at 13:16
  • Thanks, @canton7. Yep, that's a good point – Alex May 20 '22 at 13:32
  • Related: [Asynchronously wait for Task to complete with timeout](https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout) – Theodor Zoulias May 20 '22 at 16:40

2 Answers2

2

You only risk deadlocks if you synchronously wait for a Task to complete.

If you know that longRunningTask has completed, it's perfectly safe to access longRunningTask.Result. So just do:

    if (completed)
        tcs.SetResult(longRunningTask.Result);
    else
        tcs.SetException(new TimeoutException("error!"));

And even then it's more complex than this: your case won't deadlock even if you do synchronously wait for longRunningTask to complete, as longRunningTask doesn't rely on any awaits. It's well worth understanding why this deadlock situation can occur.


That said, abandoning a thread which has got stuck somewhere sounds like a terrible idea: the thread will just sit around forever leaking resources, and you'll accumulate them as more reports hang, until you run out of memory.

If you really can't figure out what's going on, I'd recommend writing a small wrapper application around the report generation, and run that as a separate process. Killing processes is well-supported, and lets you ensure that nothing is leaked. You can return the bytes of the report back through standard output.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Thank you. One thing I'd like to do is determine why the silly Render hangs. I have no visibility into it as it's a Microsoft method and it throws no exceptions. – Alex May 20 '22 at 13:41
  • You should still be able to get *some* information: make sure that Just My Code is turned off, and break the debugger when it's hanging. The Stack Trace window will tell you what method the thread is in (you might need to open the Threads window and find the appropriate thread first -- double click it to put its stack in the Stack Trace window). You won't be able to view the source at that point necessarily, but the method name might give you a clue. You can decompile the DLL quite effectively in e.g. dotPeek (and newer VS versions) to see what it's doing in that method – canton7 May 20 '22 at 13:43
  • Another wrinkle: it only hangs on the servers, not on localhost. Next time it occurs, I'll check the logs for any clues. I end up bouncing IIS and that fixes it. I wonder if using the Winforms version of the reports vs. the Webforms is the issue. This is an MVC app anyway – Alex May 20 '22 at 13:45
  • 1
    Ah, fun. There are tools you can use: [dotnet-stack](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-stack) might be able to help. Or you might be able to attach windbg (with SOS) – canton7 May 20 '22 at 13:48
0

What works for me is to use ContinueWith:

This shows a first run with a successful retrieval in the specified 2-seconds and then a second run where it times out.

enter image description here

So it's just one approach, but is this helpful at all?

using System;
using System.Threading;
using System.Threading.Tasks;

namespace task_completion
{
    class Program
    {
        static void Main(string[] args)
        {
            runAsync();
            Console.ReadKey();            
        }

        static async void runAsync()
        {
            // Normal
            await longRunningByteGetter(1000, new CancellationTokenSource(2000).Token)                
                .ContinueWith((Task<byte[]> t)=> 
                {
                    switch (t.Status)
                    {
                        case TaskStatus.RanToCompletion:
                            var bytesReturned = t.Result;
                            Console.WriteLine($"Received {bytesReturned.Length} bytes");
                            break;
                        default:
                            Console.WriteLine(nameof(TimeoutException));
                            break;
                    }
                });

            // TimeOut
            await longRunningByteGetter(3000, new CancellationTokenSource(2000).Token)
                .ContinueWith((Task<byte[]> t) =>
                {
                    switch (t.Status)
                    {
                        case TaskStatus.RanToCompletion:
                            var bytesReturned = t.Result;
                            Console.WriteLine($"Received {bytesReturned.Length} bytes");
                            break;
                        default:
                            Console.WriteLine(nameof(TimeoutException));
                            break;
                    }
                });
        }

        async static Task<Byte[]> longRunningByteGetter(int delay, CancellationToken token)
        {
            await Task.Delay(delay, token); // This is a mock of your file retrieval
            return new byte[100];
        }
    }
}
IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • I don't think this is equivalent: you're cancelling `longRunningTask` (well, your `longRunningByteGetter`) using the normal cancellation method, which will stop the `Task`. OP can't do that; their `Task` can't be cancelled. `ContinueWith` has nothing to do with this: you could (and should) rewrite your code using `await` and it would work exactly the same – canton7 May 20 '22 at 13:38
  • Point taken, but I read the question as "how do you provide the byte[] file returned by RenderReport to the SetResult in the following code?" – IVSoftware May 20 '22 at 13:43
  • OP can just do that by accessing `.Result` (as your code does): no need for `ContinueWith` there either – canton7 May 20 '22 at 13:44
  • 1
    @canton7 I think you provided a great answer and I upvoted it, but I don't feel quite embarrassed enough about my own answer to remove it :) – IVSoftware May 20 '22 at 13:47
  • 1
    I still **strongly** recommend against `ContinueWith` these days: it's an incredibly complex API with a ton of subtlety (especially around the corner cases); `await` is a much cleaner and more intuitive API which can do almost everything that `ContinueWith` can do, but with far fewer gotchas. There's almost no good reason to use `ContinueWith` these days – canton7 May 20 '22 at 13:50
  • The additional explanation is helpful to me and gives me something too look at and learn from. I appreciate you providing that. – IVSoftware May 20 '22 at 13:53