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 dd89a4fcf42f76c54c44c2decae8b4811c6e43e5 Author: Shad Storhaug <[email protected]> AuthorDate: Wed Oct 13 12:35:13 2021 +0700 Lucene.Net.Support.Threading: Created UninterruptableMonitor class to handle entering locks without throwing System.Threading.ThreadInterruptedException (which is what happens in Java) --- .../Threading/TestUninterruptableMonitor.cs | 313 +++++++++++++++++++++ .../Support/Threading/UninterruptableMonitor.cs | 167 +++++++++++ 2 files changed, 480 insertions(+) diff --git a/src/Lucene.Net.Tests/Support/Threading/TestUninterruptableMonitor.cs b/src/Lucene.Net.Tests/Support/Threading/TestUninterruptableMonitor.cs new file mode 100644 index 0000000..8da4b04 --- /dev/null +++ b/src/Lucene.Net.Tests/Support/Threading/TestUninterruptableMonitor.cs @@ -0,0 +1,313 @@ +using J2N.Threading; +using J2N.Threading.Atomic; +using Lucene.Net.Attributes; +using Lucene.Net.Support.Threading; +using Lucene.Net.Util; +using NUnit.Framework; +using RandomizedTesting.Generators; +using System; +using System.Threading; +using Assert = Lucene.Net.TestFramework.Assert; + +namespace Lucene.Net.Support +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + [TestFixture] + public class TestUninterruptableMonitor : LuceneTestCase + { + private class TransactionlThreadInterrupt : ThreadJob + { + private static AtomicInt32 transactionNumber = new AtomicInt32(0); + + // Share locks between threads + private static readonly object lock1 = new object(); + private static readonly object lock2 = new object(); + private static readonly object lock3 = new object(); + + internal volatile bool failed; + internal volatile bool finish; + + internal volatile bool allowInterrupt = false; + internal volatile bool transactionInProgress = false; + + + public override void Run() + { + while (!finish) + { + try + { + TransactionalMethod(); + TransactionalMethod(); + InterruptableMethod(); + TransactionalMethod(); + InterruptableMethod(); + + // Make sure these don't throw System.Threading.ThreadInterruptedException + Assert.IsFalse(UninterruptableMonitor.IsEntered(lock1)); + Assert.IsFalse(UninterruptableMonitor.IsEntered(lock2)); + Assert.IsFalse(UninterruptableMonitor.IsEntered(lock3)); + + if (UninterruptableMonitor.TryEnter(lock1)) + { + try + { + Assert.IsTrue(UninterruptableMonitor.IsEntered(lock1)); + } + finally + { + UninterruptableMonitor.Exit(lock1); + } + } + + allowInterrupt = true; + } + catch (Util.ThreadInterruptedException re) + { + // Success - we received the correct exception type + Console.WriteLine("TEST: got interrupt"); + Console.WriteLine(GetToStringFrom(re)); + + Exception e = re.InnerException; + Assert.IsTrue(e is System.Threading.ThreadInterruptedException); + + // Make sure we didn't interrupt in the middle of a transaction + Assert.IsFalse(transactionInProgress); + + if (finish) + { + break; + } + } + catch (Exception t) when (t.IsThrowable()) + { + Console.WriteLine("FAILED; unexpected exception"); + Console.WriteLine(GetToStringFrom(t)); + + // Make sure we didn't error in the middle of a transaction + Assert.IsFalse(transactionInProgress); + + failed = true; + break; + } + } + } + + + private void TransactionalMethod() + { + Assert.IsFalse(transactionInProgress, "The prior transaction failed to complete (was interrupted)."); + transactionInProgress = true; + int transactionId = transactionNumber.IncrementAndGet(); + if (Verbose) + { + Console.WriteLine($"transaction STARTED: {transactionId}"); + } + + // Nested lock test + UninterruptableMonitor.Enter(lock1); + try + { + if (Verbose) + { + Console.WriteLine("acquired lock1 successfully"); + Console.WriteLine("sleeping..."); + } + + // Use SpinWait instead of Sleep to demonstrate the + // effect of calling Interrupt on a running thread. + Thread.SpinWait(1000000); + + UninterruptableMonitor.Enter(lock2); + try + { + if (Verbose) + { + Console.WriteLine("acquired lock2 successfully"); + Console.WriteLine("sleeping..."); + } + + // Use SpinWait instead of Sleep to demonstrate the + // effect of calling Interrupt on a running thread. + Thread.SpinWait(1000000); + } + finally + { + UninterruptableMonitor.Exit(lock2); + } + } + finally + { + UninterruptableMonitor.Exit(lock1); + } + + // inline lock test + UninterruptableMonitor.Enter(lock3); + try + { + if (Verbose) + { + Console.WriteLine("acquired lock3 successfully"); + Console.WriteLine("sleeping..."); + } + + // Use SpinWait instead of Sleep to demonstrate the + // effect of calling Interrupt on a running thread. + Thread.SpinWait(1000000); + } + finally + { + UninterruptableMonitor.Exit(lock3); + } + + if (Verbose) + { + Console.WriteLine($"transaction COMPLETED: {transactionId}"); + } + + transactionInProgress = false; + } + + private void InterruptableMethod() + { + UninterruptableMonitor.Enter(lock1); + try + { + if (Verbose) + { + Console.WriteLine("acquired lock1 successfully"); + Console.WriteLine("sleeping..."); + } + + try + { + UninterruptableMonitor.Wait(lock1, TimeSpan.FromMilliseconds(200)); + } + catch (Exception ie) when (ie.IsInterruptedException()) + { + throw new Util.ThreadInterruptedException(ie); + } + } + finally + { + UninterruptableMonitor.Exit(lock1); + } + } + + /// <summary> + /// Safely gets the ToString() of an exception while ignoring any System.Threading.ThreadInterruptedException and retrying. + /// </summary> + private string GetToStringFrom(Exception exception) + { + // Clear interrupt state: + try + { + Thread.Sleep(0); + } + catch (Exception ie) when (ie.IsInterruptedException()) + { + // ignore + } + try + { + return exception.ToString(); + } + catch (Exception ie) when (ie.IsInterruptedException()) + { + return GetToStringFrom(exception); + } + } + } + + [Test, LuceneNetSpecific] + [Slow] + public virtual void TestThreadInterrupt() + { + TransactionlThreadInterrupt t = new TransactionlThreadInterrupt(); + t.IsBackground = (true); + t.Start(); + + // issue 300 interrupts to child thread + int numInterrupts = AtLeast(300); + int i = 0; + while (i < numInterrupts) + { + // TODO: would be nice to also sometimes interrupt the + // CMS merge threads too ... + Thread.Sleep(10); + if (t.allowInterrupt) + { + i++; + t.Interrupt(); + } + if (!t.IsAlive) + { + break; + } + } + t.finish = true; + t.Join(); + + Assert.IsFalse(t.failed); + Assert.IsFalse(t.transactionInProgress); + } + + [Test, LuceneNetSpecific] + [Slow] + public virtual void TestTwoThreadsInterrupt() + { + TransactionlThreadInterrupt t1 = new TransactionlThreadInterrupt(); + t1.IsBackground = (true); + t1.Start(); + + TransactionlThreadInterrupt t2 = new TransactionlThreadInterrupt(); + t2.IsBackground = (true); + t2.Start(); + + // issue 300 interrupts to child thread + int numInterrupts = AtLeast(300); + int i = 0; + while (i < numInterrupts) + { + // TODO: would be nice to also sometimes interrupt the + // CMS merge threads too ... + Thread.Sleep(10); + TransactionlThreadInterrupt t = Random.NextBoolean() ? t1 : t2; + if (t.allowInterrupt) + { + i++; + t.Interrupt(); + } + if (!t1.IsAlive && !t2.IsAlive) + { + break; + } + } + t1.finish = true; + t2.finish = true; + t1.Join(); + t2.Join(); + + Assert.IsFalse(t1.failed); + Assert.IsFalse(t2.failed); + Assert.IsFalse(t1.transactionInProgress); + Assert.IsFalse(t2.transactionInProgress); + } + } +} \ No newline at end of file diff --git a/src/Lucene.Net/Support/Threading/UninterruptableMonitor.cs b/src/Lucene.Net/Support/Threading/UninterruptableMonitor.cs new file mode 100644 index 0000000..d145f13 --- /dev/null +++ b/src/Lucene.Net/Support/Threading/UninterruptableMonitor.cs @@ -0,0 +1,167 @@ +using System; +using System.Threading; + +namespace Lucene.Net.Support.Threading +{ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + /// <summary> + /// A drop-in replacement for <see cref="Monitor"/> that doesn't throw <see cref="ThreadInterruptedException"/> + /// when entering locks, but defers the excepetion until a wait or sleep occurs. This is to mimic the behavior in Java, + /// which does not throw when entering a lock. + /// </summary> + internal static class UninterruptableMonitor + { + public static void Enter(object obj, ref bool lockTaken) + { + // enter the lock and ignore any System.Threading.ThreadInterruptedException + try + { + Monitor.Enter(obj, ref lockTaken); + } + catch (Exception ie) when(ie.IsInterruptedException()) + { + RetryEnter(obj, ref lockTaken); + + // The lock has been obtained, now reset the interrupted status for the + // current thread + Thread.CurrentThread.Interrupt(); + } + } + + private static void RetryEnter(object obj, ref bool lockTaken) + { + try + { + // An interrupted exception may have already cleared the flag, and this will succeed without any more excpetions + Monitor.Enter(obj, ref lockTaken); + } + catch (Exception ie) when (ie.IsInterruptedException()) + { + // try again until we succeed, since an interrupt could have happened since it was cleared + RetryEnter(obj, ref lockTaken); + } + } + + public static void Enter(object obj) + { + // enter the lock and ignore any System.Threading.ThreadInterruptedException + try + { + Monitor.Enter(obj); + } + catch (Exception ie) when (ie.IsInterruptedException()) + { + RetryEnter(obj); + + // The lock has been obtained, now reset the interrupted status for the + // current thread + Thread.CurrentThread.Interrupt(); + } + } + + private static void RetryEnter(object obj) + { + try + { + // An interrupted exception may have already cleared the flag, and this will succeed without any more excpetions + Monitor.Enter(obj); + } + catch (Exception ie) when (ie.IsInterruptedException()) + { + // try again until we succeed, since an interrupt could have happened since it was cleared + RetryEnter(obj); + } + } + + public static void Exit(object obj) + { + Monitor.Exit(obj); + } + + public static bool IsEntered(object obj) + { + return Monitor.IsEntered(obj); + } + + public static bool TryEnter(object obj) + { + return Monitor.TryEnter(obj); + } + + public static void TryEnter(object obj, ref bool lockTaken) + { + Monitor.TryEnter(obj, ref lockTaken); + } + + public static bool TryEnter(object obj, int millisecondsTimeout) + { + return Monitor.TryEnter(obj, millisecondsTimeout); + } + + public static bool TryEnter(object obj, TimeSpan timeout) + { + return Monitor.TryEnter(obj, timeout); + } + + public static void TryEnter(object obj, int millisecondsTimeout, ref bool lockTaken) + { + Monitor.TryEnter(obj, millisecondsTimeout, ref lockTaken); + } + + public static void TryEnter(object obj, TimeSpan timeout, ref bool lockTaken) + { + Monitor.TryEnter(obj, timeout, ref lockTaken); + } + + public static void Pulse(object obj) + { + Monitor.Pulse(obj); + } + + public static void PulseAll(object obj) + { + Monitor.PulseAll(obj); + } + + public static void Wait(object obj) + { + Monitor.Wait(obj); + } + + public static void Wait(object obj, int millisecondsTimeout) + { + Monitor.Wait(obj, millisecondsTimeout); + } + + public static void Wait(object obj, TimeSpan timeout) + { + Monitor.Wait(obj, timeout); + } + + public static void Wait(object obj, int millisecondsTimeout, bool exitContext) + { + Monitor.Wait(obj, millisecondsTimeout, exitContext); + } + + public static void Wait(object obj, TimeSpan timeout, bool exitContext) + { + Monitor.Wait(obj, timeout, exitContext); + } + } +}
