Skip to content

Commit 98ec85e

Browse files
committed
Cancellation support for LimitedConcurrencyLevelTaskScheduler, #1253
1 parent 9a1bf2e commit 98ec85e

6 files changed

Lines changed: 80 additions & 46 deletions

File tree

src/Lucene.Net.TestFramework/Support/Threading/LimitedConcurrencyLevelTaskScheduler.cs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ to the software or derivative works that you create that run directly on a Micro
4848
Office or Microsoft Dynamics).
4949
*/
5050

51-
using J2N.Threading.Atomic;
5251
using System;
5352
using System.Collections.Generic;
5453
using System.Threading;
@@ -64,8 +63,6 @@ namespace Lucene.Net.Support.Threading
6463
/// </summary>
6564
internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
6665
{
67-
private readonly AtomicBoolean shutDown = new AtomicBoolean(false);
68-
6966
// Indicates whether the current thread is processing work items.
7067
[ThreadStatic]
7168
private static bool _currentThreadIsProcessingItems;
@@ -76,21 +73,34 @@ internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
7673
// The maximum concurrency level allowed by this scheduler.
7774
private readonly int _maxDegreeOfParallelism;
7875

76+
// A cancellation token for preventing queueing new work when shut down
77+
private readonly CancellationToken _cancellationToken;
78+
7979
// Indicates whether the scheduler is currently processing work items.
8080
private int _delegatesQueuedOrRunning = 0;
8181

82-
// Creates a new instance with the specified degree of parallelism.
83-
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
82+
/// <summary>
83+
/// Creates a new instance with the specified degree of parallelism.
84+
/// </summary>
85+
/// <param name="maxDegreeOfParallelism">The max degree of parallelism for tasks.</param>
86+
/// <param name="cancellationToken">
87+
/// A cancellation token that is used to shut down the task scheduler in an orderly manner.
88+
/// If cancellation is requested, <see cref="QueueTask"/> will not queue any more tasks.
89+
/// This behaves like <c>ExecutorService.shutdown()</c> in Java, allowing any running tasks to finish.
90+
/// </param>
91+
/// <exception cref="ArgumentOutOfRangeException">if <paramref name="maxDegreeOfParallelism"/> is less than 1.</exception>
92+
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism, CancellationToken cancellationToken = default)
8493
{
8594
if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
8695
_maxDegreeOfParallelism = maxDegreeOfParallelism;
96+
_cancellationToken = cancellationToken;
8797
}
8898

8999
// Queues a task to the scheduler.
90100
protected sealed override void QueueTask(Task task)
91101
{
92102
// Don't queue any more work.
93-
if (shutDown) return;
103+
if (_cancellationToken.IsCancellationRequested) return;
94104

95105
// Add the task to the list of tasks to be processed. If there aren't enough
96106
// delegates currently queued or running to process tasks, schedule another.
@@ -203,17 +213,13 @@ protected sealed override IEnumerable<Task> GetScheduledTasks()
203213
}
204214
}
205215

206-
/// <summary>
207-
/// Stops this TaskScheduler from queuing new tasks.
208-
/// </summary>
209-
public void Shutdown()
210-
{
211-
shutDown.Value = true;
212-
}
213-
214216
/// <summary>
215217
/// Gets a value indicating whether this TaskScheduler has been shut down.
216218
/// </summary>
217-
public bool IsShutdown => shutDown;
219+
/// <remarks>
220+
/// This simply returns whether the cancellation token provided to the constructor
221+
/// has requested cancellation.
222+
/// </remarks>
223+
public bool IsShutdown => _cancellationToken.IsCancellationRequested;
218224
}
219225
}

