Skip to content

Commit d4db65a

Browse files
committed
fix: cache parsed matchers to speed up enforce
1 parent bd137d0 commit d4db65a

4 files changed

Lines changed: 55 additions & 16 deletions

File tree

‎casbin/core_enforcer.py‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import copy
1616
import logging
1717
import re
18+
from functools import lru_cache
1819

1920
from casbin.effect import Effector, get_effector, effect_to_bool
2021
from casbin.model import Model, FunctionMap
@@ -436,8 +437,9 @@ def enforce_ex(self, *rvals):
436437

437438
exp_string = self.model["m"][mtype].value
438439
exp_has_eval = util.has_eval(exp_string)
439-
if not exp_has_eval:
440-
expression = self._get_expression(exp_string, functions)
440+
expression = self._get_expression("" if exp_has_eval else exp_string, functions)
441+
if exp_has_eval:
442+
rule_names = util.get_eval_value(exp_string)
441443

442444
policy_effects = set()
443445

@@ -455,10 +457,8 @@ def enforce_ex(self, *rvals):
455457
parameters = dict(r_parameters, **p_parameters)
456458

457459
if exp_has_eval:
458-
rule_names = util.get_eval_value(exp_string)
459-
rules = [util.escape_assertion(p_parameters[rule_name]) for rule_name in rule_names]
460-
exp_with_rule = util.replace_eval(exp_string, rules)
461-
expression = self._get_expression(exp_with_rule, functions)
460+
rules = tuple(p_parameters[rule_name] for rule_name in rule_names)
461+
expression.set_expr(self._get_eval_expression(exp_string, rules))
462462

463463
result = expression.eval(parameters)
464464

@@ -546,8 +546,17 @@ def configure_logging(logging_config=None):
546546

547547
@staticmethod
548548
def _get_expression(expr, functions=None):
549+
return SimpleEval(CoreEnforcer._normalize_expression(expr), functions)
550+
551+
@staticmethod
552+
@lru_cache(maxsize=8192)
553+
def _normalize_expression(expr):
549554
expr = expr.replace("&&", "and")
550555
expr = expr.replace("||", "or")
551-
expr = re.sub(r"!(?!=)", "not ", expr)
556+
return re.sub(r"!(?!=)", "not ", expr)
552557

553-
return SimpleEval(expr, functions)
558+
@staticmethod
559+
@lru_cache(maxsize=8192)
560+
def _get_eval_expression(expr, rules):
561+
rules = [util.escape_assertion(rule) for rule in rules]
562+
return CoreEnforcer._normalize_expression(util.replace_eval(expr, rules))

‎casbin/util/expression.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from functools import lru_cache
1516
from simpleeval import EvalWithCompoundTypes
1617
import ast
1718

1819

20+
@lru_cache(maxsize=8192)
21+
def parse_expression(expr):
22+
return ast.parse(expr.strip()).body[0].value
23+
24+
1925
class SimpleEval(EvalWithCompoundTypes):
2026
"""Rewrite SimpleEval.
2127
>>> s = SimpleEval("20 + 30 - ( 10 * 5)")
@@ -29,9 +35,13 @@ def __init__(self, expr, functions=None):
2935
"""Create the evaluator instance. Set up valid operators (+,-, etc)
3036
functions (add, random, get_val, whatever) and names."""
3137
super(SimpleEval, self).__init__(functions=functions)
38+
self.set_expr(expr)
39+
40+
def set_expr(self, expr):
41+
"""replace the expression to evaluate, keeping the functions."""
3242
if expr != "":
3343
self.expr = expr
34-
self.ast_parsed_value = ast.parse(expr.strip()).body[0].value
44+
self.ast_parsed_value = parse_expression(expr)
3545

3646
def eval(self, names=None):
3747
"""evaluate an expression, using the operators, functions and

‎tests/test_enforcer.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,21 @@ def test_abac_with_sub_rule(self):
420420
self.assertFalse(e.enforce(sub3, "/data1", "write"))
421421
self.assertFalse(e.enforce(sub3, "/data2", "write"))
422422

423+
def test_abac_with_updated_sub_rule(self):
424+
e = self.get_enforcer(get_examples("abac_rule_model.conf"), get_examples("abac_rule_policy.csv"))
425+
e.enable_auto_save(False)
426+
427+
sub = MockSub("alice", 16)
428+
self.assertFalse(e.enforce(sub, "/data1", "read"))
429+
430+
e.remove_policy("r.sub.age > 18", "/data1", "read")
431+
e.add_policy("r.sub.age > 10", "/data1", "read")
432+
self.assertTrue(e.enforce(sub, "/data1", "read"))
433+
434+
e.remove_policy("r.sub.age > 10", "/data1", "read")
435+
e.add_policy("r.sub.age > 18", "/data1", "read")
436+
self.assertFalse(e.enforce(sub, "/data1", "read"))
437+
423438
def test_abac_with_multiple_sub_rules(self):
424439
e = self.get_enforcer(
425440
get_examples("abac_multiple_rules_model.conf"),

‎tests/test_fast_enforcer.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import gc
1516
import os
1617
import time
1718
from typing import Sequence
@@ -49,13 +50,17 @@ def test_performance(self) -> None:
4950
N = 5
5051
t_e1_list = []
5152
t_e2_list = []
52-
for _ in range(N):
53-
s_e1 = time.perf_counter()
54-
e1.enforce("alice", "data1", "read")
55-
t_e1_list.append(time.perf_counter() - s_e1)
56-
s_e2 = time.perf_counter()
57-
e2.enforce("alice", "data1", "read")
58-
t_e2_list.append(time.perf_counter() - s_e2)
53+
gc.disable()
54+
try:
55+
for _ in range(N):
56+
s_e1 = time.perf_counter()
57+
e1.enforce("alice", "data1", "read")
58+
t_e1_list.append(time.perf_counter() - s_e1)
59+
s_e2 = time.perf_counter()
60+
e2.enforce("alice", "data1", "read")
61+
t_e2_list.append(time.perf_counter() - s_e2)
62+
finally:
63+
gc.enable()
5964
avg_t_e1 = sum(t_e1_list) / N
6065
avg_t_e2 = sum(t_e2_list) / N
6166
assert avg_t_e1 > avg_t_e2 * 5

0 commit comments

Comments
 (0)