mirror of
https://github.com/retspen/webvirtcloud
synced 2026-10-18 00:50:07 +00:00
fix(vrtManager): modernize rwlock for Python 3.12 compatibility and add tests
This commit is contained in:
parent
a26407176c
commit
dc6327c554
3 changed files with 205 additions and 7 deletions
|
|
@ -20,7 +20,6 @@ paramiko==3.4.0
|
||||||
python-engineio==4.12.0
|
python-engineio==4.12.0
|
||||||
python-socketio==5.13.0
|
python-socketio==5.13.0
|
||||||
qrcode==8.2
|
qrcode==8.2
|
||||||
rwlock==0.0.7
|
|
||||||
tzdata
|
tzdata
|
||||||
websockify==0.13.0
|
websockify==0.13.0
|
||||||
whitenoise==6.9.0
|
whitenoise==6.9.0
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ found at: http://code.activestate.com/recipes/502283-read-write-lock-class-rlock
|
||||||
|
|
||||||
# Imports
|
# Imports
|
||||||
# -------
|
# -------
|
||||||
from threading import Condition, Lock, currentThread
|
from threading import Condition, Lock, current_thread
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
# Read write lock
|
# Read write lock
|
||||||
|
|
@ -71,7 +71,7 @@ class ReadWriteLock(object):
|
||||||
|
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
endtime = time() + timeout
|
endtime = time() + timeout
|
||||||
me = currentThread()
|
me = current_thread()
|
||||||
self.__condition.acquire()
|
self.__condition.acquire()
|
||||||
try:
|
try:
|
||||||
if self.__writer is me:
|
if self.__writer is me:
|
||||||
|
|
@ -121,7 +121,7 @@ class ReadWriteLock(object):
|
||||||
|
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
endtime = time() + timeout
|
endtime = time() + timeout
|
||||||
me, upgradewriter = currentThread(), False
|
me, upgradewriter = current_thread(), False
|
||||||
self.__condition.acquire()
|
self.__condition.acquire()
|
||||||
try:
|
try:
|
||||||
if self.__writer is me:
|
if self.__writer is me:
|
||||||
|
|
@ -196,7 +196,7 @@ class ReadWriteLock(object):
|
||||||
|
|
||||||
In case the current thread holds no lock, a ValueError is thrown."""
|
In case the current thread holds no lock, a ValueError is thrown."""
|
||||||
|
|
||||||
me = currentThread()
|
me = current_thread()
|
||||||
self.__condition.acquire()
|
self.__condition.acquire()
|
||||||
try:
|
try:
|
||||||
if self.__writer is me:
|
if self.__writer is me:
|
||||||
|
|
@ -206,7 +206,7 @@ class ReadWriteLock(object):
|
||||||
# No more write locks; take our writer position away and
|
# No more write locks; take our writer position away and
|
||||||
# notify waiters of the new circumstances.
|
# notify waiters of the new circumstances.
|
||||||
self.__writer = None
|
self.__writer = None
|
||||||
self.__condition.notifyAll()
|
self.__condition.notify_all()
|
||||||
elif me in self.__readers:
|
elif me in self.__readers:
|
||||||
# We are a reader currently, take one nesting depth away.
|
# We are a reader currently, take one nesting depth away.
|
||||||
self.__readers[me] -= 1
|
self.__readers[me] -= 1
|
||||||
|
|
@ -216,7 +216,7 @@ class ReadWriteLock(object):
|
||||||
if not self.__readers:
|
if not self.__readers:
|
||||||
# No more readers, notify waiters of the new
|
# No more readers, notify waiters of the new
|
||||||
# circumstances.
|
# circumstances.
|
||||||
self.__condition.notifyAll()
|
self.__condition.notify_all()
|
||||||
else:
|
else:
|
||||||
raise ValueError("Trying to release unheld lock")
|
raise ValueError("Trying to release unheld lock")
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
199
vrtManager/test_rwlock.py
Normal file
199
vrtManager/test_rwlock.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue