Skip to content

Commit 3eb35e8

Browse files
thomhurstclaude
andcommitted
feat: Add unbounded parallel processor and increase I/O concurrency limits
- Add UnboundedParallelAsyncProcessor for true unlimited parallelism - Increase IOBoundParallelAsyncProcessor default concurrency from 100-800 to 1000-4096 - Add ProcessInParallelUnbounded() methods to all builder classes - Enable scenarios like running 10,000+ unit tests truly in parallel This addresses bottlenecks reported by TUnit consumers where EnumerableAsyncProcessor was limiting parallelism and preventing all tests from starting concurrently. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1808fe7 commit 3eb35e8

12 files changed

Lines changed: 189 additions & 8 deletions

EnumerableAsyncProcessor/Builders/ActionAsyncProcessorBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ public IAsyncProcessor ProcessOneAtATime()
6262
{
6363
return new OneAtATimeAsyncProcessor(_count, _taskSelector, _cancellationTokenSource).StartProcessing();
6464
}
65+
66+
/// <summary>
67+
/// Process ALL tasks in parallel without any concurrency limits.
68+
/// WARNING: Use with caution - can overwhelm system resources with large task counts.
69+
/// Ideal for scenarios requiring maximum parallelism like running thousands of unit tests.
70+
/// </summary>
71+
/// <returns>An async processor with unbounded parallelism.</returns>
72+
public IAsyncProcessor ProcessInParallelUnbounded()
73+
{
74+
return new UnboundedParallelAsyncProcessor(_count, _taskSelector, _cancellationTokenSource).StartProcessing();
75+
}
6576

6677
#if NET6_0_OR_GREATER
6778
/// <summary>

EnumerableAsyncProcessor/Builders/ActionAsyncProcessorBuilder_1.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ public IAsyncProcessor<TOutput> ProcessOneAtATime()
6262
{
6363
return new ResultOneAtATimeAsyncProcessor<TOutput>(_count, _taskSelector, _cancellationTokenSource).StartProcessing();
6464
}
65+
66+
/// <summary>
67+
/// Process ALL tasks in parallel without any concurrency limits and return results.
68+
/// WARNING: Use with caution - can overwhelm system resources with large task counts.
69+
/// Ideal for scenarios requiring maximum parallelism like running thousands of unit tests.
70+
/// </summary>
71+
/// <returns>An async processor with unbounded parallelism that returns results.</returns>
72+
public IAsyncProcessor<TOutput> ProcessInParallelUnbounded()
73+
{
74+
return new ResultUnboundedParallelAsyncProcessor<TOutput>(_count, _taskSelector, _cancellationTokenSource).StartProcessing();
75+
}
6576

6677
#if NET6_0_OR_GREATER
6778
/// <summary>

EnumerableAsyncProcessor/Builders/ItemActionAsyncProcessorBuilder_1.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ public IAsyncProcessor ProcessOneAtATime()
6969
return new OneAtATimeAsyncProcessor<TInput>(_items, _taskSelector, _cancellationTokenSource)
7070
.StartProcessing();
7171
}
72+
73+
/// <summary>
74+
/// Process ALL items in parallel without any concurrency limits.
75+
/// WARNING: Use with caution - can overwhelm system resources with large item counts.
76+
/// Ideal for scenarios requiring maximum parallelism like running thousands of unit tests.
77+
/// </summary>
78+
/// <returns>An async processor with unbounded parallelism.</returns>
79+
public IAsyncProcessor ProcessInParallelUnbounded()
80+
{
81+
return new UnboundedParallelAsyncProcessor<TInput>(_items, _taskSelector, _cancellationTokenSource)
82+
.StartProcessing();
83+
}
7284

7385
#if NET6_0_OR_GREATER
7486
/// <summary>

