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 d4db65a fix: cache parsed matchers to speed up enforce
d4db65a is described below
commit d4db65a8f42ffef0fde1a58b1081cf0745d1e6d1
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 21 00:26:03 2026 +0800
fix: cache parsed matchers to speed up enforce
---
casbin/core_enforcer.py | 25 +++++++++++++++++--------
casbin/util/expression.py | 12 +++++++++++-
tests/test_enforcer.py | 15 +++++++++++++++
tests/test_fast_enforcer.py | 19 ++++++++++++-------
4 files changed, 55 insertions(+), 16 deletions(-)
diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py
index a80744b..3473309 100644
--- a/casbin/core_enforcer.py
+++ b/casbin/core_enforcer.py
@@ -15,6 +15,7 @@
import copy
import logging
import re
+from functools import lru_cache
from casbin.effect import Effector, get_effector, effect_to_bool
from casbin.model import Model, FunctionMap
@@ -436,8 +437,9 @@ class CoreEnforcer:
exp_string = self.model["m"][mtype].value
exp_has_eval = util.has_eval(exp_string)
- if not exp_has_eval:
- expression = self._get_expression(exp_string, functions)
+ expression = self._get_expression("" if exp_has_eval else exp_string,
functions)
+ if exp_has_eval:
+ rule_names = util.get_eval_value(exp_string)
policy_effects = set()
@@ -455,10 +457,8 @@ class CoreEnforcer:
parameters = dict(r_parameters, **p_parameters)
if exp_has_eval:
- rule_names = util.get_eval_value(exp_string)
- rules = [util.escape_assertion(p_parameters[rule_name])
for rule_name in rule_names]
- exp_with_rule = util.replace_eval(exp_string, rules)
- expression = self._get_expression(exp_with_rule, functions)
+ rules = tuple(p_parameters[rule_name] for rule_name in
rule_names)
+ expression.set_expr(self._get_eval_expression(exp_string,
rules))
result = expression.eval(parameters)
@@ -546,8 +546,17 @@ class CoreEnforcer:
@staticmethod
def _get_expression(expr, functions=None):
+ return SimpleEval(CoreEnforcer._normalize_expression(expr), functions)
+
+ @staticmethod
+ @lru_cache(maxsize=8192)
+ def _normalize_expression(expr):
expr = expr.replace("&&", "and")
expr = expr.replace("||", "or")
- expr = re.sub(r"!(?!=)", "not ", expr)
+ return re.sub(r"!(?!=)", "not ", expr)
- return SimpleEval(expr, functions)
+ @staticmethod
+ @lru_cache(maxsize=8192)
+ def _get_eval_expression(expr, rules):
+ rules = [util.escape_assertion(rule) for rule in rules]
+ return CoreEnforcer._normalize_expression(util.replace_eval(expr,
rules))
diff --git a/casbin/util/expression.py b/casbin/util/expression.py
index e2abb5d..3df8d26 100644
--- a/casbin/util/expression.py
+++ b/casbin/util/expression.py
@@ -12,10 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from functools import lru_cache
from simpleeval import EvalWithCompoundTypes
import ast
+@lru_cache(maxsize=8192)
+def parse_expression(expr):
+ return ast.parse(expr.strip()).body[0].value
+
+
class SimpleEval(EvalWithCompoundTypes):
"""Rewrite SimpleEval.
>>> s = SimpleEval("20 + 30 - ( 10 * 5)")
@@ -29,9 +35,13 @@ class SimpleEval(EvalWithCompoundTypes):
"""Create the evaluator instance. Set up valid operators (+,-, etc)
functions (add, random, get_val, whatever) and names."""
super(SimpleEval, self).__init__(functions=functions)
+ self.set_expr(expr)
+
+ def set_expr(self, expr):
+ """replace the expression to evaluate, keeping the functions."""
if expr != "":
self.expr = expr
- self.ast_parsed_value = ast.parse(expr.strip()).body[0].value
+ self.ast_parsed_value = parse_expression(expr)
def eval(self, names=None):
"""evaluate an expression, using the operators, functions and
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
index 64f9e2f..c295283 100644
--- a/tests/test_enforcer.py
+++ b/tests/test_enforcer.py
@@ -420,6 +420,21 @@ class TestConfig(TestCaseBase):
self.assertFalse(e.enforce(sub3, "/data1", "write"))
self.assertFalse(e.enforce(sub3, "/data2", "write"))
+ def test_abac_with_updated_sub_rule(self):
+ e = self.get_enforcer(get_examples("abac_rule_model.conf"),
get_examples("abac_rule_policy.csv"))
+ e.enable_auto_save(False)
+
+ sub = MockSub("alice", 16)
+ self.assertFalse(e.enforce(sub, "/data1", "read"))
+
+ e.remove_policy("r.sub.age > 18", "/data1", "read")
+ e.add_policy("r.sub.age > 10", "/data1", "read")
+ self.assertTrue(e.enforce(sub, "/data1", "read"))
+
+ e.remove_policy("r.sub.age > 10", "/data1", "read")
+ e.add_policy("r.sub.age > 18", "/data1", "read")
+ self.assertFalse(e.enforce(sub, "/data1", "read"))
+
def test_abac_with_multiple_sub_rules(self):
e = self.get_enforcer(
get_examples("abac_multiple_rules_model.conf"),
diff --git a/tests/test_fast_enforcer.py b/tests/test_fast_enforcer.py
index 77a2ff5..c3d8ee1 100644
--- a/tests/test_fast_enforcer.py
+++ b/tests/test_fast_enforcer.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import gc
import os
import time
from typing import Sequence
@@ -49,13 +50,17 @@ class TestFastEnforcer(TestCaseBase):
N = 5
t_e1_list = []
t_e2_list = []
- for _ in range(N):
- s_e1 = time.perf_counter()
- e1.enforce("alice", "data1", "read")
- t_e1_list.append(time.perf_counter() - s_e1)
- s_e2 = time.perf_counter()
- e2.enforce("alice", "data1", "read")
- t_e2_list.append(time.perf_counter() - s_e2)
+ gc.disable()
+ try:
+ for _ in range(N):
+ s_e1 = time.perf_counter()
+ e1.enforce("alice", "data1", "read")
+ t_e1_list.append(time.perf_counter() - s_e1)
+ s_e2 = time.perf_counter()
+ e2.enforce("alice", "data1", "read")
+ t_e2_list.append(time.perf_counter() - s_e2)
+ finally:
+ gc.enable()
avg_t_e1 = sum(t_e1_list) / N
avg_t_e2 = sum(t_e2_list) / N
assert avg_t_e1 > avg_t_e2 * 5
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]