Skip to content

Commit

Permalink
Expose Compile alongside Parse with CEL compilers
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyhb committed Jan 9, 2024
1 parent 63ecd3f commit 19e459e
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 163 deletions.
83 changes: 83 additions & 0 deletions caching_compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package expr

import (
"sync/atomic"
"time"

"github.com/google/cel-go/cel"
"github.com/karlseguin/ccache/v2"
)

var (
CacheTime = time.Hour
)

// NewCachingCompiler returns a CELCompiler which lifts quoted literals out of the expression
// as variables and uses caching to cache expression parsing, resulting in improved
// performance when parsing expressions.
func NewCachingCompiler(env *cel.Env, cache *ccache.Cache) CELCompiler {
return &cachingCompiler{
cache: cache,
env: env,
}
}

type cachingCompiler struct {
// cache is a global cache of precompiled expressions.
cache *ccache.Cache

env *cel.Env

hits int64
misses int64
}

// Parse calls
func (c *cachingCompiler) Parse(expr string) (*cel.Ast, *cel.Issues, LiftedArgs) {
if c.cache == nil {
c.cache = ccache.New(ccache.Configure())
}

expr, vars := liftLiterals(expr)

if cached := c.cache.Get("cc:" + expr); cached != nil {
cached.Extend(CacheTime)
p := cached.Value().(parsedCELExpr)
atomic.AddInt64(&c.hits, 1)
return p.AST, p.ParseIssues, vars
}

ast, issues := c.env.Parse(expr)

c.cache.Set("cc:"+expr, parsedCELExpr{
Expr: expr,
AST: ast,
ParseIssues: issues,
}, CacheTime)

atomic.AddInt64(&c.misses, 1)
return ast, issues, vars
}

func (c *cachingCompiler) Compile(expr string) (*cel.Ast, *cel.Issues, LiftedArgs) {
ast, issues, args := c.Parse(expr)
if issues != nil {
return ast, issues, args
}
ast, issues = c.env.Check(ast)
return ast, issues, args
}

func (c *cachingCompiler) Hits() int64 {
return atomic.LoadInt64(&c.hits)
}

func (c *cachingCompiler) Misses() int64 {
return atomic.LoadInt64(&c.misses)
}

