Skip to content

Commit a376032

Browse files
authored
Fix TwoWayAgent deadlock in TaskCompletionSource constructor (#56)
* 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 * Handle Post() failure in TwoWayAgent.Tell() and remove unused using - Fault or cancel the TaskCompletionSource when ActionBlock.Post() returns false, preventing callers from hanging indefinitely on a completed/canceled agent. - Remove unused System.Threading using in test file.
1 parent 0c2eccd commit a376032

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

src/Dbosoft.Functional/Agent.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,16 @@ await process(state, t.Item1)
138138

139139
public Task<TReply> Tell(TMsg message)
140140
{
141-
var tcs = new TaskCompletionSource<TReply>(TaskContinuationOptions.RunContinuationsAsynchronously);
142-
_actionBlock.Post((message, tcs));
143-
141+
var tcs = new TaskCompletionSource<TReply>(TaskCreationOptions.RunContinuationsAsynchronously);
142+
143+
if (!_actionBlock.Post((message, tcs)))
144+
{
145+
if (_cancellationToken.IsCancellationRequested)
146+
tcs.SetCanceled();
147+
else
148+
tcs.SetException(new InvalidOperationException("The agent is no longer accepting messages."));
149+
}
150+
144151
return tcs.Task;
145152
}
146153
}

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

0 commit comments

Comments
 (0)