diff --git a/casbin/synced_enforcer.py b/casbin/synced_enforcer.py index c5982c6..b154c70 100644 --- a/casbin/synced_enforcer.py +++ b/casbin/synced_enforcer.py @@ -36,7 +36,6 @@ def value(self, value): class SyncedEnforcer: - """SyncedEnforcer wraps Enforcer and provides synchronized access. It's also a drop-in replacement for Enforcer""" @@ -48,6 +47,14 @@ def __init__(self, model=None, adapter=None): self._auto_loading = AtomicBool(False) self._auto_loading_thread = None + def _notify_watcher(self, method_name, *args): + """Helper method to notify watcher outside of locks to prevent deadlock.""" + if self._e.watcher and self._e.auto_notify_watcher: + if callable(getattr(self._e.watcher, method_name, None)): + getattr(self._e.watcher, method_name)(*args) + else: + self._e.watcher.update() + def is_auto_loading_running(self): """check if SyncedEnforcer is auto loading policies""" return self._auto_loading.value @@ -113,6 +120,9 @@ def set_watcher(self, watcher): """sets the current watcher.""" with self._wl: self._e.set_watcher(watcher) + # Set the callback to use load_policy which will properly acquire locks + if watcher and callable(getattr(watcher, "set_update_callback", None)): + watcher.set_update_callback(self.load_policy) def set_effector(self, eft): """sets the current effector.""" @@ -260,36 +270,108 @@ def add_policy(self, *params): If the rule already exists, the function returns false and the rule will not be added. Otherwise the function returns true by adding the new rule. """ - with self._wl: - return self._e.add_policy(*params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_policy(*params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policy", "p", "p", list(params)) + + return result def add_named_policy(self, ptype, *params): """adds an authorization rule to the current named policy. If the rule already exists, the function returns false and the rule will not be added. Otherwise the function returns true by adding the new rule. """ - with self._wl: - return self._e.add_named_policy(ptype, *params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_named_policy(ptype, *params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policy", "p", ptype, list(params)) + + return result def remove_policy(self, *params): """removes an authorization rule from the current policy.""" - with self._wl: - return self._e.remove_policy(*params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_policy(*params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policy", "p", "p", list(params)) + + return result def remove_filtered_policy(self, field_index, *field_values): """removes an authorization rule from the current policy, field filters can be specified.""" - with self._wl: - return self._e.remove_filtered_policy(field_index, *field_values) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_filtered_policy(field_index, *field_values) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_filtered_policy", "p", "p", field_index, *field_values) + + return result def remove_named_policy(self, ptype, *params): """removes an authorization rule from the current named policy.""" - with self._wl: - return self._e.remove_named_policy(ptype, *params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_named_policy(ptype, *params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policy", "p", ptype, list(params)) + + return result def remove_filtered_named_policy(self, ptype, field_index, *field_values): """removes an authorization rule from the current named policy, field filters can be specified.""" - with self._wl: - return self._e.remove_filtered_named_policy(ptype, field_index, *field_values) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_filtered_named_policy(ptype, field_index, *field_values) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_filtered_policy", "p", ptype, field_index, *field_values) + + return result def has_grouping_policy(self, *params): """determines whether a role inheritance rule exists.""" @@ -306,36 +388,108 @@ def add_grouping_policy(self, *params): If the rule already exists, the function returns false and the rule will not be added. Otherwise the function returns true by adding the new rule. """ - with self._wl: - return self._e.add_grouping_policy(*params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_grouping_policy(*params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policy", "g", "g", list(params)) + + return result def add_named_grouping_policy(self, ptype, *params): """adds a named role inheritance rule to the current policy. If the rule already exists, the function returns false and the rule will not be added. Otherwise the function returns true by adding the new rule. """ - with self._wl: - return self._e.add_named_grouping_policy(ptype, *params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_named_grouping_policy(ptype, *params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policy", "g", ptype, list(params)) + + return result def remove_grouping_policy(self, *params): """removes a role inheritance rule from the current policy.""" - with self._wl: - return self._e.remove_grouping_policy(*params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_grouping_policy(*params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policy", "g", "g", list(params)) + + return result def remove_filtered_grouping_policy(self, field_index, *field_values): """removes a role inheritance rule from the current policy, field filters can be specified.""" - with self._wl: - return self._e.remove_filtered_grouping_policy(field_index, *field_values) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_filtered_grouping_policy(field_index, *field_values) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_filtered_policy", "g", "g", field_index, *field_values) + + return result def remove_named_grouping_policy(self, ptype, *params): """removes a role inheritance rule from the current named policy.""" - with self._wl: - return self._e.remove_named_grouping_policy(ptype, *params) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_named_grouping_policy(ptype, *params) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policy", "g", ptype, list(params)) + + return result def remove_filtered_named_grouping_policy(self, ptype, field_index, *field_values): """removes a role inheritance rule from the current named policy, field filters can be specified.""" - with self._wl: - return self._e.remove_filtered_named_grouping_policy(ptype, field_index, *field_values) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_filtered_named_grouping_policy(ptype, field_index, *field_values) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_filtered_policy", "g", ptype, field_index, *field_values) + + return result def add_function(self, name, func): """adds a customized function.""" @@ -547,6 +701,11 @@ def enable_auto_save(self, auto_save): with self._wl: return self._e.enable_auto_save(auto_save) + def enable_auto_notify_watcher(self, auto_notify_watcher): + """controls whether to notify the watcher automatically when a policy rule is added or removed.""" + with self._wl: + self._e.auto_notify_watcher = auto_notify_watcher + def enable_enforce(self, enabled=True): """changes the enforcing state of Casbin, when Casbin is disabled, all access will be allowed by the Enforce() function. @@ -597,8 +756,20 @@ def add_policies(self, rules): If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. Otherwise the function returns true for the corresponding rule by adding the new rule. """ - with self._wl: - return self._e.add_policies(rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_policies(rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies", "p", "p", rules) + + return result def add_policies_ex(self, rules): """add_policies_ex adds authorization rules to the current policy. @@ -606,8 +777,20 @@ def add_policies_ex(self, rules): If the rule already exists, the rule will not be added. But unlike add_policies, other non-existent rules are added instead of returning false directly. """ - with self._wl: - return self._e.add_policies_ex(rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_policies_ex(rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies_ex", "p", "p", rules) + + return result def add_named_policies_ex(self, ptype, rules): """add_named_policies_ex adds authorization rules to the current policy. @@ -615,26 +798,74 @@ def add_named_policies_ex(self, ptype, rules): If the rule already exists, the rule will not be added. But unlike add_named_policies, other non-existent rules are added instead of returning false directly. """ - with self._wl: - return self._e.add_named_policies_ex(ptype, rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_named_policies_ex(ptype, rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies_ex", "p", ptype, rules) + + return result def add_named_policies(self, ptype, rules): """adds authorization rules to the current named policy. If the rule already exists, the function returns false for the corresponding rule and the rule will not be added. Otherwise the function returns true for the corresponding by adding the new rule.""" - with self._wl: - return self._e.add_named_policies(ptype, rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_named_policies(ptype, rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies", "p", ptype, rules) + + return result def remove_policies(self, rules): """removes authorization rules from the current policy.""" - with self._wl: - return self._e.remove_policies(rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_policies(rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policies", "p", "p", rules) + + return result def remove_named_policies(self, ptype, rules): """removes authorization rules from the current named policy.""" - with self._wl: - return self._e.remove_named_policies(ptype, rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_named_policies(ptype, rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policies", "p", ptype, rules) + + return result def add_grouping_policies(self, rules): """adds role inheritance rules to the current policy. @@ -642,8 +873,20 @@ def add_grouping_policies(self, rules): If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. Otherwise the function returns true for the corresponding policy rule by adding the new rule. """ - with self._wl: - return self._e.add_grouping_policies(rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_grouping_policies(rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies", "g", "g", rules) + + return result def add_grouping_policies_ex(self, rules): """add_grouping_policies_ex adds role inheritance rules to the current policy. @@ -651,16 +894,40 @@ def add_grouping_policies_ex(self, rules): If the rule already exists, the rule will not be added. But unlike add_grouping_policies, other non-existent rules are added instead of returning false directly. """ - with self._wl: - return self._e.add_grouping_policies_ex(rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_grouping_policies_ex(rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies_ex", "g", "g", rules) + + return result def add_named_grouping_policies(self, ptype, rules): """ "adds named role inheritance rules to the current policy. If the rule already exists, the function returns false for the corresponding policy rule and the rule will not be added. Otherwise the function returns true for the corresponding policy rule by adding the new rule.""" - with self._wl: - return self._e.add_named_grouping_policies(ptype, rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_named_grouping_policies(ptype, rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies", "g", ptype, rules) + + return result def add_named_grouping_policies_ex(self, ptype, rules): """add_named_grouping_policies_ex adds role inheritance rules to the current named policy. @@ -668,18 +935,54 @@ def add_named_grouping_policies_ex(self, ptype, rules): If the rule already exists, the rule will not be added. But unlike add_named_grouping_policies, other non-existent rules are added instead of returning false directly. """ - with self._wl: - return self._e.add_named_grouping_policies_ex(ptype, rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.add_named_grouping_policies_ex(ptype, rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_add_policies_ex", "g", ptype, rules) + + return result def remove_grouping_policies(self, rules): """removes role inheritance rules from the current policy.""" - with self._wl: - return self._e.remove_grouping_policies(rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_grouping_policies(rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policies", "g", "g", rules) + + return result def remove_named_grouping_policies(self, ptype, rules): """removes role inheritance rules from the current named policy.""" - with self._wl: - return self._e.remove_named_grouping_policies(ptype, rules) + # Temporarily disable auto_notify to prevent deadlock + old_auto_notify = self._e.auto_notify_watcher + self._e.auto_notify_watcher = False + try: + with self._wl: + result = self._e.remove_named_grouping_policies(ptype, rules) + finally: + self._e.auto_notify_watcher = old_auto_notify + + # Notify watcher outside of lock to prevent deadlock + if result and old_auto_notify: + self._notify_watcher("update_for_remove_policies", "g", ptype, rules) + + return result def build_incremental_role_links(self, op, ptype, rules): self.get_model().build_incremental_role_links(self.get_role_manager(), op, "g", ptype, rules) diff --git a/tests/test_synced_enforcer_deadlock.py b/tests/test_synced_enforcer_deadlock.py new file mode 100644 index 0000000..9511beb --- /dev/null +++ b/tests/test_synced_enforcer_deadlock.py @@ -0,0 +1,217 @@ +# Copyright 2024 The casbin Authors. All Rights Reserved. +# +# 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 +# +# 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. + +import threading +import time +import unittest +import casbin +from tests.test_enforcer import get_examples + + +class MockRedisWatcher: + """Mock watcher that simulates Redis watcher behavior with mutex.""" + + def __init__(self): + self.mutex = threading.Lock() + self.callback = None + self.update_count = 0 + self.subscribe_thread = None + self.should_stop = False + + def set_update_callback(self, callback): + """Set the callback function that will be called when policy changes.""" + self.callback = callback + + def update(self): + """Simulate Redis watcher update - acquires mutex and publishes.""" + with self.mutex: + self.update_count += 1 + return True + + def update_for_add_policy(self, sec, ptype, rule): + """Update for add policy.""" + return self.update() + + def update_for_remove_policy(self, sec, ptype, rule): + """Update for remove policy.""" + return self.update() + + def update_for_add_policies(self, sec, ptype, rules): + """Update for add policies.""" + return self.update() + + def update_for_remove_policies(self, sec, ptype, rules): + """Update for remove policies.""" + return self.update() + + def update_for_remove_filtered_policy(self, sec, ptype, field_index, *field_values): + """Update for remove filtered policy.""" + return self.update() + + def simulate_subscribe(self): + """Simulate subscribe thread that calls callback with mutex held.""" + while not self.should_stop: + time.sleep(0.01) # Small delay to simulate listening + # Simulate receiving a message and calling callback + if self.callback: + with self.mutex: # Acquire mutex like Redis watcher does + try: + self.callback() + except Exception as e: + print(f"Callback error: {e}") + + def start_subscribe(self): + """Start the subscribe thread.""" + self.should_stop = False + self.subscribe_thread = threading.Thread(target=self.simulate_subscribe, daemon=True) + self.subscribe_thread.start() + + def stop_subscribe(self): + """Stop the subscribe thread.""" + self.should_stop = True + if self.subscribe_thread: + self.subscribe_thread.join(timeout=1.0) + + def close(self): + """Close the watcher.""" + self.stop_subscribe() + + +class TestSyncedEnforcerDeadlock(unittest.TestCase): + """Test that SyncedEnforcer doesn't deadlock with watcher.""" + + def test_no_deadlock_with_concurrent_operations(self): + """Test that concurrent policy updates and load_policy don't cause deadlock.""" + e = casbin.SyncedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + # Create and attach mock watcher + watcher = MockRedisWatcher() + e.set_watcher(watcher) + e.enable_auto_notify_watcher(True) + + # Start the subscribe thread that will call load_policy + watcher.start_subscribe() + + # Give subscribe thread time to start + time.sleep(0.05) + + deadlock_detected = False + errors = [] + + def add_policies_repeatedly(): + """Repeatedly add policies.""" + try: + for i in range(10): + e.add_policy(f"user{i}", "data1", "read") + time.sleep(0.01) + except Exception as ex: + errors.append(f"add_policy error: {ex}") + + def remove_policies_repeatedly(): + """Repeatedly remove policies.""" + try: + time.sleep(0.02) # Slight offset + for i in range(5): + e.remove_policy(f"user{i}", "data1", "read") + time.sleep(0.01) + except Exception as ex: + errors.append(f"remove_policy error: {ex}") + + # Start threads that will compete for locks + t1 = threading.Thread(target=add_policies_repeatedly) + t2 = threading.Thread(target=remove_policies_repeatedly) + + t1.start() + t2.start() + + # Wait for threads with timeout to detect deadlock + t1.join(timeout=5.0) + t2.join(timeout=5.0) + + # Check if threads completed (no deadlock) + if t1.is_alive() or t2.is_alive(): + deadlock_detected = True + + # Clean up + watcher.stop_subscribe() + watcher.close() + + # Assert no deadlock occurred + self.assertFalse(deadlock_detected, "Deadlock detected: threads didn't complete in time") + self.assertEqual(len(errors), 0, f"Errors occurred: {errors}") + + # Verify watcher was notified + self.assertGreater(watcher.update_count, 0, "Watcher should have been notified") + + def test_watcher_notified_after_lock_release(self): + """Test that watcher is notified after the lock is released.""" + e = casbin.SyncedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + watcher = MockRedisWatcher() + e.set_watcher(watcher) + e.enable_auto_notify_watcher(True) + + # Add a policy - watcher should be notified + result = e.add_policy("alice", "data1", "write") + self.assertTrue(result) + self.assertEqual(watcher.update_count, 1) + + # Remove a policy - watcher should be notified + result = e.remove_policy("alice", "data1", "write") + self.assertTrue(result) + self.assertEqual(watcher.update_count, 2) + + # Add multiple policies - watcher should be notified + rules = [ + ["bob", "data2", "read"], + ["charlie", "data3", "write"], + ] + result = e.add_policies(rules) + self.assertTrue(result) + self.assertEqual(watcher.update_count, 3) + + # Remove multiple policies - watcher should be notified + result = e.remove_policies(rules) + self.assertTrue(result) + self.assertEqual(watcher.update_count, 4) + + watcher.close() + + def test_watcher_not_notified_when_disabled(self): + """Test that watcher is not notified when auto_notify is disabled.""" + e = casbin.SyncedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + watcher = MockRedisWatcher() + e.set_watcher(watcher) + e.enable_auto_notify_watcher(False) + + # Add a policy - watcher should NOT be notified + result = e.add_policy("alice", "data1", "write") + self.assertTrue(result) + self.assertEqual(watcher.update_count, 0) + + watcher.close() + + +if __name__ == "__main__": + unittest.main()