type parsedCELExpr struct {
Expr string
AST *cel.Ast
ParseIssues *cel.Issues
}
62 changes: 9 additions & 53 deletions caching_parser_test.go → caching_coompiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestCachingParser_CachesSame(t *testing.T) {
c := cachingParser{env: newEnv()}
c := cachingCompiler{env: newEnv()}

a := `event.data.a == "cache"`
b := `event.data.b == "cache"`
Expand All @@ -20,7 +20,7 @@ func TestCachingParser_CachesSame(t *testing.T) {
)

t.Run("With an uncached expression", func(t *testing.T) {
prevAST, prevIssues, prevVars = c.Parse(a)
prevAST, prevIssues, prevVars = c.Compile(a)
require.NotNil(t, prevAST)
require.Nil(t, prevIssues)
require.NotNil(t, prevVars)
Expand All @@ -29,7 +29,7 @@ func TestCachingParser_CachesSame(t *testing.T) {
})

t.Run("With a cached expression", func(t *testing.T) {
ast, issues, vars := c.Parse(a)
ast, issues, vars := c.Compile(a)
require.NotNil(t, ast)
require.Nil(t, issues)

Expand All @@ -42,7 +42,7 @@ func TestCachingParser_CachesSame(t *testing.T) {
})

t.Run("With another uncached expression", func(t *testing.T) {
prevAST, prevIssues, prevVars = c.Parse(b)
prevAST, prevIssues, prevVars = c.Compile(b)
require.NotNil(t, prevAST)
require.Nil(t, prevIssues)
// This misses the cache, as the vars have changed - not the
Expand All @@ -52,8 +52,8 @@ func TestCachingParser_CachesSame(t *testing.T) {
})
}

func TestCachingParser_CacheIgnoreLiterals_Unescaped(t *testing.T) {
c := cachingParser{env: newEnv()}
func TestCachingCompile(t *testing.T) {
c := cachingCompiler{env: newEnv()}

a := `event.data.a == "literal-a" && event.data.b == "yes-1"`
b := `event.data.a == "literal-b" && event.data.b == "yes-2"`
Expand All @@ -65,15 +65,15 @@ func TestCachingParser_CacheIgnoreLiterals_Unescaped(t *testing.T) {
)

t.Run("With an uncached expression", func(t *testing.T) {
prevAST, prevIssues, prevVars = c.Parse(a)
prevAST, prevIssues, prevVars = c.Compile(a)
require.NotNil(t, prevAST)
require.Nil(t, prevIssues)
require.EqualValues(t, 0, c.Hits())
require.EqualValues(t, 1, c.Misses())
})

t.Run("With a cached expression", func(t *testing.T) {
ast, issues, vars := c.Parse(a)
ast, issues, vars := c.Compile(a)
require.NotNil(t, ast)
require.Nil(t, issues)

Expand All @@ -86,55 +86,11 @@ func TestCachingParser_CacheIgnoreLiterals_Unescaped(t *testing.T) {
})

t.Run("With a cached expression having different literals ONLY", func(t *testing.T) {
prevAST, prevIssues, _ = c.Parse(b)
prevAST, prevIssues, _ = c.Compile(b)
require.NotNil(t, prevAST)
require.Nil(t, prevIssues)
// This misses the cache.
require.EqualValues(t, 2, c.Hits())
require.EqualValues(t, 1, c.Misses())
})
}

/*
func TestCachingParser_CacheIgnoreLiterals_Escaped(t *testing.T) {
return
c := cachingParser{env: newEnv()}
a := `event.data.a == "literal\"-a" && event.data.b == "yes"`
b := `event.data.a == "literal\"-b" && event.data.b == "yes"`
var (
prevAST *cel.Ast
prevIssues *cel.Issues
)
t.Run("With an uncached expression", func(t *testing.T) {
prevAST, prevIssues = c.Parse(a)
require.NotNil(t, prevAST)
require.Nil(t, prevIssues)
require.EqualValues(t, 0, c.Hits())
require.EqualValues(t, 1, c.Misses())
})
t.Run("With a cached expression", func(t *testing.T) {
ast, issues := c.Parse(a)
require.NotNil(t, ast)
require.Nil(t, issues)
require.Equal(t, prevAST, ast)
require.Equal(t, prevIssues, issues)
require.EqualValues(t, 1, c.Hits())
require.EqualValues(t, 1, c.Misses())
})
t.Run("With a cached expression having different literals ONLY", func(t *testing.T) {
prevAST, prevIssues = c.Parse(b)
require.NotNil(t, prevAST)
require.Nil(t, prevIssues)
// This misses the cache.
require.EqualValues(t, 2, c.Hits())
require.EqualValues(t, 1, c.Misses())
})
}
*/
73 changes: 0 additions & 73 deletions caching_parser.go

This file was deleted.

25 changes: 25 additions & 0 deletions evaluable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package expr

// Evaluable represents an evaluable expression with a unique identifier.
type Evaluable interface {
// GetID returns a unique identifier for the evaluable item. If there are
// two instances of the same expression, the identifier should return a unique
// string for each instance of the expression (eg. for two pauses).
//
// It has the Get prefix to reduce collisions with implementations who expose an
// ID member.
GetID() string

// GetExpression returns an expression as a raw string.
//
// It has the Get prefix to reduce collisions with implementations who expose an
// Expression member.
GetExpression() string
}

// StringExpression is a string type that implements Evaluable, useful for basic
// ephemeral expressions that have no lifetime.
type StringExpression string

func (s StringExpression) GetID() string { return string(s) }
func (s StringExpression) GetExpression() string { return string(s) }
17 changes: 0 additions & 17 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,6 @@ func NewAggregateEvaluator(
}
}

// Evaluable represents an evaluable expression with a unique identifier.
type Evaluable interface {
// GetID returns a unique identifier for the evaluable item. If there are
// two instances of the same expression, the identifier should return a unique
// string for each instance of the expression (eg. for two pauses).
//
// It has the Get prefix to reduce collisions with implementations who expose an
// ID member.
GetID() string

// GetExpression returns an expression as a raw string.
//
// It has the Get prefix to reduce collisions with implementations who expose an
// Expression member.
GetExpression() string
}

type aggregator struct {
eval ExpressionEvaluator
parser TreeParser
Expand Down
12 changes: 6 additions & 6 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (
)

func BenchmarkCachingEvaluate1_000(b *testing.B) {
benchEval(1_000, NewCachingParser(newEnv(), nil), b)
benchEval(1_000, NewCachingCompiler(newEnv(), nil), b)
}

// func BenchmarkNonCachingEvaluate1_000(b *testing.B) { benchEval(1_000, EnvParser(newEnv()), b) }
func benchEval(i int, p CELParser, b *testing.B) {
func benchEval(i int, p CELCompiler, b *testing.B) {
for n := 0; n < b.N; n++ {
parser := NewTreeParser(p)
_ = evaluate(b, i, parser)
Expand Down Expand Up @@ -57,7 +57,7 @@ func evaluate(b *testing.B, i int, parser TreeParser) error {

func TestEvaluate_Strings(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingParser(newEnv(), nil))
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))
e := NewAggregateEvaluator(parser, testBoolEvaluator)

expected := tex(`event.data.account_id == "yes" && event.data.match == "true"`)
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestEvaluate_Strings(t *testing.T) {

func TestEvaluate_Concurrently(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingParser(newEnv(), nil))
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))
e := NewAggregateEvaluator(parser, testBoolEvaluator)

expected := tex(`event.data.account_id == "yes" && event.data.match == "true"`)
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestEvaluate_Concurrently(t *testing.T) {

func TestEvaluate_ArrayIndexes(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingParser(newEnv(), nil))
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))
e := NewAggregateEvaluator(parser, testBoolEvaluator)

expected := tex(`event.data.ids[1] == "id-b" && event.data.ids[2] == "id-c"`)
Expand Down Expand Up @@ -192,7 +192,7 @@ func TestEvaluate_ArrayIndexes(t *testing.T) {

func TestEvaluate_Compound(t *testing.T) {
ctx := context.Background()
parser := NewTreeParser(NewCachingParser(newEnv(), nil))
parser := NewTreeParser(NewCachingCompiler(newEnv(), nil))
e := NewAggregateEvaluator(parser, testBoolEvaluator)

expected := tex(`event.data.a == "ok" && event.data.b == "yes" && event.data.c == "please"`)
Expand Down
Loading

0 comments on commit 19e459e

Please sign in to comment.