Skip to content

Commit 232d6a5

Browse files
Performance improvements to CEL rule evaluation
Three improvements to hot-path validation, inspired by protovalidate-go. Guard `_validate_cel` when `self._cel` is empty (bufbuild/protovalidate-go#261) Adds an early return in `CelRules._validate_cel` when there are no CEL runners, skipping activation dict creation and `datetime.now()` entirely. Also guards the `_msg_to_cel` call in `MessageRules.validate()` so the message-to-CEL conversion is skipped when there are no CEL rules to run. Benchmark (required-only field, 0 CEL runners): -37%. Skip `now` computation when unused (bufbuild/protovalidate-go#289) Adds `_uses_now` to `CelRules`, set at compile time in `add_rule` by checking whether `"now"` appears in the expression string. Only `timestamp.gt_now`, `timestamp.lt_now`, `timestamp.within`, and custom expressions referencing `now` will call `datetime.datetime.now()`. Benchmarks: int32.gt (5 CEL runners) -25%, timestamp.gt_now -6%. Early-exit loop in `cel_unique` (bufbuild/protovalidate-go#289) Replaces `len(val) == len(set(val))` with a loop that returns `False` on the first duplicate, avoiding building the full set unnecessarily.
1 parent a4d0988 commit 232d6a5

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

protovalidate/internal/extra_func.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,12 @@ def cel_unique(val: celtypes.Value) -> celpy.Result:
328328
if not isinstance(val, celtypes.ListType | list):
329329
msg = "invalid argument, expected list"
330330
raise celpy.CELEvalError(msg)
331-
return celtypes.BoolType(len(val) == len(set(val)))
331+
seen: set[celtypes.Value] = set()
332+
for item in val:
333+
if item in seen:
334+
return celtypes.BoolType(False) # noqa: FBT003
335+
seen.add(item)
336+
return celtypes.BoolType(True) # noqa: FBT003
332337

333338

334339
class Ipv4:

protovalidate/internal/rules.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ class CelRules(Rules):
342342
_cel: list[CelRunner]
343343
_rules: message.Message | None = None
344344
_rules_cel: celtypes.Value | None = None
345+
_uses_now: bool = False
345346

346347
def __init__(self, rules: message.Message | None):
347348
self._cel = []
@@ -357,11 +358,14 @@ def _validate_cel(
357358
this_cel: celtypes.Value | None = None,
358359
for_key: bool = False,
359360
):
361+
if not self._cel:
362+
return
360363
activation: dict[str, celtypes.Value] = {}
361364
if this_cel is not None:
362365
activation["this"] = this_cel
363366
activation["rules"] = self._rules_cel
364-
activation["now"] = celtypes.TimestampType(datetime.datetime.now(tz=datetime.timezone.utc))
367+
if self._uses_now:
368+
activation["now"] = celtypes.TimestampType(datetime.datetime.now(tz=datetime.timezone.utc))
365369
for cel in self._cel:
366370
activation["rule"] = cel.rule_cel
367371
result = cel.runner.evaluate(activation)
@@ -409,6 +413,8 @@ def add_rule(
409413
rules = validate_pb2.Rule()
410414
rules.id = expression
411415
rules.expression = expression
416+
if "now" in rules.expression:
417+
self._uses_now = True
412418
ast = env.compile(rules.expression)
413419
prog = env.program(ast, functions=funcs)
414420
rule_value = None
@@ -463,9 +469,10 @@ def __init__(self, rules: message.Message | None, desc: descriptor.Descriptor):
463469
self._desc = desc
464470

465471
def validate(self, ctx: RuleContext, message: message.Message):
466-
self._validate_cel(ctx, this_cel=_msg_to_cel(message))
467-
if ctx.done:
468-
return
472+
if self._cel:
473+
self._validate_cel(ctx, this_cel=_msg_to_cel(message))
474+
if ctx.done:
475+
return
469476
for oneof in self._oneofs:
470477
oneof.validate(ctx, message)
471478
if ctx.done:

0 commit comments

Comments
 (0)