src/Lucene.Net.TestFramework/Util/LuceneTestCase.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,14 +2084,15 @@ public static IndexSearcher NewSearcher(IndexReader r, bool maybeWrap, bool wrap
20842084
{
20852085
int threads = 0;
20862086
LimitedConcurrencyLevelTaskScheduler ex;
2087+
var cts = new CancellationTokenSource();
20872088
if (random.NextBoolean())
20882089
{
20892090
ex = null;
20902091
}
20912092
else
20922093
{
20932094
threads = TestUtil.NextInt32(random, 1, 8);
2094-
ex = new LimitedConcurrencyLevelTaskScheduler(threads);
2095+
ex = new LimitedConcurrencyLevelTaskScheduler(threads, cts.Token);
20952096
//ex = new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<IThreadRunnable>(), new NamedThreadFactory("LuceneTestCase"));
20962097
// uncomment to intensify LUCENE-3840
20972098
// ex.prestartAllCoreThreads();
@@ -2102,7 +2103,7 @@ public static IndexSearcher NewSearcher(IndexReader r, bool maybeWrap, bool wrap
21022103
{
21032104
Console.WriteLine("NOTE: newSearcher using ExecutorService with " + threads + " threads");
21042105
}
2105-
r.AddReaderDisposedListener(new ReaderClosedListenerAnonymousClass(ex));
2106+
r.AddReaderDisposedListener(new ReaderClosedListenerAnonymousClass(cts));
21062107
}
21072108
IndexSearcher ret;
21082109
if (wrapWithAssertions)
@@ -3264,17 +3265,17 @@ public static double RandomGaussian() // LUCENENET: CA1822: Mark members as stat
32643265

32653266
private sealed class ReaderClosedListenerAnonymousClass : IReaderDisposedListener
32663267
{
3267-
private readonly LimitedConcurrencyLevelTaskScheduler ex;
3268+
private readonly CancellationTokenSource cts; // LUCENENET-specific: cancellation support
32683269

3269-
public ReaderClosedListenerAnonymousClass(LimitedConcurrencyLevelTaskScheduler ex)
3270+
public ReaderClosedListenerAnonymousClass(CancellationTokenSource cts)
32703271
{
3271-
this.ex = ex;
3272+
this.cts = cts;
32723273
}
32733274

32743275
public void OnDispose(IndexReader reader)
32753276
{
3276-
ex?.Shutdown();
3277-
//TestUtil.ShutdownExecutorService(ex);
3277+
cts.Cancel();
3278+
cts.Dispose();
32783279
}
32793280
}
32803281
}

src/Lucene.Net.Tests.Suggest/Spell/TestSpellChecker.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -472,11 +472,19 @@ public async Task TestConcurrentAccess()
472472
int numThreads = 5 + Random.nextInt(5);
473473
var tasks = new ConcurrentBag<Task>();
474474
SpellCheckWorker[] workers = new SpellCheckWorker[numThreads];
475-
var executor = new LimitedConcurrencyLevelTaskScheduler(numThreads); // LUCENENET NOTE: Not sure why in Java they decided to pass the max concurrent threads as all of the threads, but this demonstrates how to use a custom TaskScheduler in .NET.
475+
476+
// LUCENENET NOTE: Not sure why in Java they decided to pass the max concurrent threads as all of the threads, but this demonstrates how to use a custom TaskScheduler in .NET.
477+
// LUCENENET NOTE: This cancellation token/source is intentionally separate from the one below, because it is solely used as an equivalent
478+
// to ExecutorService.shutdown(), which just stops queueing new tasks.
479+
using var executorShutdown = new CancellationTokenSource();
480+
var executor = new LimitedConcurrencyLevelTaskScheduler(numThreads, executorShutdown.Token);
481+
476482
using var shutdown = new CancellationTokenSource();
477483
var cancellationToken = shutdown.Token;
484+
478485
var stop = new AtomicBoolean(false);
479486
var taskFactory = new TaskFactory(executor);
487+
480488
for (int i = 0; i < numThreads; i++)
481489
{
482490
SpellCheckWorker spellCheckWorker = new SpellCheckWorker(this, r, stop, cancellationToken, taskNum: i);
@@ -495,7 +503,11 @@ public async Task TestConcurrentAccess()
495503
}
496504

497505
stop.Value = true;
498-
executor.Shutdown(); // Stop allowing tasks to queue
506+
507+
// LUCENENET NOTE: This is technically pointless, since by this point we've already queued all the tasks.
508+
// Leaving this here for compatibility and possible future test changes that might use it properly.
509+
await executorShutdown.CancelAsync(); // Stop queueing new tasks
510+
499511
try
500512
{
501513
// wait for 60 seconds - usually this is very fast but coverage runs could take quite long
@@ -526,7 +538,7 @@ public async Task TestConcurrentAccess()
526538
AssertSearchersClosed();
527539
}
528540

529-
private void AssertLastSearcherOpen(int numSearchers)
541+
private static void AssertLastSearcherOpen(int numSearchers)
530542
{
531543
assertEquals(numSearchers, searchers.Count);
532544
IndexSearcher[] searcherArray = searchers.ToArray();
@@ -545,7 +557,7 @@ private void AssertLastSearcherOpen(int numSearchers)
545557
}
546558
}
547559

548-
private void AssertSearchersClosed()
560+
private static void AssertSearchersClosed()
549561
{
550562
foreach (IndexSearcher searcher in searchers)
551563
{

src/Lucene.Net.Tests/Search/TestIndexSearcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public virtual void TestHugeN()
134134
}
135135
}
136136

137-
// LUCENENET: .NET doesn't have a way to shut down the TaskScheduler explicitly
137+
// LUCENENET: shutdown not needed here since all searches above run synchronously
138138
//TestUtil.ShutdownExecutorService(service);
139139
}
140140

@@ -361,7 +361,7 @@ public virtual void TestCancellation_MultiThreaded_CancelDuringSearch_Throws()
361361
Assume.That(r.Leaves.Count >= 2, "Test requires a multi-segment index");
362362

