diff --git a/conf/requirements.txt b/conf/requirements.txt index a745a5c..3ab6772 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -20,7 +20,6 @@ paramiko==3.4.0 python-engineio==4.12.0 python-socketio==5.13.0 qrcode==8.2 -rwlock==0.0.7 tzdata websockify==0.13.0 whitenoise==6.9.0 diff --git a/vrtManager/rwlock.py b/vrtManager/rwlock.py index a32903f..694a6d8 100644 --- a/vrtManager/rwlock.py +++ b/vrtManager/rwlock.py @@ -11,7 +11,7 @@ found at: http://code.activestate.com/recipes/502283-read-write-lock-class-rlock # Imports # ------- -from threading import Condition, Lock, currentThread +from threading import Condition, Lock, current_thread from time import time # Read write lock @@ -71,7 +71,7 @@ class ReadWriteLock(object): if timeout is not None: endtime = time() + timeout - me = currentThread() + me = current_thread() self.__condition.acquire() try: if self.__writer is me: @@ -121,7 +121,7 @@ class ReadWriteLock(object): if timeout is not None: endtime = time() + timeout - me, upgradewriter = currentThread(), False + me, upgradewriter = current_thread(), False self.__condition.acquire() try: if self.__writer is me: @@ -196,7 +196,7 @@ class ReadWriteLock(object): In case the current thread holds no lock, a ValueError is thrown.""" - me = currentThread() + me = current_thread() self.__condition.acquire() try: if self.__writer is me: @@ -206,7 +206,7 @@ class ReadWriteLock(object): # No more write locks; take our writer position away and # notify waiters of the new circumstances. self.__writer = None - self.__condition.notifyAll() + self.__condition.notify_all() elif me in self.__readers: # We are a reader currently, take one nesting depth away. self.__readers[me] -= 1 @@ -216,7 +216,7 @@ class ReadWriteLock(object): if not self.__readers: # No more readers, notify waiters of the new # circumstances. - self.__condition.notifyAll() + self.__condition.notify_all() else: raise ValueError("Trying to release unheld lock") finally: diff --git a/vrtManager/test_rwlock.py b/vrtManager/test_rwlock.py new file mode 100644 index 0000000..1fbcf30 --- /dev/null +++ b/vrtManager/test_rwlock.py @@ -0,0 +1,199 @@ +import threading +import time +import unittest +import warnings + +from vrtManager.rwlock import ReadWriteLock + + +class TestReadWriteLock(unittest.TestCase): + def setUp(self): + self.lock = ReadWriteLock() + + def test_single_thread_read_lock(self): + """Test basic read lock acquire and release in a single thread.""" + self.lock.acquireRead() + self.lock.release() + + def test_single_thread_write_lock(self): + """Test basic write lock acquire and release in a single thread.""" + self.lock.acquireWrite() + self.lock.release() + + def test_nested_read_locks(self): + """Test reentrant read locks by the same thread.""" + self.lock.acquireRead() + self.lock.acquireRead() + self.lock.release() + self.lock.release() + + def test_nested_write_locks(self): + """Test reentrant write locks by the same thread.""" + self.lock.acquireWrite() + self.lock.acquireWrite() + self.lock.release() + self.lock.release() + + def test_release_unheld_lock_raises_value_error(self): + """Releasing a lock when none is held should raise ValueError.""" + with self.assertRaises(ValueError): + self.lock.release() + + def test_read_to_write_upgrade(self): + """A thread holding a read lock can upgrade to a write lock.""" + self.lock.acquireRead() + self.lock.acquireWrite(timeout=1.0) + # Should now hold write lock with nested count + self.lock.release() + self.lock.release() + + def test_multiple_concurrent_readers(self): + """Multiple threads should be able to acquire read locks simultaneously.""" + active_readers = 0 + max_concurrent_readers = 0 + state_lock = threading.Lock() + barrier = threading.Barrier(5) + + def reader_task(): + nonlocal active_readers, max_concurrent_readers + barrier.wait() + self.lock.acquireRead(timeout=2.0) + try: + with state_lock: + active_readers += 1 + if active_readers > max_concurrent_readers: + max_concurrent_readers = active_readers + time.sleep(0.05) + finally: + with state_lock: + active_readers -= 1 + self.lock.release() + + threads = [threading.Thread(target=reader_task) for _ in range(5)] + for t in threads: + t.start() + for t in threads: + t.join() + + self.assertEqual(max_concurrent_readers, 5) + + def test_writer_excludes_readers(self): + """While a writer holds the lock, no reader should be able to acquire it.""" + reader_acquired = False + reader_timed_out = False + + self.lock.acquireWrite() + + def reader_task(): + nonlocal reader_acquired, reader_timed_out + try: + self.lock.acquireRead(timeout=0.1) + reader_acquired = True + self.lock.release() + except RuntimeError: + reader_timed_out = True + + t = threading.Thread(target=reader_task) + t.start() + t.join() + + self.lock.release() + self.assertFalse(reader_acquired) + self.assertTrue(reader_timed_out) + + def test_readers_exclude_writer(self): + """While readers hold the lock, a writer should be blocked until released.""" + writer_acquired = False + writer_timed_out = False + + self.lock.acquireRead() + + def writer_task(): + nonlocal writer_acquired, writer_timed_out + try: + self.lock.acquireWrite(timeout=0.1) + writer_acquired = True + self.lock.release() + except RuntimeError: + writer_timed_out = True + + t = threading.Thread(target=writer_task) + t.start() + t.join() + + self.lock.release() + self.assertFalse(writer_acquired) + self.assertTrue(writer_timed_out) + + def test_writer_blocks_subsequent_readers(self): + """Pending writers take priority and should block subsequent new readers.""" + order = [] + lock_held_event = threading.Event() + writer_ready_event = threading.Event() + + # Step 1: Initial reader holds read lock + self.lock.acquireRead() + + # Step 2: Writer attempts to acquire write lock and waits + def writer_task(): + writer_ready_event.set() + self.lock.acquireWrite(timeout=2.0) + try: + order.append("writer") + finally: + self.lock.release() + + # Step 3: New reader attempts to acquire read lock after writer is pending + def new_reader_task(): + writer_ready_event.wait() + # small delay to ensure writer is queued in pendingwriters + time.sleep(0.05) + self.lock.acquireRead(timeout=2.0) + try: + order.append("new_reader") + finally: + self.lock.release() + + w_thread = threading.Thread(target=writer_task) + r_thread = threading.Thread(target=new_reader_task) + + w_thread.start() + r_thread.start() + + writer_ready_event.wait() + time.sleep(0.1) + + # Release initial reader: writer should acquire and complete before new reader + self.lock.release() + + w_thread.join() + r_thread.join() + + self.assertEqual(order, ["writer", "new_reader"]) + + def test_no_deprecation_warnings(self): + """Verify that lock operations do not emit DeprecationWarnings (e.g. currentThread, notifyAll).""" + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always", DeprecationWarning) + lock = ReadWriteLock() + lock.acquireRead() + lock.release() + lock.acquireWrite() + lock.release() + + # Check for currentThread or notifyAll warnings + deprecations = [ + str(w.message) + for w in caught_warnings + if issubclass(w.category, DeprecationWarning) + and ("currentThread" in str(w.message) or "notifyAll" in str(w.message)) + ] + self.assertEqual( + deprecations, + [], + f"Found deprecation warnings: {deprecations}", + ) + + +if __name__ == "__main__": + unittest.main()