Skip to content

Commit

Permalink
feat: support caching even if there're template functions
Browse files Browse the repository at this point in the history
  • Loading branch information
adityathebe authored and moshloop committed Jan 11, 2024
1 parent 76a2cf7 commit eb755aa
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 59 deletions.
3 changes: 3 additions & 0 deletions structtemplater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type test struct {

var tests = []test{
{
name: "template and no template",
StructTemplater: StructTemplater{
RequiredTag: "template",
Values: map[string]interface{}{
Expand All @@ -44,6 +45,7 @@ var tests = []test{
},
},
{
name: "just template",
StructTemplater: StructTemplater{
DelimSets: []Delims{
{Left: "{{", Right: "}}"},
Expand All @@ -62,6 +64,7 @@ var tests = []test{
},
},
{
name: "template & no template but with maps",
StructTemplater: StructTemplater{
RequiredTag: "template",
DelimSets: []Delims{
Expand Down
65 changes: 35 additions & 30 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os"
"reflect"
"sort"
"strings"
gotemplate "text/template"
Expand All @@ -24,8 +25,9 @@ import (
var funcMap gotemplate.FuncMap

var (
goTemplateCache = cache.New(24*time.Hour, 24*time.Hour)
celExpressionCache = cache.New(24*time.Hour, 24*time.Hour)
// keep the cache period low as lots of anonymous functions can pile up the cache.
goTemplateCache = cache.New(time.Hour, time.Hour)
celExpressionCache = cache.New(time.Hour, time.Hour)
)

func init() {
Expand All @@ -42,6 +44,27 @@ type Template struct {
LeftDelim string `yaml:"-" json:"-"`
}

func (t Template) CacheKey(env map[string]any) string {
envVars := make([]string, 0, len(env)+1)
for k := range env {
envVars = append(envVars, k)
}
sort.Slice(envVars, func(i, j int) bool { return envVars[i] < envVars[j] })

funcNames := make([]string, 0, len(t.Functions))
for k := range t.Functions {
funcNames = append(funcNames, k)
}
sort.Slice(funcNames, func(i, j int) bool { return funcNames[i] < funcNames[j] })

funcKeys := make([]string, 0, len(t.Functions))
for _, fnName := range funcNames {
funcKeys = append(funcKeys, fmt.Sprintf("%d", reflect.ValueOf(t.Functions[fnName]).Pointer()))
}

return strings.Join(envVars, "-") + strings.Join(funcKeys, "-") + t.RightDelim + t.LeftDelim + t.Expression + t.Javascript + t.JSONPath + t.Template
}

func (t Template) IsEmpty() bool {
return t.Template == "" && t.JSONPath == "" && t.Expression == "" && t.Javascript == ""
}
Expand All @@ -68,13 +91,10 @@ func RunExpression(_environment map[string]any, template Template) (any, error)
}

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
}
cached, ok := celExpressionCache.Get(template.CacheKey(_environment))
if ok {
if cachedPrg, ok := cached.(*cel.Program); ok {
prg = *cachedPrg
}
}

Expand All @@ -94,7 +114,7 @@ func RunExpression(_environment map[string]any, template Template) (any, error)
return "", err
}

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

out, _, err := prg.Eval(data)
Expand Down Expand Up @@ -146,14 +166,10 @@ func RunTemplate(environment map[string]any, template Template) (string, error)

func goTemplate(template Template, environment map[string]any) (string, error) {
var tpl *gotemplate.Template
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 := goTemplateCache.Get(cacheKey(nil, template.Template))
if ok {
if cachedTpl, ok := cached.(*gotemplate.Template); ok {
tpl = cachedTpl
}
cached, ok := goTemplateCache.Get(template.CacheKey(nil))
if ok {
if cachedTpl, ok := cached.(*gotemplate.Template); ok {
tpl = cachedTpl
}
}

Expand All @@ -177,7 +193,7 @@ func goTemplate(template Template, environment map[string]any) (string, error) {
return "", err
}

goTemplateCache.SetDefault(cacheKey(nil, template.Template), tpl)
goTemplateCache.SetDefault(template.CacheKey(nil), tpl)
}

data, err := Serialize(environment)
Expand Down Expand Up @@ -205,14 +221,3 @@ func LoadSharedLibrary(source string) error {
registry.Register(func() string { return string(data) })
return nil
}

// cacheKey for cel expressions & go templates
func cacheKey(env map[string]any, expr string) string {
cacheKey := make([]string, 0, len(env)+1)
for k := range env {
cacheKey = append(cacheKey, k)
}

sort.Slice(cacheKey, func(i, j int) bool { return cacheKey[i] < cacheKey[j] })
return strings.Join(cacheKey, "-") + "--" + expr
}
73 changes: 44 additions & 29 deletions template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,54 @@ import (
_ "github.com/robertkrimen/otto/underscore"
)

func TestCacheKey(t *testing.T) {
type args struct {
env map[string]any
expr string
func TestCacheKeyConsistency(t *testing.T) {
var hello = func() any {
return "world"
}

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}}",
},
{
name: "simple",
args: args{
expr: "{{.name}}{{.age}}{{.profession}}",
env: map[string]any{"profession": "software engineer", "age": 28, "name": "lex"},
var foo = func() any {
return "bar"
}

{
tt := Template{
Expression: "{{.name}}{{.age}}",
Functions: map[string]func() any{
"hello": hello,
"Hello": foo,
"foo": foo,
"Foo": foo,
},
want: "age-name-profession--{{.name}}{{.age}}{{.profession}}",
},
}

expectedCacheKey := tt.CacheKey(map[string]any{"age": 19, "name": "james"})
for i := 0; i < 10; i++ {
key := tt.CacheKey(map[string]any{"age": 19, "name": "james"})
if key != expectedCacheKey {
t.Errorf("cache key mismatch: %s != %s", key, expectedCacheKey)
}
}
}
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)

{
tt := Template{
Template: "{{.name}}{{.age}}",
LeftDelim: "{{",
RightDelim: "}}",
Functions: map[string]func() any{
"hello": hello,
"Hello": foo,
"foo": foo,
"Foo": foo,
},
}

expectCacheKey := tt.CacheKey(map[string]any{"age": 19, "name": "james"})
for i := 0; i < 10; i++ {
key := tt.CacheKey(map[string]any{"age": 19, "name": "james"})
if key != expectCacheKey {
t.Errorf("cache key mismatch: %s != %s", key, expectCacheKey)
}
})
}
}
}

0 comments on commit eb755aa

Please sign in to comment.