This is an automated email from the ASF dual-hosted git repository.

nightowl888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git

commit 00caabd8f911d65bd60cad9a254478e25ea94e82
Author: Shad Storhaug <[email protected]>
AuthorDate: Mon Oct 18 21:06:35 2021 +0700

    BUG: 
Lucene.Net.Tests.Suggest.Spell.TestSpellChecker::TestConcurrentAccess(): Fixed 
hanging test issue due to timeouts on each thread rather than using a single 
timeout for the entire batch. Also fail the test if we go over 5 minutes, which 
will prevent us from hanging the test runner, but still give us a failure to 
investigate.
---
 .../Spell/TestSpellChecker.cs                      | 70 +++++++++++++++++-----
 1 file changed, 54 insertions(+), 16 deletions(-)

diff --git a/src/Lucene.Net.Tests.Suggest/Spell/TestSpellChecker.cs 
b/src/Lucene.Net.Tests.Suggest/Spell/TestSpellChecker.cs
index db6906d..13335d3 100644
--- a/src/Lucene.Net.Tests.Suggest/Spell/TestSpellChecker.cs
+++ b/src/Lucene.Net.Tests.Suggest/Spell/TestSpellChecker.cs
@@ -14,6 +14,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading;
+using System.Threading.Tasks;
+using Console = Lucene.Net.Util.SystemConsole;
 
 namespace Lucene.Net.Search.Spell
 {
@@ -450,7 +452,11 @@ namespace Lucene.Net.Search.Spell
          * when the spellchecker is concurrently accessed and closed.
          */
         [Test]
-        public void TestConcurrentAccess()
+        // LUCENENET: In Java, awaitTermination kills all of the threads 
forcefully after 60 seconds, which would cause a failure.
+        // We attempt to cancel the tasks gracefully after 60 seconds, but if 
they don't respond within 300 seconds it is a failure.
+        // This prevents us from hanging during testing, but still effectively 
gives us the same result.
+        [Timeout(300000)]
+        public async Task TestConcurrentAccess()
         {
             assertEquals(1, searchers.Count);
             using IndexReader r = DirectoryReader.Open(userindex);
@@ -464,14 +470,20 @@ namespace Lucene.Net.Search.Spell
             int num_field2 = this.NumDoc();
             assertEquals(num_field2, num_field1 + 1);
             int numThreads = 5 + Random.nextInt(5);
+            var tasks = new ConcurrentBag<Task>();
             SpellCheckWorker[] workers = new SpellCheckWorker[numThreads];
+            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.
+            using var shutdown = new CancellationTokenSource();
+            var cancellationToken = shutdown.Token;
             var stop = new AtomicBoolean(false);
+            var taskFactory = new TaskFactory(executor);
             for (int i = 0; i < numThreads; i++)
             {
-                SpellCheckWorker spellCheckWorker = new SpellCheckWorker(this, 
r, stop);
+                SpellCheckWorker spellCheckWorker = new SpellCheckWorker(this, 
r, stop, cancellationToken, taskNum: i);
                 workers[i] = spellCheckWorker;
-                spellCheckWorker.Start();
+                tasks.Add(taskFactory.StartNew(() => spellCheckWorker.Run(), 
cancellationToken));
             }
+
             int iterations = 5 + Random.nextInt(5);
             for (int i = 0; i < iterations; i++)
             {
@@ -482,14 +494,23 @@ namespace Lucene.Net.Search.Spell
                 // showSearchersOpen();
             }
 
-            spellChecker.Dispose();
             stop.Value = true;
-
-            // wait for 60 seconds - usually this is very fast but coverage 
runs could take quite long
-            //executor.awaitTermination(60L, TimeUnit.SECONDS);
-            foreach (SpellCheckWorker worker in workers)
+            executor.Shutdown(); // Stop allowing tasks to queue
+            try
+            {
+                // wait for 60 seconds - usually this is very fast but 
coverage runs could take quite long
+                shutdown.CancelAfter(TimeSpan.FromSeconds(60));
+                await Task.WhenAll(tasks.ToArray());
+            }
+            catch (OperationCanceledException)
+            {
+                if (Verbose)
+                    Console.WriteLine($"\n{nameof(OperationCanceledException)} 
thrown\n(safe shutdown after timeout)");
+            }
+            finally
             {
-                worker.Join((long)TimeSpan.FromSeconds(60).TotalMilliseconds);
+                shutdown.Dispose();
+                spellChecker.Dispose(); // In Lucene, this was the line that 
did "stop" and the running task responded to the AlreadyClosedException to 
break out of the loop, but we are using AtomicBoolean to signal instead.
             }
 
             for (int i = 0; i < workers.Length; i++)
@@ -542,40 +563,51 @@ namespace Lucene.Net.Search.Spell
         //    System.out.println(count);
         //  }
 
-        private class SpellCheckWorker : ThreadJob
+        private class SpellCheckWorker
         {
             private readonly TestSpellChecker outerInstance;
 
             private readonly IndexReader reader;
             private readonly AtomicBoolean stop;
+            private readonly CancellationToken cancellationToken;
+            private readonly int taskNum;
             private volatile Exception error;
             internal volatile bool terminated = false;
 
-            public SpellCheckWorker(TestSpellChecker outerInstance, 
IndexReader reader, AtomicBoolean stop)
+            public SpellCheckWorker(TestSpellChecker outerInstance, 
IndexReader reader, AtomicBoolean stop, CancellationToken cancellationToken, 
int taskNum)
             {
                 this.outerInstance = outerInstance;
                 this.reader = reader;
+                
                 this.stop = stop;
+                this.cancellationToken = cancellationToken;
+                this.taskNum = taskNum;
             }
 
             public Exception Error => error;
 
-            public override void Run()
+            public void Run()
             {
-                Priority += 1;
                 try
                 {
+                    // Was cancellation already requested?
+                    if (cancellationToken.IsCancellationRequested)
+                    {
+                        if (Verbose)
+                            Console.WriteLine("Task {0} was cancelled before 
it got started.",
+                                          taskNum);
+                        cancellationToken.ThrowIfCancellationRequested();
+                    }
+
                     while (!stop)
                     {
                         try
                         {
                             outerInstance.CheckCommonSuggestions(reader);
-
-                            Thread.Sleep(10);// don't starve refresh()'s CPU, 
which sleeps every 50 bytes for 1 ms
                         }
                         catch (Exception e) when (e.IsAlreadyClosedException())
                         {
-                            return;
+                            return; // LUCENENET: In Java, this was the "safe" 
shutdown signal, however in .NET we are shutting down proactively using 
AtomicBoolean stop
                         }
                         catch (Exception e) when (e.IsThrowable())
                         {
@@ -583,6 +615,12 @@ namespace Lucene.Net.Search.Spell
                             error = e;
                             return;
                         }
+                        if (cancellationToken.IsCancellationRequested)
+                        {
+                            if (Verbose)
+                                Console.WriteLine("Task {0} cancelled", 
taskNum);
+                            cancellationToken.ThrowIfCancellationRequested();
+                        }
                     }
                 }
                 finally

Reply via email to