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

EmryZhang pushed a commit to branch prep/pycasbin-rwlock
in repository https://gitbox.apache.org/repos/asf/casbin-pycasbin.git

commit db27e0823931896ba0ea69f7126191462b97551a
Author: Zhangyanrui123 <[email protected]>
AuthorDate: Sat Nov 7 14:41:03 2026 +0800

    refactor: delegate reader writer locking to readerwriterlock
---
 casbin/util/rwlock.py     | 82 ++++++++++++++++++++++++++---------------------
 requirements.txt          |  3 +-
 tests/util/test_rwlock.py | 54 +++++++++++++++++++++++++++++++
 3 files changed, 102 insertions(+), 37 deletions(-)

diff --git a/casbin/util/rwlock.py b/casbin/util/rwlock.py
index 0c0355d..0adb33f 100644
--- a/casbin/util/rwlock.py
+++ b/casbin/util/rwlock.py
@@ -1,56 +1,66 @@
-# Copyright 2021 The casbin Authors. All Rights Reserved.
+# 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
 #
-# Licensed 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
+#   https://www.apache.org/licenses/LICENSE-2.0
 #
-#      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.
+# 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.
+
+"""Compatibility wrappers for the separately installed MIT-licensed 
readerwriterlock.
+
+A lock handle from that library records acquisition state. Give each 
acquisition
+its own handle so the context objects shared by SyncedEnforcer remain safe when
+several threads enter the same read context concurrently.
+"""
 
-from threading import RLock, Condition
+from threading import local
 
-# This implementation was adapted from 
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
+from readerwriterlock.rwlock import RWLockWrite as _WriterPreferredLock
 
 
 class RWLockWrite:
-    """write preferring readers-wirter lock"""
+    """A writer-preferring lock retaining Casbin's existing context/direct 
API."""
 
     def __init__(self):
-        self._lock = RLock()
-        self._cond = Condition(self._lock)
-        self._active_readers = 0
-        self._waiting_writers = 0
-        self._writer_active = False
+        self._delegate = _WriterPreferredLock()
+        self._held = local()
+
+    def _acquire(self, kind):
+        handle = self._delegate.gen_rlock() if kind == "read" else 
self._delegate.gen_wlock()
+        handle.acquire()
+        stack = getattr(self._held, kind, None)
+        if stack is None:
+            stack = []
+            setattr(self._held, kind, stack)
+        stack.append(handle)
+
+    def _release(self, kind):
+        stack = getattr(self._held, kind, None)
+        if not stack:
+            raise RuntimeError("No {} lock acquired by this 
thread".format(kind))
+        stack.pop().release()
 
     def aquire_read(self):
-        with self._lock:
-            while self._waiting_writers > 0 or self._writer_active:
-                self._cond.wait()
-            self._active_readers += 1
+        # Preserve the historical method spelling for existing callers.
+        self._acquire("read")
 
     def release_read(self):
-        with self._lock:
-            self._active_readers -= 1
-            if self._active_readers == 0:
-                self._cond.notify_all()
+        self._release("read")
 
     def aquire_write(self):
-        with self._lock:
-            self._waiting_writers += 1
-            while self._active_readers > 0 or self._writer_active:
-                self._cond.wait()
-            self._waiting_writers -= 1
-            self._writer_active = True
+        self._acquire("write")
 
     def release_write(self):
-        with self._lock:
-            self._writer_active = False
-            self._cond.notify_all()
+        self._release("write")
 
     def gen_rlock(self):
         return ReadRWLock(self)
diff --git a/requirements.txt b/requirements.txt
index 1c6b0bd..bded624 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 simpleeval >= 1.0.3
-wcmatch >= 10.1
\ No newline at end of file
+wcmatch >= 10.1
+readerwriterlock >= 1.0.9, < 2
diff --git a/tests/util/test_rwlock.py b/tests/util/test_rwlock.py
index d4a248a..f580273 100644
--- a/tests/util/test_rwlock.py
+++ b/tests/util/test_rwlock.py
@@ -17,9 +17,63 @@ from casbin.util.rwlock import RWLockWrite
 from concurrent.futures import ThreadPoolExecutor
 import time
 import queue
+import threading
 
 
 class TestRWLock(TestCase):
+    def test_shared_read_context_keeps_both_readers_locked(self):
+        lock = RWLockWrite()
+        read = lock.gen_rlock()
+        entered = [threading.Event(), threading.Event()]
+        leave = [threading.Event(), threading.Event()]
+        finished = [threading.Event(), threading.Event()]
+        written = threading.Event()
+
+        def reader(index):
+            with read:
+                entered[index].set()
+                leave[index].wait(5)
+            finished[index].set()
+
+        def writer():
+            with lock.gen_wlock():
+                written.set()
+
+        threads = [threading.Thread(target=reader, args=(i,), daemon=True) for 
i in range(2)]
+        for thread in threads:
+            thread.start()
+        writer_thread = threading.Thread(target=writer, daemon=True)
+        try:
+            self.assertTrue(all(event.wait(2) for event in entered))
+            writer_thread.start()
+            self.assertFalse(written.wait(0.05))
+            leave[0].set()
+            self.assertTrue(finished[0].wait(2))
+            self.assertFalse(written.wait(0.05))
+            leave[1].set()
+            self.assertTrue(written.wait(2))
+        finally:
+            for event in leave:
+                event.set()
+            for thread in threads:
+                thread.join(2)
+            if writer_thread.ident is not None:
+                writer_thread.join(2)
+
+    def test_direct_api_and_nested_read_contexts(self):
+        lock = RWLockWrite()
+        lock.aquire_read()
+        with lock.gen_rlock():
+            pass
+        lock.release_read()
+        lock.aquire_write()
+        lock.release_write()
+        with self.assertRaises(ValueError):
+            with lock.gen_wlock():
+                raise ValueError("caller failure")
+        with lock.gen_wlock():
+            pass
+
     def gen_locks(self):
         rw_lock = RWLockWrite()
         rl = rw_lock.gen_rlock()


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to