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

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


The following commit(s) were added to refs/heads/master by this push:
     new b139a8a  fix: speed up bulk policy loading, quiet rule logs and cap 
domain cache
b139a8a is described below

commit b139a8ab60aab3dbbfca491b98822ad594387fb4
Author: Yang Luo <[email protected]>
AuthorDate: Sun Nov 1 01:54:07 2026 +0800

    fix: speed up bulk policy loading, quiet rule logs and cap domain cache
---
 casbin/model/assertion.py                        |   2 +-
 casbin/model/policy.py                           |  49 +++++++--
 casbin/rbac/default_role_manager/role_manager.py |  35 +++++--
 tests/model/test_policy.py                       | 120 +++++++++++++++++++++++
 tests/rbac/test_role_manager.py                  |  29 ++++++
 tests/test_enforcer.py                           |  29 ++++++
 6 files changed, 244 insertions(+), 20 deletions(-)

diff --git a/casbin/model/assertion.py b/casbin/model/assertion.py
index f92da8d..408e369 100644
--- a/casbin/model/assertion.py
+++ b/casbin/model/assertion.py
@@ -46,7 +46,7 @@ class Assertion:
 
             self.rm.add_link(*rule[:count])
 
-        self.logger.info("Role links for: {}".format(self.key))
+        self.logger.debug(f"Role links for: {self.key}")
         self.rm.print_roles()
 
     def build_incremental_role_links(self, rm, op, rules):
diff --git a/casbin/model/policy.py b/casbin/model/policy.py
index 9c57cb3..65882b8 100644
--- a/casbin/model/policy.py
+++ b/casbin/model/policy.py
@@ -68,15 +68,15 @@ class Policy:
                 ast.build_conditional_role_links(cond_rm)
 
     def print_policy(self):
-        """Log using info"""
+        """Logs the policy at debug level."""
 
-        self.logger.info("Policy:")
+        self.logger.debug("Policy:")
         for sec in ["p", "g"]:
             if sec not in self.keys():
                 continue
 
             for key, ast in self[sec].items():
-                self.logger.info("{} : {} : {}".format(key, ast.value, 
ast.policy))
+                self.logger.debug(f"{key} : {ast.value} : {ast.policy}")
 
     def clear_policy(self):
         """clears all current policy."""
@@ -156,15 +156,24 @@ class Policy:
         return True
 
     def add_policies(self, sec, ptype, rules):
-        """adds policy rules to the model."""
+        """adds policy rules to the model in one pass.
+
+        Adapters loading many rules should use this instead of calling 
add_policy in a loop.
+        """
+        assertion = self[sec][ptype]
+        rules = list(rules)
+        policy_keys = {tuple(rule) for rule in assertion.policy}
 
         for rule in rules:
-            if self.has_policy(sec, ptype, rule):
+            if tuple(rule) in policy_keys:
                 return False
 
-        for rule in rules:
-            self.add_policy(sec, ptype, rule)
+        if sec == "p" and assertion.priority_index >= 0:
+            for rule in rules:
+                self.add_policy(sec, ptype, rule)
+            return True
 
+        self._append_policies(assertion, rules, policy_keys)
         return True
 
     def add_policies_ex(self, sec, ptype, rules):
