简体   繁体   中英

C# unhandled exception in try / catch block, detecting a freeze

Goal: Give a timeout to external code which is not tested. If my app would cause the other party to freeze, I want to know it immediately. Let's say it's very sophisticated unit test. If a test client used my app and returned in 500ms - it's passed, if not - it's failed.

So yes, I'm not going to be gentle on client, this is a crash test site. I wait 500ms, then I don't care what else client waits, I want to crash it with exception immediately, but THEN REPORT IT as a test result.

Possibly as one of many test results.

So, now, here, code:

    Console.Write("Testing");
    try {
        for (int j = 0; j < 3; j++) {
            Console.Write(".");
            for (int i = 0; i < 256; i++) {
                using (var cts = new CancellationTokenSource(500)) {
                    var token = cts.Token;
                    token.Register(() => throw new TimeoutException("FREEZE DETECTED"));
                    clients.Add(LDSCheckAsync().Result);
                }
            }
            clients.ForEach(i => i.Dispose());
            clients.Clear();
        }
        Console.WriteLine("OK.");
    }
    catch {
        clients.ForEach(i => i?.Dispose());
        Console.WriteLine("FAIL!");
    }

I designed the test to fail. But I don't get "FAIL" message, I get UNHANDLED EXCEPTION.

Why, and how to do it correctly. Before you even try do write a comment to it, please really do consider I can't use IfCancellationRequested or anything similar here. The function under the test would normally exit immediately, but since some network errors it can enter a 30 seconds freeze. It's totally unknown to me when it happens and even more unknown why. The test I make should help me diagnose it. THE FREEZE CANNOT BE CANCELED. I know that. What I want is to convert freeze into a nice catchable exception, then catch it, then log it. But when try / catch doesn't work as expected (yes, even outside VS), I'm helpless.

Since it's answered...

I made a little handy tool to make my life easier next time I would need to test code for freezing:

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

namespace Woof.DebugEx {

    public class FreezeTest {

        private readonly Action Subject;
        private int Timeout;
        private bool IsDone;

        public FreezeTest(Action subject) => Subject = subject;

        public void Test(int timeout) {
            Timeout = timeout;
            var tested = new Task(Tested, null, TaskCreationOptions.LongRunning);
            var watchdog = new Task(WatchDog, null, TaskCreationOptions.LongRunning);
            tested.Start();
            watchdog.Start();
            Task.WaitAny(tested, watchdog);
            if (!IsDone) throw new TimeoutException("FREEZE DETECTED.");
        }

        private void Tested(object state) {
            Subject();
            IsDone = true;
        }

        private void WatchDog(object state) => Thread.Sleep(Timeout);

    }

}

Now my test fragment looks like this:

    Console.Write("Testing");
    try {
        for (int j = 0; j < 8; j++) {
            Console.Write(".");
            for (int i = 0; i < 16; i++) {
                new FreezeTest(() => {
                    clients.Add(LDSCheckAsync().Result);
                }).Test(100);
            }
            clients.ForEach(i => i.Close());
            clients.Clear();
            Thread.Sleep(1000);
        }
        Console.WriteLine("OK.");
    }
    catch {
        clients.ForEach(i => i?.Dispose());
        Console.WriteLine("FAIL!");
    }

You can do it like this:

for (int i = 0; i < 256; i++) {
    var check = LDSCheckAsync();
    var idx = Task.WaitAny(Task.Delay(500), check);
    if (idx == 0) {
        throw new TimeoutException("FREEZE DETECTED");
    }
    else if (idx == 1) {
         var result = check.Result;
         // do something with it
    }
}

The reason why your current approach fails is because continuation of CancellationToken.Register runs on another thread and so your catch block cannot really catch it (and it cannot run on current thread anyway because it's blocked by Result call).

Note that Task.WaitAny will not throw any exceptions even if your task will fail, so you should expect exception to be thrown when you check result. Also keep in mind that after you have thrown your timeout exception - your main task is still running and might potentially fail in future, which might again crash your application (depending on framework version as I remember). So it's good idea to register continuation for your main task and obverve exception there, even if you don't care about it's result at this point. Something like this:

if (idx == 0) {
    check.ContinueWith(t =>
    {
         // observe, maybe do something like logging it
         var e = t.Exception;
    }, TaskContinuationOptions.OnlyOnFaulted);
    throw new TimeoutException("FREEZE DETECTED");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM