Skip to content
This repository has been archived by the owner on Oct 8, 2024. It is now read-only.

Commit

Permalink
Merge branch 'features/2-cache-generated-expressions' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
willie68 committed Jan 9, 2022
2 parents d7b11ed + ac95f94 commit 6bcaae7
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 241 deletions.
17 changes: 1 addition & 16 deletions api/cel-service.proto
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
syntax = "proto3";
package protofiles;

import "google/protobuf/any.proto";
import "google/protobuf/struct.proto";

option go_package = "./pkg/protofiles";

message CelRequest {
google.protobuf.Struct Context = 1;
string Expression = 2;
}

message ContextValue {
enum Type {
int23 = 0;
int64 = 1;
double = 2;
float = 3;
bool = 4;
string = 5;
bytes = 6;
map = 7;
}
Type type = 1;
google.protobuf.Any value = 2;
string identifier = 3;
}

message CelResponse {
Expand Down
88 changes: 58 additions & 30 deletions internal/celproc/celproc.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import (
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)

var lrucache LRUList

func init() {
lrucache = LRUList{
MaxCount: int(10000),
}
lrucache.Init()
}

func GRPCProcCel(celRequest *protofiles.CelRequest) (*protofiles.CelResponse, error) {
context := convertJson2Map(celRequest.Context.AsMap())
celModel := model.CelModel{
Expand Down Expand Up @@ -61,45 +70,64 @@ func convertJson2Map(src map[string]interface{}) (dst map[string]interface{}) {

func ProcCel(celModel model.CelModel) (model.CelResult, error) {
context := convertJson2Map(celModel.Context)
var declList = make([]*exprpb.Decl, len(context))
x := 0
for k := range context {
declList[x] = decls.NewVar(k, decls.Dyn)
x++
}
env, err := cel.NewEnv(
cel.Declarations(
declList...,
),
)
if err != nil {
log.Logger.Errorf("env declaration error: %s", err)
}
ast, issues := env.Compile(celModel.Expression)
if issues != nil && issues.Err() != nil {
log.Logger.Errorf("type-check error: %v", issues.Err())
return model.CelResult{
Error: fmt.Sprintf("%v", issues.Err()),
Message: issues.Err().Error(),
}, issues.Err()
ok := false
var prg cel.Program
id := celModel.Identifier
if id != "" {
var entry LRUEntry
entry, ok = lrucache.Get(id)
if ok {
prg = entry.Program
}
}
prg, err := env.Program(ast)
if err != nil {
log.Logger.Errorf("program construction error: %v", err)
return model.CelResult{
Error: fmt.Sprintf("%v", err),
Message: fmt.Sprintf("program construction error: %s", err.Error()),
}, err
if !ok {
var declList = make([]*exprpb.Decl, len(context))
x := 0
for k := range context {
declList[x] = decls.NewVar(k, decls.Dyn)
x++
}
env, err := cel.NewEnv(
cel.Declarations(
declList...,
),
)
if err != nil {
log.Logger.Errorf("env declaration error: %s", err)
}
ast, issues := env.Compile(celModel.Expression)
if issues != nil && issues.Err() != nil {
log.Logger.Errorf("type-check error: %v", issues.Err())
return model.CelResult{
Error: fmt.Sprintf("%v", issues.Err()),
Message: issues.Err().Error(),
}, issues.Err()
}
prg, err = env.Program(ast)
if err != nil {
log.Logger.Errorf("program construction error: %v", err)
return model.CelResult{
Error: fmt.Sprintf("%v", err),
Message: fmt.Sprintf("program construction error: %s", err.Error()),
}, err
}
if id != "" {
lrucache.Add(LRUEntry{
ID: id,
Program: prg,
})
go lrucache.HandleContrains()
}
}
out, details, err := prg.Eval(context)
fmt.Printf("result: %v\ndetails: %v\nerror: %v\n", out, details, err)
//fmt.Printf("result: %v\ndetails: %v\nerror: %v\n", out, details, err)

if err != nil {
log.Logger.Errorf("program evaluation error: %v", err)

return model.CelResult{
Error: fmt.Sprintf("%v", err),
Message: fmt.Sprintf("program evaluation error: %s", err.Error()),
Message: fmt.Sprintf("program evaluation error: %s\r\ndetails: %s", err.Error(), details),
}, err
}
switch v := out.(type) {
Expand Down
55 changes: 55 additions & 0 deletions internal/celproc/celproc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"io/ioutil"
"testing"
"time"

"google.golang.org/protobuf/types/known/structpb"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -44,6 +45,47 @@ func TestJson(t *testing.T) {
}
}

func BenchmarkJsonManyWithoutCache(t *testing.B) {
ast := assert.New(t)

celModels := readJsonB("../../test/data/data1.json", t)
stt := time.Now()
for i := 0; i < 10000; i++ {
for _, cm := range celModels {
cm.Request.Context = convertJson2Map(cm.Request.Context)
cm.Request.Identifier = "" //fmt.Sprintf("%d_%d", i, x)
result, err := ProcCel(cm.Request)
ast.Nil(err)
ast.NotNil(result)

ast.Equal(cm.Result, result.Result)
}
}
ste := time.Now()

t.Logf("execution: %d", ste.Sub(stt).Milliseconds())
}

func BenchmarkJsonManyWithCache(t *testing.B) {
ast := assert.New(t)

celModels := readJsonB("../../test/data/data1.json", t)
stt := time.Now()
for i := 0; i < 10000; i++ {
for _, cm := range celModels {
cm.Request.Context = convertJson2Map(cm.Request.Context)
result, err := ProcCel(cm.Request)
ast.Nil(err)
ast.NotNil(result)

ast.Equal(cm.Result, result.Result)
}
}
ste := time.Now()

t.Logf("execution: %d", ste.Sub(stt).Milliseconds())
}

func TestGRPCJson(t *testing.T) {
ast := assert.New(t)

Expand Down Expand Up @@ -88,3 +130,16 @@ func readJson(filename string, t *testing.T) []model.TestCelModel {
ast.Nil(err)
return celModels
}

func readJsonB(filename string, t *testing.B) []model.TestCelModel {
ast := assert.New(t)
ya, err := ioutil.ReadFile(filename)
ast.Nil(err)
var celModels []model.TestCelModel
decoder := json.NewDecoder(bytes.NewReader(ya))
decoder.UseNumber()
err = decoder.Decode(&celModels)
//err = json.Unmarshal(ya, &celModels)
ast.Nil(err)
return celModels
}
161 changes: 161 additions & 0 deletions internal/celproc/lrulist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package celproc

import (
"sort"
"sync"
"time"

"github.com/google/cel-go/cel"
)

type LRUEntry struct {
LastAccess time.Time `json:"lastAccess"`
ID string `json:"id"`
Program cel.Program `json:"description"`
}

type LRUList struct {
MaxCount int
entries []LRUEntry
dmu sync.Mutex
cstLock sync.Mutex
}

func (l *LRUList) Init() {
l.entries = make([]LRUEntry, 0)
}

func (l *LRUList) Size() int {
return len(l.entries)
}

func (l *LRUList) Add(e LRUEntry) bool {
l.dmu.Lock()
defer l.dmu.Unlock()
e.LastAccess = time.Now()
l.entries = l.insertSorted(l.entries, e)
return true
}

func (l *LRUList) Update(e LRUEntry) {
id := e.ID
l.dmu.Lock()
defer l.dmu.Unlock()
i := sort.Search(len(l.entries), func(i int) bool { return l.entries[i].ID >= id })
if i < len(l.entries) && l.entries[i].ID == id {
e.LastAccess = time.Now()
l.entries[i] = e
}
}

func (l *LRUList) UpdateAccess(id string) {
l.dmu.Lock()
defer l.dmu.Unlock()
i := sort.Search(len(l.entries), func(i int) bool { return l.entries[i].ID >= id })
if i < len(l.entries) && l.entries[i].ID == id {
l.entries[i].LastAccess = time.Now()
}
}

func (l *LRUList) GetFullIDList() []string {
l.dmu.Lock()
defer l.dmu.Unlock()
ids := make([]string, len(l.entries))
for x, e := range l.entries {
ids[x] = e.ID
}
return ids
}

func (l *LRUList) HandleContrains() {
l.cstLock.Lock()
defer l.cstLock.Unlock()
for {
id := l.getSingleContrained()
if id != "" {
l.Delete(id)
} else {
break
}
}
}

func (l *LRUList) getSingleContrained() string {
var id string
l.dmu.Lock()
defer l.dmu.Unlock()
if len(l.entries) > int(l.MaxCount) {
// remove oldest entry from cache
oldest := l.getOldest()
id = l.entries[oldest].ID
}
return id
}

func (l *LRUList) Has(id string) bool {
l.dmu.Lock()
defer l.dmu.Unlock()
i := sort.Search(len(l.entries), func(i int) bool { return l.entries[i].ID >= id })
if i < len(l.entries) && l.entries[i].ID == id {
return true
}
return false
}

func (l *LRUList) Get(id string) (LRUEntry, bool) {
l.dmu.Lock()
defer l.dmu.Unlock()
i := sort.Search(len(l.entries), func(i int) bool { return l.entries[i].ID >= id })
if i < len(l.entries) && l.entries[i].ID == id {
l.entries[i].LastAccess = time.Now()
return l.entries[i], true
}
return LRUEntry{}, false
}

func (l *LRUList) Delete(id string) string {
l.dmu.Lock()
defer l.dmu.Unlock()
i := sort.Search(len(l.entries), func(i int) bool { return l.entries[i].ID >= id })
if i < len(l.entries) && l.entries[i].ID == id {
ret := make([]LRUEntry, 0)
ret = append(ret, l.entries[:i]...)
l.entries = append(ret, l.entries[i+1:]...)
return id
}
return ""
}

func (l *LRUList) getOldest() int {
oldest := 0
for x, e := range l.entries {
if e.LastAccess.Before(l.entries[oldest].LastAccess) {
oldest = x
}
}
return oldest
}

func (l *LRUList) insertSorted(data []LRUEntry, v LRUEntry) []LRUEntry {
i := sort.Search(len(data), func(i int) bool { return data[i].ID >= v.ID })
return l.insertEntryAt(data, i, v)
}

func (l *LRUList) insertEntryAt(data []LRUEntry, i int, v LRUEntry) []LRUEntry {
if i == len(data) {
// Insert at end is the easy case.
return append(data, v)
}

// Make space for the inserted element by shifting
// values at the insertion index up one index. The call
// to append does not allocate memory when cap(data) is
// greater ​than len(data).
data = append(data[:i+1], data[i:]...)

// Insert the new element.
data[i] = v

// Return the updated slice.
return data
}
1 change: 1 addition & 0 deletions pkg/model/celmodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package model
type CelModel struct {
Context map[string]interface{} `yaml:"context" json:"context"`
Expression string `yaml:"expression" json:"expression"`
Identifier string `yaml:"identifier" json:"identifier"`
}

type CelResult struct {
Expand Down
Loading

0 comments on commit 6bcaae7

Please sign in to comment.