EnumerableAsyncProcessor/Builders/ItemActionAsyncProcessorBuilder_2.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ public IAsyncProcessor<TOutput> ProcessOneAtATime()
6262
{
6363
return new ResultOneAtATimeAsyncProcessor<TInput, TOutput>(_items, _taskSelector, _cancellationTokenSource).StartProcessing();
6464
}
65+
66+
/// <summary>
67+
/// Process ALL items in parallel without any concurrency limits and return results.
68+
/// WARNING: Use with caution - can overwhelm system resources with large item counts.
69+
/// Ideal for scenarios requiring maximum parallelism like running thousands of unit tests.
70+
/// </summary>
71+
/// <returns>An async processor with unbounded parallelism that returns results.</returns>
72+
public IAsyncProcessor<TOutput> ProcessInParallelUnbounded()
73+
{
74+
return new ResultUnboundedParallelAsyncProcessor<TInput, TOutput>(_items, _taskSelector, _cancellationTokenSource).StartProcessing();
75+
}
6576

6677
#if NET6_0_OR_GREATER
6778
/// <summary>

EnumerableAsyncProcessor/RunnableProcessors/IOBoundParallelAsyncProcessor.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public class IOBoundParallelAsyncProcessor : AbstractAsyncProcessor
1313

1414
internal IOBoundParallelAsyncProcessor(int count, Func<Task> taskSelector, CancellationTokenSource cancellationTokenSource, int? maxConcurrency = null) : base(count, taskSelector, cancellationTokenSource)
1515
{
16-
// For I/O operations, allow much higher concurrency - default to 10x processor count or minimum 100
17-
_maxConcurrency = maxConcurrency ?? Math.Max(100, Environment.ProcessorCount * 10);
16+
// For I/O operations, allow much higher concurrency - default to 64x processor count or minimum 1000
17+
// This enables scenarios like running 10,000 unit tests in parallel
18+
_maxConcurrency = maxConcurrency ?? Math.Max(1000, Environment.ProcessorCount * 64);
1819
}
1920

2021
internal override async Task Process()

EnumerableAsyncProcessor/RunnableProcessors/IOBoundParallelAsyncProcessor_1.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public class IOBoundParallelAsyncProcessor<TInput> : AbstractAsyncProcessor<TInp
1313

1414
internal IOBoundParallelAsyncProcessor(IEnumerable<TInput> items, Func<TInput, Task> taskSelector, CancellationTokenSource cancellationTokenSource, int? maxConcurrency = null) : base(items, taskSelector, cancellationTokenSource)
1515
{
16-
// For I/O operations, allow much higher concurrency - default to 10x processor count or minimum 100
17-
_maxConcurrency = maxConcurrency ?? Math.Max(100, Environment.ProcessorCount * 10);
16+
// For I/O operations, allow much higher concurrency - default to 64x processor count or minimum 1000
17+
// This enables scenarios like running 10,000 unit tests in parallel
18+
_maxConcurrency = maxConcurrency ?? Math.Max(1000, Environment.ProcessorCount * 64);
1819
}
1920

2021
internal override async Task Process()

EnumerableAsyncProcessor/RunnableProcessors/ResultProcessors/ResultIOBoundParallelAsyncProcessor_1.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public class ResultIOBoundParallelAsyncProcessor<TOutput> : ResultAbstractAsyncP
1313

1414
internal ResultIOBoundParallelAsyncProcessor(int count, Func<Task<TOutput>> taskSelector, CancellationTokenSource cancellationTokenSource, int? maxConcurrency = null) : base(count, taskSelector, cancellationTokenSource)
1515
{
16-
// For I/O operations, allow much higher concurrency - default to 10x processor count or minimum 100
17-
_maxConcurrency = maxConcurrency ?? Math.Max(100, Environment.ProcessorCount * 10);
16+
// For I/O operations, allow much higher concurrency - default to 64x processor count or minimum 1000
17+
// This enables scenarios like running 10,000 unit tests in parallel
18+
_maxConcurrency = maxConcurrency ?? Math.Max(1000, Environment.ProcessorCount * 64);
1819
}
1920

2021
internal override async Task Process()

