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]
