Skip to content

Commit

Permalink
feat: add cache to cel expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
adityathebe authored and moshloop committed Jan 11, 2024
1 parent d9e5f78 commit 5326673
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 14 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/itchyny/gojq v0.12.14
github.com/mitchellh/reflectwalk v1.0.2
github.com/ohler55/ojg v1.20.2
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/robertkrimen/otto v0.2.1
github.com/stretchr/testify v1.8.4
Expand All @@ -22,6 +23,7 @@ require (
golang.org/x/tools v0.16.1
google.golang.org/protobuf v1.32.0
gotest.tools/v3 v3.5.1
k8s.io/api v0.28.2
k8s.io/apimachinery v0.28.2
sigs.k8s.io/yaml v1.3.0
)
Expand Down Expand Up @@ -54,7 +56,6 @@ require (
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.28.2 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ohler55/ojg v1.20.2 h1:ragZXnawcsq/knqIHaflvIX/8RzzAKMZSluAZAz+RFs=
github.com/ohler55/ojg v1.20.2/go.mod h1:uHcD1ErbErC27Zhb5Df2jUjbseLLcmOCo6oxSr3jZxo=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
60 changes: 47 additions & 13 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"context"
"fmt"
"os"
"slices"
"strings"
gotemplate "text/template"
"time"

_ "github.com/flanksource/gomplate/v3/js"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"github.com/robertkrimen/otto"
"github.com/robertkrimen/otto/registry"
Expand All @@ -20,6 +23,11 @@ import (

var funcMap gotemplate.FuncMap

var (
goTemplateCache = cache.New(24*time.Hour, 24*time.Hour)
celExpressionCache = cache.New(24*time.Hour, 24*time.Hour)
)

func init() {
funcMap = CreateFuncs(context.Background())
}
Expand All @@ -39,17 +47,16 @@ func (t Template) IsEmpty() bool {
}

func RunExpression(_environment map[string]any, template Template) (any, error) {

data, err := Serialize(_environment)
if err != nil {
return "", err
}

funcs := GetCelEnv(data)
envOptions := GetCelEnv(data)
for name, fn := range template.Functions {
_name := name
_fn := fn
funcs = append(funcs, cel.Function(_name, cel.Overload(
envOptions = append(envOptions, cel.Function(_name, cel.Overload(
_name,
nil,
cel.AnyType,
Expand All @@ -60,18 +67,34 @@ func RunExpression(_environment map[string]any, template Template) (any, error)
)))
}

env, err := cel.NewEnv(funcs...)
if err != nil {
return "", err
}
ast, issues := env.Compile(strings.ReplaceAll(template.Expression, "\n", " "))
if issues != nil && issues.Err() != nil {
return "", issues.Err()
var prg cel.Program
if len(template.Functions) == 0 {
// only use cache if there's no custom template functions because we can't hash those functions to use them as a cache key
cached, ok := celExpressionCache.Get(cacheKey(_environment, template.Expression))
if ok {
if cachedPrg, ok := cached.(*cel.Program); ok {
prg = *cachedPrg
}
}
}

prg, err := env.Program(ast, cel.Globals(data))
if err != nil {
return "", err
if prg == nil {
env, err := cel.NewEnv(envOptions...)
if err != nil {
return "", err
}

ast, issues := env.Compile(strings.ReplaceAll(template.Expression, "\n", " "))
if issues != nil && issues.Err() != nil {
return "", issues.Err()
}

prg, err = env.Program(ast, cel.Globals(data))
if err != nil {
return "", err
}

celExpressionCache.SetDefault(cacheKey(_environment, template.Expression), &prg)
}

out, _, err := prg.Eval(data)
Expand Down Expand Up @@ -170,3 +193,14 @@ func LoadSharedLibrary(source string) error {
registry.Register(func() string { return string(data) })
return nil
}

// cacheKey for cel expressions
func cacheKey(env map[string]any, expr string) string {
var cacheKey []string
for k := range env {
cacheKey = append(cacheKey, k)
}
slices.Sort(cacheKey)

return strings.Join(cacheKey, "-") + "--" + expr
}
37 changes: 37 additions & 0 deletions template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gomplate

import (
"testing"

_ "github.com/flanksource/gomplate/v3/js"
_ "github.com/robertkrimen/otto/underscore"
)

func Test_hashFunction(t *testing.T) {
type args struct {
env map[string]any
expr string
}

tests := []struct {
name string
args args
want string
}{
{
name: "simple",
args: args{
expr: "{{.name}}{{.age}}",
env: map[string]any{"age": 19, "name": "james"},
},
want: "age-name--{{.name}}{{.age}}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cacheKey(tt.args.env, tt.args.expr); got != tt.want {
t.Errorf("hashFunction() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 5326673

Please sign in to comment.