EnumerableAsyncProcessor/RunnableProcessors/ResultProcessors/ResultIOBoundParallelAsyncProcessor_2.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public class ResultIOBoundParallelAsyncProcessor<TInput, TOutput> : ResultAbstra
1313

1414
internal ResultIOBoundParallelAsyncProcessor(IEnumerable<TInput> items, Func<TInput, Task<TOutput>> taskSelector, CancellationTokenSource cancellationTokenSource, int? maxConcurrency = null) : base(items, taskSelector, cancellationTokenSource)
1515
{
16-
// For I/O operations, allow much higher concurrency - default to 10x processor count or minimum 100
17-
_maxConcurrency = maxConcurrency ?? Math.Max(100, Environment.ProcessorCount * 10);
16+
// For I/O operations, allow much higher concurrency - default to 64x processor count or minimum 1000
17+
// This enables scenarios like running 10,000 unit tests in parallel
18+
_maxConcurrency = maxConcurrency ?? Math.Max(1000, Environment.ProcessorCount * 64);
1819
}
1920

2021
internal override async Task Process()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using EnumerableAsyncProcessor.RunnableProcessors.ResultProcessors.Abstract;
2+
3+
namespace EnumerableAsyncProcessor.RunnableProcessors.ResultProcessors;
4+
5+
/// <summary>
6+
/// A specialized parallel processor that starts ALL tasks immediately without any concurrency limits and returns results.
7+
/// WARNING: Use with caution - this can overwhelm system resources with large task counts.
8+
/// Ideal for scenarios where you need maximum parallelism and have sufficient resources.
9+
/// </summary>
10+
public class ResultUnboundedParallelAsyncProcessor<TOutput> : ResultAbstractAsyncProcessor<TOutput>
11+
{
12+
internal ResultUnboundedParallelAsyncProcessor(int count, Func<Task<TOutput>> taskSelector, CancellationTokenSource cancellationTokenSource) : base(count, taskSelector, cancellationTokenSource)
13+
{
14+
}
15+
16+
internal override Task Process()
17+
{
18+
// Start ALL tasks immediately without any throttling
19+
// This provides true unbounded parallelism
20+
var tasks = TaskWrappers.Select(taskWrapper =>
21+
{
22+
var task = taskWrapper.Process(CancellationToken);
23+
// Fast-path for already completed tasks
24+
if (task.IsCompleted)
25+
{
26+
return task;
27+
}
28+
return task;
29+
});
30+
31+
return Task.WhenAll(tasks);
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using EnumerableAsyncProcessor.RunnableProcessors.ResultProcessors.Abstract;
2+
3+
namespace EnumerableAsyncProcessor.RunnableProcessors.ResultProcessors;
4+
5+
/// <summary>
6+
/// A specialized parallel processor that starts ALL tasks immediately without any concurrency limits and returns results.
7+
/// WARNING: Use with caution - this can overwhelm system resources with large task counts.
8+
/// Ideal for scenarios where you need maximum parallelism and have sufficient resources.
9+
/// </summary>
10+
public class ResultUnboundedParallelAsyncProcessor<TInput, TOutput> : ResultAbstractAsyncProcessor<TInput, TOutput>
11+
{
12+
internal ResultUnboundedParallelAsyncProcessor(IEnumerable<TInput> items, Func<TInput, Task<TOutput>> taskSelector, CancellationTokenSource cancellationTokenSource) : base(items, taskSelector, cancellationTokenSource)
13+
{
14+
}
15+
16+
internal override Task Process()
17+
{
18+
// Start ALL tasks immediately without any throttling
19+
// This provides true unbounded parallelism
20+
var tasks = TaskWrappers.Select(taskWrapper =>
21+
{
22+
var task = taskWrapper.Process(CancellationToken);
23+
// Fast-path for already completed tasks
24+
if (task.IsCompleted)
25+
{
26+
return task;
27+
}
28+
return task;
29+
});
30+
31+
return Task.WhenAll(tasks);
32+
}
33+
}

0 commit comments

Comments
 (0)