Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ func NewAggregateEvaluator[T Evaluable](
},
lock: &sync.RWMutex{},
constants: map[uuid.UUID]struct{}{},
alwaysTrue: map[uuid.UUID]struct{}{},
mixed: map[uuid.UUID]struct{}{},
stopGC: make(chan struct{}),
concurrency: opts.Concurrency,
Expand Down Expand Up @@ -239,6 +240,10 @@ type aggregator[T Evaluable] struct {
// the expression containing non-aggregateable clauses.
constants map[uuid.UUID]struct{}

// alwaysTrue tracks evaluable IDs whose expression is a constant true literal,
// meaning they always match without requiring CEL evaluation.
alwaysTrue map[uuid.UUID]struct{}

// deleted tracks evaluable IDs that have been soft-deleted.
// Remove operations mark items here instead of actually removing them,
// avoiding lock contention during evaluation.
Expand All @@ -261,7 +266,7 @@ type aggregator[T Evaluable] struct {
func (a *aggregator[T]) Len() int {
a.lock.RLock()
defer a.lock.RUnlock()
return int(atomic.LoadInt32(&a.fastLen)) + len(a.mixed) + len(a.constants)
return int(atomic.LoadInt32(&a.fastLen)) + len(a.mixed) + len(a.constants) + len(a.alwaysTrue)
}

// FastLen returns the number of expressions being matched by aggregated trees.
Expand Down Expand Up @@ -308,6 +313,22 @@ func (a *aggregator[T]) Evaluate(ctx context.Context, data map[string]any) ([]T,
napool := newErrPool(errPoolOpts{concurrency: a.concurrency})

a.lock.RLock()

// Always-true expressions match without CEL evaluation.
for uuid := range a.alwaysTrue {
if _, deleted := a.deleted.Load(uuid); deleted {
continue
}
item, err := a.kv.Get(uuid)
if err != nil {
continue
}
atomic.AddInt32(&matched, 1)
s.Lock()
result = append(result, item)
s.Unlock()
}

for uuid := range a.constants {
// Skip deleted items
if _, deleted := a.deleted.Load(uuid); deleted {
Expand Down Expand Up @@ -492,6 +513,20 @@ func (a *aggregator[T]) Add(ctx context.Context, eval T) (float64, error) {
return -1, err
}

if parsed.LiteralBool != nil {
if !*parsed.LiteralBool {
// This is a constant false expression which never matches.
// Skip adding it entirely to avoid unnecessary evaluation.
return -1, nil
}
// This is a constant true expression which always matches.
// Add it to the always-true list for fast evaluation without CEL.
a.lock.Lock()
a.alwaysTrue[parsed.EvaluableID] = struct{}{}
a.lock.Unlock()
return -1, nil
}

if eval.GetExpression() == "" || parsed.HasMacros {
// This is an empty expression which always matches.
a.lock.Lock()
Expand Down Expand Up @@ -739,6 +774,7 @@ func (a *aggregator[T]) GC(ctx context.Context) bool {
a.lock.Lock()
for _, id := range constantsToDelete {
delete(a.constants, id)
delete(a.alwaysTrue, id)
}
for _, id := range mixedToDelete {
delete(a.mixed, id)
Expand Down
63 changes: 63 additions & 0 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,69 @@ func TestAdd(t *testing.T) {
require.Equal(t, 1, e.SlowLen())
}

func TestAdd_LiteralFalse(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))

e := NewAggregateEvaluator(AggregateEvaluatorOpts[testEvaluable]{
Parser: parser,
Eval: testBoolEvaluator,
Concurrency: 0,
})
defer e.Close()

// Add a literal false expression
expr := tex(`false`)
_, err := e.Add(ctx, expr)
require.NoError(t, err)

// Literal false should not be added to any evaluation path
require.Equal(t, 0, e.Len())
require.Equal(t, 0, e.FastLen())
require.Equal(t, 0, e.MixedLen())
require.Equal(t, 0, e.SlowLen())

// Evaluating should return no matches
results, matched, err := e.Evaluate(ctx, map[string]any{
"event": map[string]any{"data": map[string]any{"foo": "bar"}},
})
require.NoError(t, err)
require.Equal(t, int32(0), matched)
require.Len(t, results, 0)
}

func TestAdd_LiteralTrue(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))

e := NewAggregateEvaluator(AggregateEvaluatorOpts[testEvaluable]{
Parser: parser,
Eval: testBoolEvaluator,
Concurrency: 0,
})
defer e.Close()

// Add a literal true expression
expr := tex(`true`)
_, err := e.Add(ctx, expr)
require.NoError(t, err)

// Literal true should be tracked but not in the slow path
require.Equal(t, 1, e.Len())
require.Equal(t, 0, e.FastLen())
require.Equal(t, 0, e.MixedLen())
require.Equal(t, 0, e.SlowLen())

// Evaluating should always match the literal true expression
results, matched, err := e.Evaluate(ctx, map[string]any{
"event": map[string]any{"data": map[string]any{"foo": "bar"}},
})
require.NoError(t, err)
require.Equal(t, int32(1), matched)
require.Len(t, results, 1)
require.Equal(t, expr.GetExpression(), results[0].GetExpression())
}

func TestEvaluate_Strings(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))
Expand Down
21 changes: 21 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,27 @@ func (p *parser) Parse(ctx context.Context, eval Evaluable) (*ParsedExpression,
}

node.normalize()

// Check if the expression is a constant boolean literal (true or false).
// A bare literal has no predicates, no ands, no ors after normalization.
var literalBool *bool
if !node.HasPredicate() && len(node.Ands) == 0 && len(node.Ors) == 0 && !hasMacros {
nativeExpr := ast.NativeRep().Expr()
if nativeExpr.Kind() == celast.LiteralKind {
if val := nativeExpr.AsLiteral(); val != nil {
if boolVal, ok := val.Value().(bool); ok {
literalBool = &boolVal
}
}
}
}

return &ParsedExpression{
Root: *node,
Vars: vars,
EvaluableID: eval.GetID(),
HasMacros: hasMacros,
LiteralBool: literalBool,
}, nil
}

Expand All @@ -154,6 +170,11 @@ type ParsedExpression struct {
EvaluableID uuid.UUID

HasMacros bool

// LiteralBool is non-nil when the expression is a constant boolean literal.
// When false, the expression never matches any input.
// When true, the expression always matches any input.
LiteralBool *bool
}

// RootGroups returns the top-level matching groups within an expression. This is a small
Expand Down
Loading