Skip to content

Commit 97e9df7

Browse files
committed
fix mergeEnumerable in case of reentrancy on SubscribeAsync
1 parent b2ebbf8 commit 97e9df7

3 files changed

Lines changed: 72 additions & 13 deletions

File tree

src/R3Async.Tests/Operators/MergeEnumerableTest.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,46 @@ public async Task MergeEnumerable_Basic()
5151
results.OrderBy(x => x).ShouldBe(new[] { 1, 2, 3 });
5252
}
5353

54+
[Fact]
55+
public async Task MergeEnumerableSync_Basic()
56+
{
57+
var tcs1 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
58+
var tcs2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
59+
60+
var obs1 = AsyncObservable.Create<int>(async (observer, token) =>
61+
{
62+
await observer.OnNextAsync(1, token);
63+
await observer.OnNextAsync(2, token);
64+
await observer.OnCompletedAsync(Result.Success);
65+
tcs1.TrySetResult();
66+
return AsyncDisposable.Empty;
67+
});
68+
69+
var obs2 = AsyncObservable.Create<int>(async (observer, token) =>
70+
{
71+
await observer.OnNextAsync(3, token);
72+
await observer.OnCompletedAsync(Result.Success);
73+
tcs2.TrySetResult();
74+
return AsyncDisposable.Empty;
75+
});
76+
77+
var merge = new[] { obs1, obs2 }.Merge();
78+
var results = new List<int>();
79+
var completedTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
80+
81+
await using var subscription = await merge.SubscribeAsync(
82+
async (x, token) => results.Add(x),
83+
async (ex, token) => { },
84+
async result => completedTcs.TrySetResult(result.IsSuccess),
85+
CancellationToken.None);
86+
87+
await tcs1.Task;
88+
await tcs2.Task;
89+
await completedTcs.Task;
90+
91+
results.OrderBy(x => x).ShouldBe(new[] { 1, 2, 3 });
92+
}
93+
5494
[Fact]
5595
public async Task MergeEnumerable_Empty()
5696
{

src/R3Async/Operators/Merge.cs

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ sealed class MergeEnumerableSubscription : IAsyncDisposable
242242
readonly TaskCompletionSource<bool> _subscriptionFinished = new(TaskCreationOptions.RunContinuationsAsynchronously);
243243
readonly AsyncLocal<bool> _reentrant = new();
244244
int _active;
245-
int _disposed;
245+
bool _enumerationCompleted;
246+
bool _disposed;
246247
readonly AsyncObserver<T> _observer;
247248

248249
public MergeEnumerableSubscription(AsyncObserver<T> observer, IEnumerable<AsyncObservable<T>> sources)
@@ -261,7 +262,11 @@ public async void StartAsync()
261262
{
262263
foreach (var src in _sources)
263264
{
264-
Interlocked.Increment(ref _active);
265+
_disposedCancellationToken.ThrowIfCancellationRequested();
266+
lock (_onSomethingGate)
267+
{
268+
_active++;
269+
}
265270

266271
var innerObserver = new InnerAsyncObserver(this);
267272
await _innerDisposables.AddAsync(innerObserver);
@@ -280,7 +285,14 @@ public async void StartAsync()
280285
}
281286
}
282287

283-
if (Volatile.Read(ref _active) == 0)
288+
bool shouldComplete;
289+
lock (_onSomethingGate)
290+
{
291+
_enumerationCompleted = true;
292+
shouldComplete = _active == 0;
293+
}
294+
295+
if (shouldComplete)
284296
{
285297
await CompleteAsync(Result.Success);
286298
}
@@ -305,7 +317,7 @@ async ValueTask OnNextAsync(T value, CancellationToken token)
305317
using var linked = CancellationTokenSource.CreateLinkedTokenSource(_disposedCancellationToken, token);
306318
using (await _onSomethingGate.LockAsync())
307319
{
308-
if (_disposed == 1) return;
320+
if (_disposed) return;
309321
await _observer.OnNextAsync(value, linked.Token);
310322
}
311323
}
@@ -315,7 +327,7 @@ async ValueTask OnErrorResumeAsync(Exception ex, CancellationToken token)
315327
using var linked = CancellationTokenSource.CreateLinkedTokenSource(_disposedCancellationToken, token);
316328
using (await _onSomethingGate.LockAsync())
317329
{
318-
if (_disposed == 1) return;
330+
if (_disposed) return;
319331
await _observer.OnErrorResumeAsync(ex, linked.Token);
320332
}
321333
}
@@ -327,21 +339,28 @@ ValueTask OnCompletedAsync(Result result)
327339
return CompleteAsync(result);
328340
}
329341

330-
if (Interlocked.Decrement(ref _active) == 0)
342+
bool shouldComplete;
343+
lock (_onSomethingGate)
331344
{
332-
return CompleteAsync(Result.Success);
345+
_active--;
346+
shouldComplete = _active == 0 && _enumerationCompleted;
333347
}
334348

335-
return default;
349+
return shouldComplete ? CompleteAsync(Result.Success) : default;
336350
}
337351

338352
async ValueTask CompleteAsync(Result? result)
339353
{
340-
if (Interlocked.Exchange(ref _disposed, 1) == 1)
354+
using (await _onSomethingGate.LockAsync())
341355
{
342-
if (result?.Exception is not null and var ex)
343-
UnhandledExceptionHandler.OnUnhandledException(ex);
344-
return;
356+
if (_disposed)
357+
{
358+
if (result?.Exception is not null and var ex)
359+
UnhandledExceptionHandler.OnUnhandledException(ex);
360+
return;
361+
}
362+
363+
_disposed = true;
345364
}
346365

347366
_cts.Cancel();

src/R3Async/R3Async.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Description>Async version of R3 (Variant of async rx.net)</Description>
1212
<PackageReadmeFile>README.md</PackageReadmeFile>
1313
<RepositoryUrl>https://github.com/fedeAlterio/R3Async</RepositoryUrl>
14-
<Version>0.0.11</Version>
14+
<Version>0.0.12</Version>
1515
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1616
<PackageIcon>icon.png</PackageIcon>
1717
<PackageProjectUrl>https://github.com/fedeAlterio/R3Async</PackageProjectUrl>

0 commit comments

Comments
 (0)