lprun hangs when dumping an unfinished Task

Hi,
I'd like to report a possible bug with lprun. It seems that it is possible to indefinitely hang a thread by dumping an unfinished Task. I managed to reproduce it using the code below. It works in LinqPad(v5.40.00), but not via lprun.
async Task Main() {
    var tcs = new TaskCompletionSource();
    
    // Not awaited, so execution should continue
    // Comment to make the script finish even in lprun
    tcs.Task.Dump();

    tcs.SetResult(true);
    
    "done".Dump();
}
In my case I initially discovered this when I was making async network calls. I had one Task reading from the network, and another writing. When the writing side was blocked by dumping a task, even the reading side got blocked. Took a while to figure that out.
This is probably due to the limitation of the console, and wanting the result of the finished Task to appear where it was initially dumped? But I think that in this case this leads to the undesireable behaviour of breaking the script.
Is this a real bug, or has this been already documented somewhere, and I just haven't read the manual?

Comments

  • Not sure what you're expecting but it's not an issue with LINQPad but with your code. Why would you be dumping the task of a TaskCompletionSource prior to triggering what would update the task anyway?

    If you were to dump a task (synchronously), it would try to output the result of that task. And since nothing will be able to update that, there's no way it will be able to complete.

    The problem would be in your script. Sounds like you have a producer/consumer setup and your producers are getting blocked somewhere.
  • When you dump a task in LPRun mode, it waits until the task completes before proceeding. This is because LPRun streams results out to the Console, so it doesn't have any way to go back and update the task placeholder asynchronously when it completes.
  • Yeah. I understand the reason why that is happening, but find this behaviour unsatisfactory, as it makes the scripts behave differently between LINQPad and LPRun.
    If you are interested, I made a larger test case that simulates the problem I was having. In this test case the result of the Task is set asynchronously via another Task, instead of doing it in the main flow. When this is executed under LPRun, the network task never prints "Result set". Instead the whole program, including the network Task on the background, gets blocked Dumping the result of the second network call.
    I understand that ultimately this is the result of mixing blocking IO with async stuff, but it came as a surprise, as the same code doesn't block in LINQPad. This makes it harder to write code that can be trusted to run succesfully in LPRun after testing it with just LINQPad.
    In my opinion this is a problem, but I don't really have a good solution to propose. Maybe there could be DumpAsync, but it would be a pain to use it. Maybe there could be a command line option to not block on dumping unfinished Tasks and risk making the output more messy?
    I'm sorry this a bit long:
    async Task Main() {
    	var replyTasks = new ConcurrentQueue<TaskCompletionSource<bool>>();
    	
    	// A Task simulating the reading side of a socket connection.
    	// This completes the reply tasks as results arrive from network.
    	// This is obviously simplified.
    	bool done = false;
    	async Task simulatedNetworkReplyWorkerAsync() {
    		while (!done) {
    			"[NET] Checking for work".Dump();
    			if (replyTasks.TryDequeue(out var tcs)) {
    				"[NET] Got work".Dump();
    				await Task.Delay(1000);
    				"[NET] Work done".Dump();
    				tcs.SetResult(true);
    				"[NET] Result set".Dump();
    			} else {
    				await Task.Delay(100);
    			}
    		}
    	}
    
    	// Start the network worker on background
    	var networkTask = simulatedNetworkReplyWorkerAsync();
    
    	async Task<bool> makeNetworkCallAsync() {
    		"[REQ] Sending request".Dump();
    		var tcs = new TaskCompletionSource<bool>();
    		replyTasks.Enqueue(tcs);
    		
    		"[REQ] Waiting for Task".Dump();
    		var r = await tcs.Task;
    		"[REQ] Got result".Dump();
    		return r;
    	}
    	
    	// Make two network calls.
    	// Wait until the first one is done before the next.
    	await makeNetworkCallAsync();
    	"[MAIN] First network call done".Dump();
    	
    	// Comment out the Dump statement to make the script finish under lprun
    	await makeNetworkCallAsync().Dump("[MAIN]");
    	
    	// Stop network
    	done = true;
    	await networkTask;
    	
    	"[MAIN] done".Dump();
    }
Sign In or Register to comment.