Skip to content

Commit 4597201

Browse files
committed
Fix TwoWayAgent deadlock caused by wrong enum type in TaskCompletionSource constructor
TaskCompletionSource<T> was created with TaskContinuationOptions.RunContinuationsAsynchronously which silently matched the (object? state) overload instead of configuring async continuations. This caused SetResult() to run continuations inline, deadlocking when a continuation called Tell() on the same agent. Fixes #55
1 parent 0c2eccd commit 4597201

3 files changed

Lines changed: 64 additions & 2 deletions

File tree

src/Dbosoft.Functional/Agent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ await process(state, t.Item1)
138138

139139
public Task<TReply> Tell(TMsg message)
140140
{
141-
var tcs = new TaskCompletionSource<TReply>(TaskContinuationOptions.RunContinuationsAsynchronously);
141+
var tcs = new TaskCompletionSource<TReply>(TaskCreationOptions.RunContinuationsAsynchronously);
142142
_actionBlock.Post((message, tcs));
143143

144144
return tcs.Task;

test/Dbosoft.Functional.Tests/Dbosoft.Functional.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Dbosoft.Functional;
5+
using Xunit;
6+
7+
namespace Dbosoft.Functional.Tests;
8+
9+
public class TwoWayAgentTests
10+
{
11+
[Fact]
12+
public async Task Tell_from_synchronous_continuation_does_not_deadlock()
13+
{
14+
// Use the sync (non-async) TwoWayAgent constructor.
15+
// In the sync handler, SetResult is called directly on the ActionBlock thread.
16+
// If RunContinuationsAsynchronously is not properly configured on the TCS,
17+
// a synchronous continuation calling Tell() will deadlock because:
18+
// 1. SetResult runs the continuation inline on the ActionBlock thread
19+
// 2. The continuation posts a new message and waits for its result
20+
// 3. The ActionBlock can't process the new message - it's single-threaded
21+
// and the current handler hasn't returned yet
22+
var agent = Agent.Start<int, string, string>(
23+
0,
24+
(state, msg) => (state + 1, msg + "_reply"));
25+
26+
var deadlockDetected = false;
27+
var result2 = (string?)null;
28+
29+
// Attach a synchronous continuation that calls Tell on the same agent.
30+
// ExecuteSynchronously forces it to run on the thread that calls SetResult.
31+
var task = agent.Tell("msg1").ContinueWith(t =>
32+
{
33+
// This runs on the ActionBlock's thread if continuations are synchronous
34+
try
35+
{
36+
var innerTask = agent.Tell("msg2");
37+
// Use a short timeout - if this doesn't complete quickly, we're deadlocked
38+
#pragma warning disable xUnit1031 // blocking is intentional to reproduce the deadlock
39+
if (!innerTask.Wait(TimeSpan.FromSeconds(3)))
40+
#pragma warning restore xUnit1031
41+
deadlockDetected = true;
42+
else
43+
result2 = innerTask.Result;
44+
}
45+
catch (Exception)
46+
{
47+
deadlockDetected = true;
48+
}
49+
50+
return t.Result;
51+
}, TaskContinuationOptions.ExecuteSynchronously);
52+
53+
var completed = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)));
54+
55+
Assert.True(completed == task,
56+
"Outer Tell() did not complete within timeout - likely deadlocked");
57+
Assert.Equal("msg1_reply", task.Result);
58+
Assert.False(deadlockDetected,
59+
"Inner Tell() deadlocked - TaskCompletionSource is not configured with RunContinuationsAsynchronously");
60+
Assert.Equal("msg2_reply", result2);
61+
}
62+
}

0 commit comments

Comments
 (0)