Skip to content

Commit f63be53

Browse files
committed
Release 5.25.2
1 parent d6a3ba5 commit f63be53

File tree

9 files changed

+112
-90
lines changed

9 files changed

+112
-90
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Release Notes
22
====
33

4+
# 09-29-2025
5+
<a href="https://www.nuget.org/packages/dotnext.threading/5.25.2">DotNext.Threading 5.25.2</a>
6+
* Fixed [272](https://github.com/dotnet/dotNext/pull/272)
7+
8+
<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.25.2">DotNext.Net.Cluster 5.25.2</a>
9+
* Forced upgrade to newer `DotNext.Threading` library
10+
11+
<a href="https://www.nuget.org/packages/dotnext.aspnetcore.cluster/5.25.2">DotNext.AspNetCore.Cluster 5.25.2</a>
12+
* Forced upgrade to newer `DotNext.Threading` library
13+
414
# 09-15-2025
515
<a href="https://www.nuget.org/packages/dotnext/5.25.0">DotNext 5.25.0</a>
616
* Added `CatchException` extension method to capture the exception produced by `await` operator instead of raising it at the call site

README.md

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,16 @@ All these things are implemented in 100% managed code on top of existing .NET AP
4444
* [NuGet Packages](https://www.nuget.org/profiles/rvsakno)
4545

4646
# What's new
47-
Release Date: 09-15-2025
47+
Release Date: 09-29-2025
4848

49-
<a href="https://www.nuget.org/packages/dotnext/5.25.0">DotNext 5.25.0</a>
50-
* Added `CatchException` extension method to capture the exception produced by `await` operator instead of raising it at the call site
51-
* Various performance improvements
49+
<a href="https://www.nuget.org/packages/dotnext.threading/5.25.2">DotNext.Threading 5.25.2</a>
50+
* Fixed [272](https://github.com/dotnet/dotNext/pull/272)
5251

53-
<a href="https://www.nuget.org/packages/dotnext.metaprogramming/5.25.0">DotNext.Metaprogramming 5.25.0</a>
54-
* Fixed mutability modifiers for properties of `UnmanagedMemory<T>` type
52+
<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.25.2">DotNext.Net.Cluster 5.25.2</a>
53+
* Forced upgrade to newer `DotNext.Threading` library
5554

56-
<a href="https://www.nuget.org/packages/dotnext.unsafe/5.25.0">DotNext.Unsafe 5.25.0</a>
57-
* Updated dependencies
58-
59-
<a href="https://www.nuget.org/packages/dotnext.threading/5.25.0">DotNext.Threading 5.25.0</a>
60-
* Added optional hard concurrency limit for async lock primitives
61-
* Rewritten the internal engine for async lock primitives to decrease the lock contention and increase the response time
62-
* Async lock primitive no longer produce lock contention time to improve the response time
63-
64-
<a href="https://www.nuget.org/packages/dotnext.io/5.25.0">DotNext.IO 5.25.0</a>
65-
* Updated dependencies
66-
67-
<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.25.0">DotNext.Net.Cluster 5.25.0</a>
68-
* Updated dependencies
69-
70-
<a href="https://www.nuget.org/packages/dotnext.aspnetcore.cluster/5.25.0">DotNext.AspNetCore.Cluster 5.25.0</a>
71-
* Updated dependencies
72-
73-
<a href="https://www.nuget.org/packages/dotnext.maintenanceservices/0.6.0">DotNext.MaintenanceServices 0.6.0</a>
74-
* Upgrade to newer `System.CommandLine` library
75-
* Interactive session now prints `>` in the prompt
76-
* Fixed buffer leak caused by interactive session
55+
<a href="https://www.nuget.org/packages/dotnext.aspnetcore.cluster/5.25.2">DotNext.AspNetCore.Cluster 5.25.2</a>
56+
* Forced upgrade to newer `DotNext.Threading` library
7757

7858
Changelog for previous versions located [here](./CHANGELOG.md).
7959

src/DotNext.Tests/Threading/CancellationTokenMultiplexerTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ public static void CheckPooling()
4040
}
4141
}
4242

43+
[Fact]
44+
public async Task CheckPoolingNonInterference()
45+
{
46+
var multiplexer = new CancellationTokenMultiplexer();
47+
48+
using var cts = new CancellationTokenSource();
49+
50+
await multiplexer.Combine([cts.Token, cts.Token, cts.Token]).DisposeAsync();
51+
52+
// same source is reused from pool, but should now not be associated with cts.
53+
await using var combined = multiplexer.Combine([new(), new(), new()]);
54+
55+
cts.Cancel();
56+
57+
False(combined.Token.IsCancellationRequested);
58+
}
59+
4360
[Fact]
4461
public static void ExtraListOverflow()
4562
{

src/DotNext.Threading/DotNext.Threading.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<ImplicitUsings>true</ImplicitUsings>
88
<IsAotCompatible>true</IsAotCompatible>
99
<Features>nullablePublicOnly</Features>
10-
<VersionPrefix>5.25.1</VersionPrefix>
10+
<VersionPrefix>5.25.2</VersionPrefix>
1111
<VersionSuffix></VersionSuffix>
1212
<Authors>.NET Foundation and Contributors</Authors>
1313
<Product>.NEXT Family of Libraries</Product>

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.CTS.cs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Diagnostics;
22
using System.Runtime.CompilerServices;
3-
using System.Runtime.InteropServices;
43

54
namespace DotNext.Threading;
65

@@ -13,46 +12,54 @@ private sealed class PooledCancellationTokenSource : LinkedCancellationTokenSour
1312
private static readonly int InlinedListCapacity = GetCapacity<InlinedTokenList>();
1413

1514
private InlinedTokenList inlinedList;
16-
private int inlinedTokenCount;
17-
private List<CancellationTokenRegistration>? extraTokens;
15+
private int count;
16+
private CancellationTokenRegistration[]? extraTokens;
1817
internal PooledCancellationTokenSource? Next;
1918

20-
public void Add(CancellationToken token)
21-
=> Add(Attach(token));
22-
23-
private void Add(CancellationTokenRegistration registration)
19+
public void AddRange(ReadOnlySpan<CancellationToken> tokens)
2420
{
25-
if (inlinedTokenCount < InlinedListCapacity)
21+
// register inlined tokens
22+
var inlinedRegistrations = inlinedList.AsSpan();
23+
var inlinedCount = Math.Min(inlinedRegistrations.Length, tokens.Length);
24+
25+
for (var i = 0; i < inlinedCount; i++)
2626
{
27-
Unsafe.Add(ref FirstInlinedRegistration, inlinedTokenCount++) = registration;
27+
inlinedRegistrations[i] = Attach(tokens[i]);
2828
}
29-
else
29+
30+
// register extra tokens
31+
tokens = tokens.Slice(inlinedCount);
32+
count = inlinedCount + tokens.Length;
33+
if (tokens.IsEmpty)
34+
return;
35+
36+
if (extraTokens is null || extraTokens.Length < tokens.Length)
3037
{
31-
extraTokens ??= new();
32-
extraTokens.Add(registration);
38+
extraTokens = new CancellationTokenRegistration[tokens.Length];
3339
}
34-
}
3540

36-
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
37-
private ref CancellationTokenRegistration FirstInlinedRegistration
38-
=> ref Unsafe.As<InlinedTokenList, CancellationTokenRegistration>(ref inlinedList);
41+
for (var i = 0; i < tokens.Length; i++)
42+
{
43+
extraTokens[i] = Attach(tokens[i]);
44+
}
45+
}
3946

40-
public int Count => inlinedTokenCount + extraTokens?.Count ?? 0;
47+
public int Count => count;
4148

42-
public ref CancellationTokenRegistration this[int index]
49+
public ref readonly CancellationTokenRegistration this[int index]
4350
{
4451
get
4552
{
46-
Debug.Assert((uint)index < (uint)Count);
47-
53+
Debug.Assert((uint)index < (uint)count);
54+
4855
Span<CancellationTokenRegistration> registrations;
4956
if (index < InlinedListCapacity)
5057
{
5158
registrations = inlinedList.AsSpan();
5259
}
5360
else
5461
{
55-
registrations = CollectionsMarshal.AsSpan(extraTokens);
62+
registrations = extraTokens;
5663
index -= InlinedListCapacity;
5764
}
5865

@@ -62,9 +69,14 @@ public ref CancellationTokenRegistration this[int index]
6269

6370
public void Reset()
6471
{
65-
inlinedTokenCount = 0;
6672
inlinedList = default;
67-
extraTokens?.Clear();
73+
74+
if (extraTokens is not null && count > InlinedListCapacity)
75+
{
76+
Array.Clear(extraTokens, 0, count - InlinedListCapacity);
77+
}
78+
79+
count = 0;
6880
}
6981

7082
private static int GetCapacity<T>()

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.Scope.cs

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,13 @@ partial class CancellationTokenMultiplexer
2222

2323
internal Scope(CancellationTokenMultiplexer multiplexer, ReadOnlySpan<CancellationToken> tokens)
2424
{
25-
switch (tokens)
26-
{
27-
case []:
28-
source = null;
29-
multiplexerOrToken = InlineToken(new(canceled: false));
30-
break;
31-
case [var token]:
32-
source = null;
33-
multiplexerOrToken = InlineToken(token);
34-
break;
35-
case [var token1, var token2]:
36-
source = null;
37-
if (!token1.CanBeCanceled || token1 == token2)
38-
{
39-
multiplexerOrToken = InlineToken(token2);
40-
}
41-
else if (!token2.CanBeCanceled)
42-
{
43-
multiplexerOrToken = InlineToken(token1);
44-
}
45-
else
46-
{
47-
goto default;
48-
}
49-
50-
break;
51-
default:
52-
multiplexerOrToken = new(multiplexer);
53-
source = multiplexer.Rent(tokens);
54-
break;
55-
}
25+
multiplexerOrToken = new(multiplexer);
26+
source = multiplexer.Rent(tokens);
5627
}
5728

29+
internal Scope(CancellationToken token)
30+
=> multiplexerOrToken = InlineToken(token);
31+
5832
private static ValueTuple<object> InlineToken(CancellationToken token)
5933
=> CanInlineToken ? Unsafe.BitCast<CancellationToken, ValueTuple<object>>(token) : new(token);
6034

src/DotNext.Threading/Threading/CancellationTokenMultiplexer.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,38 @@ public int MaximumRetained
3030
/// <param name="tokens">The tokens to be combined.</param>
3131
/// <returns>The scope that contains a single multiplexed token.</returns>
3232
public Scope Combine(ReadOnlySpan<CancellationToken> tokens) // TODO: use params
33-
=> new(this, tokens);
33+
{
34+
Scope scope;
35+
switch (tokens)
36+
{
37+
case []:
38+
scope = new();
39+
break;
40+
case [var token]:
41+
scope = new(token);
42+
break;
43+
case [var token1, var token2]:
44+
if (!token1.CanBeCanceled || token1 == token2)
45+
{
46+
scope = new(token2);
47+
}
48+
else if (!token2.CanBeCanceled)
49+
{
50+
scope = new(token1);
51+
}
52+
else
53+
{
54+
goto default;
55+
}
56+
57+
break;
58+
default:
59+
scope = new(this, tokens);
60+
break;
61+
}
62+
63+
return scope;
64+
}
3465

3566
private void Return(PooledCancellationTokenSource source)
3667
{
@@ -87,11 +118,9 @@ private PooledCancellationTokenSource Rent(ReadOnlySpan<CancellationToken> token
87118
var source = Rent();
88119
Debug.Assert(source.Count is 0);
89120

90-
foreach (var token in tokens)
91-
{
92-
source.Add(token);
93-
}
94-
121+
source.AddRange(tokens);
122+
Debug.Assert(source.Count == tokens.Length);
123+
95124
return source;
96125
}
97126
}

src/cluster/DotNext.AspNetCore.Cluster/DotNext.AspNetCore.Cluster.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<ImplicitUsings>true</ImplicitUsings>
99
<IsAotCompatible>true</IsAotCompatible>
1010
<Features>nullablePublicOnly</Features>
11-
<VersionPrefix>5.25.0</VersionPrefix>
11+
<VersionPrefix>5.25.2</VersionPrefix>
1212
<VersionSuffix></VersionSuffix>
1313
<Authors>.NET Foundation and Contributors</Authors>
1414
<Product>.NEXT Family of Libraries</Product>

src/cluster/DotNext.Net.Cluster/DotNext.Net.Cluster.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Nullable>enable</Nullable>
99
<IsAotCompatible>true</IsAotCompatible>
1010
<Features>nullablePublicOnly</Features>
11-
<VersionPrefix>5.25.0</VersionPrefix>
11+
<VersionPrefix>5.25.2</VersionPrefix>
1212
<VersionSuffix></VersionSuffix>
1313
<Authors>.NET Foundation and Contributors</Authors>
1414
<Product>.NEXT Family of Libraries</Product>

0 commit comments

Comments
 (0)