@@ -173,11 +182,33 @@ class Policy:
         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.
         """
+        assertion = self[sec][ptype]
+        rules = list(rules)
+        policy_keys = {tuple(rule) for rule in assertion.policy}
+
+        if sec == "p" and assertion.priority_index >= 0:
+            rules_added = False
+            for rule in rules:
+                key = tuple(rule)
+                if key in policy_keys:
+                    continue
+                self.add_policy(sec, ptype, rule)
+                policy_keys.add(key)
+                rules_added = True
+            return rules_added
+
+        return self._append_policies(assertion, rules, policy_keys)
+
+    @staticmethod
+    def _append_policies(assertion, rules, policy_keys):
         rules_added = False
         for rule in rules:
-            if self.has_policy(sec, ptype, rule):
+            key = tuple(rule)
+            if key in policy_keys:
                 continue
-            self.add_policy(sec, ptype, rule)
+            policy_keys.add(key)
+            assertion.policy.append(rule)
+            assertion.policy_map[DEFAULT_SEP.join(rule)] = 
len(assertion.policy) - 1
             rules_added = True
         return rules_added
 
diff --git a/casbin/rbac/default_role_manager/role_manager.py 
b/casbin/rbac/default_role_manager/role_manager.py
index 84a91b3..183c1b8 100644
--- a/casbin/rbac/default_role_manager/role_manager.py
+++ b/casbin/rbac/default_role_manager/role_manager.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import logging
-from collections import namedtuple
+from collections import OrderedDict, namedtuple
 from enum import Enum
 
 from casbin.rbac import RoleManager as RM
@@ -216,7 +216,7 @@ class RoleManager(RM):
         return ", ".join(line)
 
     def print_roles(self):
-        self.logger.info(self.to_string())
+        self.logger.debug(self.to_string())
 
 
 class DomainManagerBase(RM):
@@ -297,19 +297,34 @@ class DomainManagerBase(RM):
 
 
 class DomainManager(DomainManagerBase):
-    def __init__(self, max_hierarchy_level=10):
+    def __init__(self, max_hierarchy_level=10, max_cached_role_managers=1000):
+        """Caches one role manager per domain.
+
+        max_cached_role_managers limits the cache; the least recently used role
+        manager is evicted when the limit is exceeded. Pass 0 or None for no 
limit.
+        ConditionalDomainManager does not use this limit because its link
+        conditions live in the role managers.
+        """
         super().__init__(max_hierarchy_level)
-        self.rm_map = dict()  # type: dict[str, RoleManager]
+        self.max_cached_role_managers = max_cached_role_managers
+        self.rm_map = OrderedDict()  # type: OrderedDict[str, RoleManager]
 
     def _rebuild(self):
-        self.rm_map = dict()
+        self.rm_map = OrderedDict()
 
     def _get_role_manager(self, *domain):
         domain1 = self._get_domain(*domain)
-        if domain1 not in self.rm_map:
-            self.rm_map[domain1] = super()._get_role_manager(*domain)
+        rm = self.rm_map.get(domain1)
+        if rm is None:
+            rm = super()._get_role_manager(*domain)
+            self.rm_map[domain1] = rm
+            if self.max_cached_role_managers and self.max_cached_role_managers 
> 0:
+                while len(self.rm_map) > self.max_cached_role_managers:
+                    self.rm_map.popitem(last=False)
+        else:
+            self.rm_map.move_to_end(domain1)
 
-        return self.rm_map[domain1]
+        return rm
 
     def _affected_role_managers(self, *domain):
         domain_pattern = self._get_domain(*domain)
@@ -336,7 +351,7 @@ class DomainManager(DomainManagerBase):
 
     def clear(self):
         super().clear()
-        self.rm_map = dict()
+        self.rm_map = OrderedDict()
 
     def add_link(self, name1, name2, *domain):
         super().add_link(name1, name2, *domain)
@@ -360,7 +375,7 @@ class DomainManager(DomainManagerBase):
     def print_roles(self):
         for domain, rm in self.rm_map.items():
             line = rm.to_string()
-            self.logger.info(f"{domain}: {line}")
+            self.logger.debug(f"{domain}: {line}")
 
 
 def match_error_handler(fn, key1, key2):
diff --git a/tests/model/test_policy.py b/tests/model/test_policy.py
index b4d1cbf..62e1979 100644
--- a/tests/model/test_policy.py
+++ b/tests/model/test_policy.py
@@ -173,6 +173,126 @@ class TestPolicy(TestCase):
         for new_rule in new_rules:
             self.assertTrue(m.has_policy("p", "p", new_rule))
 
+    def test_add_policies_bulk(self):
+        m = Model()
+        m.load_model(get_examples("basic_model.conf"))
+
+        rules = [[f"user{i}", f"data{i}", "read"] for i in range(1000)]
+        self.assertTrue(m.add_policies("p", "p", rules))
+        self.assertEqual(m.get_policy("p", "p"), rules)
+        self.assertEqual(m["p"]["p"].policy_map["user999,data999,read"], 999)
+
+        self.assertFalse(
+            m.add_policies(
+                "p", "p", [["new", "data", "read"], ["user0", "data0", "read"]]
+            )
+        )
+        self.assertEqual(len(m.get_policy("p", "p")), 1000)
+        self.assertFalse(m.has_policy("p", "p", ["new", "data", "read"]))
+
+        self.assertTrue(
+            m.add_policies("p", "p", [["dup", "data", "read"], ["dup", "data", 
"read"]])
+        )
+        self.assertEqual(len(m.get_policy("p", "p")), 1001)
+        self.assertTrue(m.has_policy("p", "p", ["dup", "data", "read"]))
+
+    def test_add_policies_accepts_iterators(self):
+        m = Model()
+        m.load_model(get_examples("basic_model.conf"))
+
+        rules = ([f"user{i}", f"data{i}", "read"] for i in range(3))
+        self.assertTrue(m.add_policies("p", "p", rules))
+        self.assertEqual(
+            m.get_policy("p", "p"),
+            [
+                ["user0", "data0", "read"],
+                ["user1", "data1", "read"],
+                ["user2", "data2", "read"],
+            ],
+        )
+
+    def test_add_policies_ex_bulk(self):
+        m = Model()
+        m.load_model(get_examples("basic_model.conf"))
+
+        m.add_policy("p", "p", ["user0", "data0", "read"])
+        rules = [
+            ["user0", "data0", "read"],
+            ["user1", "data1", "read"],
+            ["user1", "data1", "read"],
+            ["user2", "data2", "read"],
+        ]
+        self.assertTrue(m.add_policies_ex("p", "p", rules))
+        self.assertEqual(
+            m.get_policy("p", "p"),
+            [
+                ["user0", "data0", "read"],
+                ["user1", "data1", "read"],
+                ["user2", "data2", "read"],
+            ],
+        )
+
+        self.assertFalse(m.add_policies_ex("p", "p", [["user0", "data0", 
"read"]]))
+
+    def test_add_policies_distinguishes_comma_in_rule(self):
+        m = Model()
+        m.load_model(get_examples("basic_model.conf"))
+
+        m.add_policy("p", "p", ["a,b", "read"])
+        self.assertTrue(m.add_policies_ex("p", "p", [["a", "b", "read"]]))
+        self.assertEqual(
+            m.get_policy("p", "p"),
+            [["a,b", "read"], ["a", "b", "read"]],
+        )
+
+    def test_add_policies_priority(self):
+        rules = [
+            ["2", "admin", "data2", "read", "allow"],
+            ["1", "admin", "data1", "read", "allow"],
+            ["3", "admin", "data3", "read", "allow"],
+        ]
+
+        sequential = Model()
+        sequential.load_model(get_examples("priority_model_explicit.conf"))
+        sequential.sort_policies_by_priority()
+        for rule in rules:
+            sequential.add_policy("p", "p", rule)
+
+        bulk = Model()
+        bulk.load_model(get_examples("priority_model_explicit.conf"))
+        bulk.sort_policies_by_priority()
+        self.assertTrue(bulk.add_policies("p", "p", rules))
+
+        self.assertEqual(bulk.get_policy("p", "p"), sequential.get_policy("p", 
"p"))
+        self.assertEqual(
+            bulk.get_policy("p", "p"),
+            [
+                ["1", "admin", "data1", "read", "allow"],
+                ["2", "admin", "data2", "read", "allow"],
+                ["3", "admin", "data3", "read", "allow"],
+            ],
+        )
+
+        self.assertTrue(
+            bulk.add_policies_ex(
+                "p",
+                "p",
+                [
+                    ["2", "admin", "data2", "read", "allow"],
+                    ["0", "admin", "data0", "read", "allow"],
+                ],
+            )
+        )
+        self.assertEqual(
+            bulk.get_policy("p", "p"),
+            [
+                ["0", "admin", "data0", "read", "allow"],
+                ["1", "admin", "data1", "read", "allow"],
+                ["2", "admin", "data2", "read", "allow"],
+                ["3", "admin", "data3", "read", "allow"],
+            ],
+        )
+
     def test_remove_policy(self):
         m = Model()
         m.load_model(get_examples("basic_model.conf"))
diff --git a/tests/rbac/test_role_manager.py b/tests/rbac/test_role_manager.py
index 40c2c1f..9c53f08 100644
--- a/tests/rbac/test_role_manager.py
+++ b/tests/rbac/test_role_manager.py
@@ -266,6 +266,35 @@ class TestDomainManager(TestRoleManager):
     def get_role_manager(self):
         return default_role_manager.DomainManager(max_hierarchy_level=10)
 
+    def test_domain_role_manager_cache_limit(self):
+        rm = default_role_manager.DomainManager(
+            max_hierarchy_level=10, max_cached_role_managers=2
+        )
+        rm.add_link("u1", "g1", "domain1")
+        rm.add_link("u2", "g2", "domain2")
+        rm.add_link("u3", "g3", "domain3")
+
+        self.assertTrue(rm.has_link("u1", "g1", "domain1"))
+        self.assertTrue(rm.has_link("u2", "g2", "domain2"))
+        self.assertTrue(rm.has_link("u3", "g3", "domain3"))
+        self.assertEqual(len(rm.rm_map), 2)
+        self.assertEqual(list(rm.rm_map.keys()), ["domain2", "domain3"])
+
+        self.assertTrue(rm.has_link("u1", "g1", "domain1"))
+        self.assertEqual(len(rm.rm_map), 2)
+        self.assertEqual(list(rm.rm_map.keys()), ["domain3", "domain1"])
+
+    def test_domain_role_manager_cache_unbounded(self):
+        rm = default_role_manager.DomainManager(
+            max_hierarchy_level=10, max_cached_role_managers=0
+        )
+        for i in range(5):
+            rm.add_link(f"u{i}", f"g{i}", f"domain{i}")
+
+        for i in range(5):
+            self.assertTrue(rm.has_link(f"u{i}", f"g{i}", f"domain{i}"))
+        self.assertEqual(len(rm.rm_map), 5)
+
     def test_domain_role(self):
         rm = self.get_role_manager()
         rm.add_link("u1", "g1", "domain1")
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
index c295283..c05059f 100644
--- a/tests/test_enforcer.py
+++ b/tests/test_enforcer.py
@@ -14,6 +14,7 @@
 
 import atexit
 import copy
+import logging
 import os
 import shutil
 import tempfile
@@ -1139,3 +1140,31 @@ class TestModelDeepCopy(TestCase):
         # the role manager must still be usable even though nothing rebuilt 
the links
         self.assertIsNotNone(e.model.model["g"]["g"].rm)
         self.assertTrue(e.enforce("alice", "data2", "read"))
+
+
+class TestLogging(TestCaseBase):
+    def test_load_policy_logs_rules_at_debug(self):
+        e = self.get_enforcer(
+            get_examples("rbac_model.conf"),
+            get_examples("rbac_policy.csv"),
+        )
+        policy_logger = logging.getLogger("casbin.policy")
+        role_logger = logging.getLogger("casbin.role")
+        policy_disabled = policy_logger.disabled
+        role_disabled = role_logger.disabled
+        policy_logger.disabled = False
+        role_logger.disabled = False
+
+        try:
+            with self.assertLogs("casbin.policy", level="DEBUG") as captured:
+                e.load_policy()
+            self.assertFalse(any(record.levelno >= logging.INFO for record in 
captured.records))
+            self.assertTrue(any("alice" in record.getMessage() for record in 
captured.records))
+
+            with self.assertLogs("casbin.role", level="DEBUG") as captured:
+                e.load_policy()
+            self.assertFalse(any(record.levelno >= logging.INFO for record in 
captured.records))
+            self.assertTrue(any("alice" in record.getMessage() for record in 
captured.records))
+        finally:
+            policy_logger.disabled = policy_disabled
+            role_logger.disabled = role_disabled


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

Reply via email to