363363
using CancellationTokenSource cts = new CancellationTokenSource();
364-
TaskScheduler service = new LimitedConcurrencyLevelTaskScheduler(4);
364+
TaskScheduler service = new LimitedConcurrencyLevelTaskScheduler(4); // LUCENENET NOTE: intentionally NOT passing cts.Token here since that parameter is for shutdown only, and that's not what we're testing
365365
IndexSearcher searcher = new CancelAfterFirstLeafSearcher(r, service, cts);
366366

367367
Exception ex = Assert.Catch(() => searcher.Search(new MatchAllDocsQuery(), 10, cts.Token));

src/Lucene.Net.Tests/Support/Threading/JSR166TestCase.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,8 @@ public void joinPool(TaskScheduler exec)
351351
{
352352
try
353353
{
354-
exec.Shutdown();
354+
// LUCENENET NOTE: no need to call a shutdown cancellation token here,
355+
// since any tasks would already be queued by the time we get here
355356
assertTrue(exec.AwaitTermination(TimeSpan.FromMilliseconds(LONG_DELAY_MS)));
356357
}
357358
// catch (SecurityException ok) // LUCENENET - not needed
@@ -448,6 +449,9 @@ public TaskState(TaskScheduler scheduler)
448449

449450
public void NewTask(Action action)
450451
{
452+
if (_factory.Scheduler is LimitedConcurrencyLevelTaskScheduler { IsShutdown: true })
453+
return;
454+
451455
var task = _factory.StartNew(action);
452456
_tasks.Add(task);
453457
}
@@ -517,14 +521,6 @@ public static int GetTaskCount(this TaskScheduler scheduler)
517521
return 0;
518522
}
519523

520-
public static void Shutdown(this TaskScheduler scheduler)
521-
{
522-
if (scheduler is LimitedConcurrencyLevelTaskScheduler lcl)
523-
{
524-
lcl.Shutdown();
525-
}
526-
}
527-
528524
public static bool IsTerminated(this TaskScheduler scheduler)
529525
{
530526
if (scheduler is LimitedConcurrencyLevelTaskScheduler lcl

src/Lucene.Net.Tests/Support/Threading/TestLimitedConcurrencyLevelTaskScheduler.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ public void TestGetCompletedTaskCount()
135135
AssumeTrue($"Expected 1, but got {p2.GetCompletedTaskCount()} - this may be a timing issue.", p2.GetCompletedTaskCount() == 1);
136136

137137
// LUCENENET NOTE: not catching SecurityException because that's not relevant here
138-
p2.Shutdown();
139138
joinPool(p2);
140139
}
141140

@@ -191,15 +190,32 @@ public void TestGetTaskCount()
191190
}
192191

193192
/// <summary>
194-
/// <see cref="LimitedConcurrencyLevelTaskScheduler.IsShutdown"/> is false before shutdown, true after
193+
/// Tests that a canceled token does not queue new tasks
195194
/// </summary>
196195
[Test]
197-
public void TestIsShutdown()
196+
public void TestCancellation()
198197
{
199-
var p1 = new LimitedConcurrencyLevelTaskScheduler(1);
200-
assertFalse(p1.IsShutdown);
201-
p1.Shutdown(); // LUCENENET NOTE: not catching SecurityException because that's not relevant here
202-
assertTrue(p1.IsShutdown);
198+
using var cts = new CancellationTokenSource();
199+
TaskScheduler p1 = new LimitedConcurrencyLevelTaskScheduler(1, cts.Token);
200+
201+
assertEquals(0, p1.GetTaskCount());
202+
203+
try
204+
{
205+
p1.Execute(MediumRunnable); // queue first task, not yet canceled
206+
// NOTE: no need to sleep here, we're just checking if tasks got queued
207+
assertEquals(1, p1.GetTaskCount());
208+
209+
cts.Cancel();
210+
p1.Execute(MediumRunnable); // queue second task (or try to)
211+
assertEquals(1, p1.GetTaskCount()); // cancellation should prevent queueing new tasks
212+
}
213+
catch (Exception)
214+
{
215+
unexpectedException();
216+
}
217+
218+
assertTrue(cts.Token.IsCancellationRequested);
203219
joinPool(p1);
204220
}
205221

@@ -215,7 +231,8 @@ public void TestIsShutdown()
215231
[Test]
216232
public void TestIsTerminated()
217233
{
218-
TaskScheduler p1 = new LimitedConcurrencyLevelTaskScheduler(1);
234+
using var cts = new CancellationTokenSource();
235+
TaskScheduler p1 = new LimitedConcurrencyLevelTaskScheduler(1, cts.Token);
219236
assertFalse(p1.IsTerminated());
220237

221238
try
@@ -224,7 +241,9 @@ public void TestIsTerminated()
224241
}
225242
finally
226243
{
227-
p1.Shutdown(); // LUCENENET NOTE: not catching SecurityException because that's not relevant here
244+
// LUCENENET NOTE: not catching SecurityException because that's not relevant here
245+
// LUCENENET NOTE: canceling here is of questionable utility, since no more tasks are queued
246+
cts.Cancel();
228247
}
229248

230249
try

0 commit comments

Comments
 (0)