From ed4a9f231515ad5c4b7765cd64fabcad5e8a417a Mon Sep 17 00:00:00 2001 From: Jon Park Date: Thu, 30 May 2019 08:22:15 -0700 Subject: [PATCH 1/4] readonly cache --- common/model/tuple.go | 20 ++++- common/model/tupledescriptor.go | 51 ++++++++++--- common/model/types.go | 7 +- config/config.go | 15 +++- examples/flogo/solar/README.md | 19 +++++ examples/flogo/solar/flogo.json | 122 ++++++++++++++++++++++++++++++ examples/flogo/solar/functions.go | 52 +++++++++++++ examples/flogo/solar/main.go | 67 ++++++++++++++++ go.mod | 1 + ruleaction/action.go | 21 ++++- ruleapi/rulesession.go | 1 + 11 files changed, 357 insertions(+), 19 deletions(-) create mode 100644 examples/flogo/solar/README.md create mode 100644 examples/flogo/solar/flogo.json create mode 100644 examples/flogo/solar/functions.go create mode 100644 examples/flogo/solar/main.go diff --git a/common/model/tuple.go b/common/model/tuple.go index d7e3bd3..a00e2ca 100644 --- a/common/model/tuple.go +++ b/common/model/tuple.go @@ -10,6 +10,18 @@ import ( var reteCTXKEY = RetecontextKeyType{} +//OMMode is uuple's object management mode. 2 modes are supported currently. +//ToDo: should this be moved to tupledescriptor? +type OMMode int + +//Tuple's object management mode. For now, only 2 are supported. +const ( + InMemory OMMode = iota + ReadOnlyCache +) + +var OMModeMap = map[string]OMMode{"InMemory": InMemory, "ReadOnlyCache": ReadOnlyCache} + //Tuple is a runtime representation of a data tuple type Tuple interface { GetTupleType() TupleType @@ -23,6 +35,7 @@ type Tuple interface { GetBool(name string) (val bool, err error) //GetDateTime(name string) time.Time GetKey() TupleKey + GetValues() map[string]interface{} } //MutableTuple mutable part of the tuple @@ -82,6 +95,10 @@ func (t *tupleImpl) GetTupleDescriptor() *TupleDescriptor { return t.td } +func (t *tupleImpl) GetValues() map[string]interface{} { + return t.tuples +} + func (t *tupleImpl) GetProperties() []string { keys := []string{} for k := range t.tuples { @@ -144,6 +161,7 @@ func (t *tupleImpl) GetBool(name string) (val bool, err error) { return v, err } + func (t *tupleImpl) SetString(ctx context.Context, name string, value string) (err error) { return t.validateAndCallListener(ctx, name, value) } @@ -210,7 +228,7 @@ func (t *tupleImpl) initTupleWithKeyValues(td *TupleDescriptor, values ...interf t.key = tk //populate the tuple key fields with the key values for _, keyProp := range td.GetKeyProps() { - t.tuples [keyProp] = tk.GetValue(keyProp) + t.tuples[keyProp] = tk.GetValue(keyProp) } return err } diff --git a/common/model/tupledescriptor.go b/common/model/tupledescriptor.go index b54e897..d870b10 100644 --- a/common/model/tupledescriptor.go +++ b/common/model/tupledescriptor.go @@ -5,7 +5,6 @@ import ( "encoding/json" "sort" "strconv" - "sync" "fmt" @@ -13,7 +12,8 @@ import ( ) var ( - typeRegistry sync.Map + //typeRegistry sync.Map + typeRegistry = make(map[string]interface{}) ) //TupleType Each tuple is of a certain type, described by TypeDescriptor @@ -25,6 +25,7 @@ type TupleDescriptor struct { TTLInSeconds int `json:"ttl"` Props []TuplePropertyDescriptor `json:"properties"` keyProps []string + PersistMode string `json:"persistMode"` } // TuplePropertyDescriptor defines the actual property, its type, key index @@ -42,7 +43,11 @@ func RegisterTupleDescriptors(jsonRegistry string) (err error) { return err } for _, key := range tds { - typeRegistry.LoadOrStore(TupleType(key.Name), key) + //typeRegistry.LoadOrStore(TupleType(key.Name), key) + if typeRegistry[key.Name] != nil { + return fmt.Errorf("TupleDescriptor already exists for [%s]", key.Name) + } + typeRegistry[key.Name] = key } return nil } @@ -53,20 +58,42 @@ func RegisterTupleDescriptorsFromTds(tds []TupleDescriptor) (err error) { return err } for _, key := range tds { - typeRegistry.LoadOrStore(TupleType(key.Name), key) + //typeRegistry.LoadOrStore(TupleType(key.Name), key) + if typeRegistry[key.Name] != nil { + return fmt.Errorf("TupleDescriptor already exists for [%s]", key.Name) + } + typeRegistry[key.Name] = key } return nil } // GetTupleDescriptor gets the TupleDescriptor based on the TupleType func GetTupleDescriptor(tupleType TupleType) *TupleDescriptor { - tdi, found := typeRegistry.Load(tupleType) - if found { - td := tdi.(TupleDescriptor) - return &td + /* + tdi, found := typeRegistry.Load(tupleType) + if found { + td := tdi.(TupleDescriptor) + return &td + } + + return nil + */ + td := typeRegistry[string(tupleType)].(TupleDescriptor) + + return &td +} + +func GetAllTupleDescriptors() []TupleDescriptor { + + tds := make([]TupleDescriptor, len(typeRegistry)) + idx := 0 + for _, val := range typeRegistry { + //tds = append(tds, val.(TupleDescriptor)) + tds[idx] = val.(TupleDescriptor) + idx++ } - return nil + return tds } // MarshalJSON allows to hook & customize TupleDescriptor to JSON conversion @@ -101,6 +128,12 @@ func (td *TupleDescriptor) UnmarshalJSON(b []byte) error { td.TTLInSeconds = int(ttl.(float64)) } + td.PersistMode = "InMemory" + pm, ok := val["persistMode"] + if ok { + td.PersistMode = pm.(string) + } + jsonProps := val["properties"].([]interface{}) idxProp := make(map[int]string) diff --git a/common/model/types.go b/common/model/types.go index 2258a93..3633ac7 100644 --- a/common/model/types.go +++ b/common/model/types.go @@ -71,7 +71,6 @@ type RuleSession interface { //RtcTransactionHandler RegisterRtcTransactionHandler(txnHandler RtcTransactionHandler, handlerCtx interface{}) - } //ConditionEvaluator is a function pointer for handling condition evaluations on the server side @@ -92,10 +91,9 @@ type ValueChangeListener interface { type RtcTxn interface { //map of type and map of key/tuple - GetRtcAdded () map[string]map[string]Tuple + GetRtcAdded() map[string]map[string]Tuple GetRtcModified() map[string]map[string]RtcModified GetRtcDeleted() map[string]map[string]Tuple - } type RtcModified interface { @@ -103,5 +101,4 @@ type RtcModified interface { GetModifiedProps() map[string]bool } -type RtcTransactionHandler func (ctx context.Context, rs RuleSession, txn RtcTxn, txnContext interface{}) - +type RtcTransactionHandler func(ctx context.Context, rs RuleSession, txn RtcTxn, txnContext interface{}) diff --git a/config/config.go b/config/config.go index ec9429e..127be32 100644 --- a/config/config.go +++ b/config/config.go @@ -12,9 +12,10 @@ import ( // RuleSessionDescriptor is a collection of rules to be loaded type RuleActionDescriptor struct { - Name string `json:"name"` - IOMetadata *metadata.IOMetadata `json:"metadata"` - Rules []*RuleDescriptor `json:"rules"` + Name string `json:"name"` + IOMetadata *metadata.IOMetadata `json:"metadata"` + Rules []*RuleDescriptor `json:"rules"` + CacheConfig *CacheConfig `json:"cache"` } type RuleSessionDescriptor struct { @@ -36,6 +37,14 @@ type ConditionDescriptor struct { Evaluator model.ConditionEvaluator } +type CacheConfig struct { + Name string `json:"name"` + ServerType string `json:"servertype"` + Address string `json:"address"` + Password string `json:"password"` + DB int `json:"databaseId"` +} + func (c *RuleDescriptor) UnmarshalJSON(d []byte) error { ser := &struct { Name string `json:"name"` diff --git a/examples/flogo/solar/README.md b/examples/flogo/solar/README.md new file mode 100644 index 0000000..7fd3f56 --- /dev/null +++ b/examples/flogo/solar/README.md @@ -0,0 +1,19 @@ +

+ +

+ +

+ Rules read-only tuple cache example +

+ +

+ + + + +

+ +## Steps to build and run cache example +Install redis, run build and start the flogo app. Run 'cat data.txt | redis-cli --pipe' to load house tuples to redis cache. Then run +'curl "localhost:7766/solar/eligible?parcel=0002&bill=300"' to fire the solar eligibility action. Running +curl "localhost:7766/solar/eligible?parcel=0001&bill=300" will not fire the action as parcel 0001 already has the solar installed. diff --git a/examples/flogo/solar/flogo.json b/examples/flogo/solar/flogo.json new file mode 100644 index 0000000..709b4a3 --- /dev/null +++ b/examples/flogo/solar/flogo.json @@ -0,0 +1,122 @@ +{ + "name": "solarRule", + "type": "flogo:app", + "version": "0.0.1", + "description": "Sample Flogo App", + "appModel": "1.0.0", + "triggers": [ + { + "id": "receive_http_message", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "7766" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/solar/eligible" + }, + "actions": [ + { + "id": "solar_rule", + "input": { + "tupletype": "solar", + "values": "=$.queryParams" + } + } + ] + } + ] + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/rules/ruleaction", + "settings": { + "ruleSessionURI": "res://rulesession:solar", + "tds": [ + { + "name":"house", + "ttl" : -1, + "properties":[ + { + "name":"parcel", + "type":"string", + "pk-index":0 + }, + { + "name":"is_solar", + "type":"bool" + } + ], + "persistMode": "ReadOnlyCache" + }, + { + "name":"solar", + "ttl" : 0, + "properties":[ + { + "name":"parcel", + "type":"string", + "pk-index":0 + }, + { + "name":"bill", + "type":"double" + } + ] + } + ] + }, + "id": "solar_rule" + } + ], + "resources": [ + { + "id": "rulesession:solar", + "data": { + "metadata": { + "input": [ + { + "name": "values", + "type": "string" + }, + { + "name": "tupletype", + "type": "string" + } + ], + "output": [ + { + "name": "outputData", + "type": "any" + } + ] + }, + "cache": { + "name": "redis", + "servertype": "redis", + "address": "localhost:6379", + "password": "", + "databaseId": 0 + }, + "rules": [ + { + "name": "solarRule", + "conditions": [ + { + "name": "solarCond", + "identifiers": [ + "house", "solar" + ], + "evaluator": "solarEval" + } + ], + "actionFunction": "solarAction" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/examples/flogo/solar/functions.go b/examples/flogo/solar/functions.go new file mode 100644 index 0000000..2ffcd0a --- /dev/null +++ b/examples/flogo/solar/functions.go @@ -0,0 +1,52 @@ +package main + +import ( + "context" + "fmt" + + "github.com/project-flogo/rules/config" + + "github.com/project-flogo/rules/common/model" +) + +//add this sample file to your flogo project +func init() { + config.RegisterActionFunction("solarAction", solarAction) + + config.RegisterConditionEvaluator("solarEval", solarEval) + + config.RegisterStartupRSFunction("res://rulesession:solar", StartupRSFunction) +} + +func solarAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + tHouse := tuples["house"] + tSolar := tuples["solar"] + fmt.Printf("Eligible for a solar promotion! [%s], [%s]\n", tHouse.GetKey().String(), tSolar.GetKey().String()) + + // add isActionFired to rule context + fmt.Printf("Rule fired: [%s]\n", ruleName) +} + +func solarEval(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + tHouse := tuples["house"] + tSolar := tuples["solar"] + if tHouse == nil || tSolar == nil { + fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + return false + } + parcelHouse, _ := tHouse.GetString("parcel") + parcelSolar, _ := tSolar.GetString("parcel") + + isSolarHouse, _ := tHouse.GetBool("is_solar") + billSolar, _ := tSolar.GetDouble("bill") + + return (parcelHouse == parcelSolar) && + (isSolarHouse == false) && + (billSolar > 200) +} + +func StartupRSFunction(ctx context.Context, rs model.RuleSession, startupCtx map[string]interface{}) (err error) { + + fmt.Printf("In startup rule function..\n") + return nil +} diff --git a/examples/flogo/solar/main.go b/examples/flogo/solar/main.go new file mode 100644 index 0000000..234f2eb --- /dev/null +++ b/examples/flogo/solar/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + "runtime/pprof" + + _ "github.com/project-flogo/core/data/expression/script" + "github.com/project-flogo/core/engine" +) + +var ( + cpuProfile = flag.String("cpuprofile", "", "Writes CPU profile to the specified file") + memProfile = flag.String("memprofile", "", "Writes memory profile to the specified file") + cfgJson string + cfgCompressed bool +) + +func main() { + + flag.Parse() + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create CPU profiling file: %v\n", err) + os.Exit(1) + } + if err = pprof.StartCPUProfile(f); err != nil { + fmt.Fprintf(os.Stderr, "Failed to start CPU profiling: %v\n", err) + os.Exit(1) + } + defer pprof.StopCPUProfile() + } + + cfg, err := engine.LoadAppConfig(cfgJson, cfgCompressed) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create engine: %v\n", err) + os.Exit(1) + } + + e, err := engine.New(cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create engine: %v\n", err) + os.Exit(1) + } + + code := engine.RunEngine(e) + + if *memProfile != "" { + f, err := os.Create(*memProfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create memory profiling file: %v\n", err) + os.Exit(1) + } + + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + fmt.Fprintf(os.Stderr, "Failed to write memory profiling data: %v", err) + os.Exit(1) + } + _ = f.Close() + } + + os.Exit(code) +} diff --git a/go.mod b/go.mod index cfa514a..5418e5e 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/project-flogo/rules require ( github.com/aws/aws-sdk-go v1.18.3 + github.com/go-redis/redis v6.15.2+incompatible github.com/gorilla/websocket v1.4.0 github.com/oklog/ulid v1.3.1 github.com/project-flogo/core v0.9.0-alpha.6 diff --git a/ruleaction/action.go b/ruleaction/action.go index 6ee587d..c78ff0d 100644 --- a/ruleaction/action.go +++ b/ruleaction/action.go @@ -12,6 +12,7 @@ import ( "github.com/project-flogo/core/app/resource" "github.com/project-flogo/core/data" "github.com/project-flogo/core/support/log" + rulecache "github.com/project-flogo/rules/cache" "github.com/project-flogo/rules/common" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/config" @@ -116,6 +117,24 @@ func (f *ActionFactory) New(cfg *action.Config) (action.Action, error) { //start the rule session here, calls the startup rule function err = ruleAction.rs.Start(nil) + //Initialize CacheManager + var rcm *rulecache.RedisCacheManager = &rulecache.RedisCacheManager{} + + if rsCfg.CacheConfig != nil { + rcm.Init(*rsCfg.CacheConfig) + + //Load tuples from cache + tds := model.GetAllTupleDescriptors() + for _, td := range tds { + if model.OMModeMap[td.PersistMode] == model.ReadOnlyCache { + err = rcm.LoadTuples(context.TODO(), &td, ruleAction.rs) + if err != nil { + return nil, err + } + } + } + } + return ruleAction, err } @@ -167,7 +186,7 @@ func (a *RuleAction) Run(ctx context.Context, inputs map[string]interface{}) (ma val, _ := valAttr.(string) valuesMap := make(map[string]interface{}) - //metadata section allows receiving 'values' string as json format. (i.e. 'name=Bob' as '{"Name":"Box"}') + //metadata section allows receiving 'values' string as json format. (i.e. 'name=Bob' as '{"Name":"Bob"}') err := json.Unmarshal([]byte(val), &valuesMap) if err != nil { diff --git a/ruleapi/rulesession.go b/ruleapi/rulesession.go index 6df0fc9..8937280 100644 --- a/ruleapi/rulesession.go +++ b/ruleapi/rulesession.go @@ -99,6 +99,7 @@ func (rs *rulesessionImpl) Assert(ctx context.Context, tuple model.Tuple) (err e if ctx == nil { ctx = context.Context(context.Background()) } + rs.reteNetwork.Assert(ctx, rs, tuple, nil, rete.ADD) return nil } From 8d7ebbbf0740579ec1fc9c6fe53f5811dbefc753 Mon Sep 17 00:00:00 2001 From: Jon Park Date: Fri, 31 May 2019 15:06:56 -0700 Subject: [PATCH 2/4] Added readme --- cache/README.md | 23 +++++ cache/cache.go | 13 +++ cache/readonlycache.png | Bin 0 -> 29060 bytes cache/rulecache.go | 69 ++++++++++++++ cache/tests/cache_test.go | 166 +++++++++++++++++++++++++++++++++ cache/tests/common.go | 62 ++++++++++++ cache/tests/data.txt | 4 + cache/tests/tests.json | 33 +++++++ examples/flogo/solar/README.md | 48 +++++++--- 9 files changed, 405 insertions(+), 13 deletions(-) create mode 100644 cache/README.md create mode 100644 cache/cache.go create mode 100644 cache/readonlycache.png create mode 100644 cache/rulecache.go create mode 100644 cache/tests/cache_test.go create mode 100644 cache/tests/common.go create mode 100644 cache/tests/data.txt create mode 100644 cache/tests/tests.json diff --git a/cache/README.md b/cache/README.md new file mode 100644 index 0000000..dc8bf9b --- /dev/null +++ b/cache/README.md @@ -0,0 +1,23 @@ + +## Caching + +Tuples can be categorized into two different types depending on their nature of durability. The event tuples, due to their transient nature, are to be consumed as it is processed by the rule engine. The data tuples, due to their persistent nature, are expected to be available within the rule network for evaluations and actions across the event tuples entered in the fule network. + +The data tuples also are expected to be stored in a persistent storage - i.e. database,distributed cache, files on disk, etc. + +For example, a data tuple would be a user's credit score while event tuple is a credit card application. A rule can be writtten to approve a credit card by joining user's credit tuple and credit card application. + +### readonly tuple cache + +A readonly tuple cache is supported with the following characteristics: +- readonly tuples are available in the cache and to be loaded before a rule session receives an event tuple. +- persistMode attribute value of "ReadOnlyCache" in the tuple descriptor indicates that the tuple is to be loaded from the cache into the rule network. +- TTL of the readonly cache is -1. +- A distributed cache would enable scale rule engine instances as needed from a single tuple data source as shown below. + + + +

+ +

+ diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..606b67f --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,13 @@ +package rulecache + +import ( + "context" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" +) + +type CacheManager interface { + Init(cfg config.CacheConfig) + LoadTuples(ctx context.Context, td *model.TupleDescriptor, rs model.RuleSession) error +} diff --git a/cache/readonlycache.png b/cache/readonlycache.png new file mode 100644 index 0000000000000000000000000000000000000000..6a29440ee847a365a44375e0d75c938dee3287d6 GIT binary patch literal 29060 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 +house:parcel:0001 parcel 0001 is_solar true +house:parcel:0002 parcel 0002 is_solar false + +solar events are asserted everytime a monthly electiricity bill is generated. + + +solar:parcel:0001 parcel 0001 bill 300 +solar:parcel:0002 parcel 0002 bill 250 + +*/ + +import ( + "context" + "fmt" + "testing" + + rulecache "github.com/project-flogo/rules/cache" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" + + "github.com/project-flogo/rules/ruleapi" +) + +func Test_Cache_1(t *testing.T) { + + rs, _ := createRuleSession() + + rule := ruleapi.NewRule("Cache_Test") + err := rule.AddCondition("TC_1", []string{"house", "solar"}, checkSolarEligibleCondition, nil) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + ruleActionCtx := make(map[string]string) + rule.SetContext(ruleActionCtx) + rule.SetAction(solarEligibleAction) + rule.SetPriority(1) + err = rs.AddRule(rule) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + t.Logf("Rule added: [%s]\n", rule.GetName()) + + err = rs.Start(nil) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + + //PreCase: Assert all house tuples from Cache. ToDo: Check if this can be done before rulesession creation. + { + var rcm rulecache.CacheManager = &rulecache.RedisCacheManager{} + cacheConfig := config.CacheConfig{"rediscache", "redis", "localhost:6379", "", 0} + rcm.Init(cacheConfig) + + //Load tuples from cache + tds := model.GetAllTupleDescriptors() + for _, td := range tds { + if model.OMModeMap[td.PersistMode] == model.ReadOnlyCache { + err = rcm.LoadTuples(context.TODO(), &td, rs) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + } + } + } + + // Case1: Assert an ineligible solar. It should not fire solarEligibleAction. + { + ctx := context.WithValue(context.TODO(), "key", t) + values := make(map[string]interface{}) + values["parcel"] = "0001" + values["bill"] = 300 + tuple, _ := model.NewTuple("solar", values) + err := rs.Assert(ctx, tuple) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + } + + // Case2: Assert an eligible solar. solarEligibleAction should be fired. + { + ctx := context.WithValue(context.TODO(), "key", t) + values := make(map[string]interface{}) + values["parcel"] = "0002" + values["bill"] = 250 + tuple, _ := model.NewTuple("solar", values) + err := rs.Assert(ctx, tuple) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + } + + // Case3: Assert the ineligible solar again. It should not fire solarEligibleAction. + { + ctx := context.WithValue(context.TODO(), "key", t) + values := make(map[string]interface{}) + values["parcel"] = "0001" + values["bill"] = 300 + tuple, _ := model.NewTuple("solar", values) + err := rs.Assert(ctx, tuple) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + } + + // Case2: Assert an eligible solar. solarEligibleAction should be fired. + { + ctx := context.WithValue(context.TODO(), "key", t) + values := make(map[string]interface{}) + values["parcel"] = "0002" + values["bill"] = 250 + tuple, _ := model.NewTuple("solar", values) + err := rs.Assert(ctx, tuple) + if err != nil { + t.Logf("%s", err) + t.FailNow() + } + } + + rs.Unregister() + +} + +func solarEligibleAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t := ctx.Value("key").(*testing.T) + tHouse := tuples["house"] + tSolar := tuples["solar"] + t.Logf("Eligible for a solar promotion! [%s], [%s]\n", tHouse.GetKey().String(), tSolar.GetKey().String()) +} + +func checkSolarEligibleCondition(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + tHouse := tuples["house"] + tSolar := tuples["solar"] + if tHouse == nil || tSolar == nil { + fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + return false + } + parcelHouse, _ := tHouse.GetString("parcel") + parcelSolar, _ := tSolar.GetString("parcel") + + isSolarHouse, _ := tHouse.GetBool("is_solar") + billSolar, _ := tSolar.GetDouble("bill") + + return (parcelHouse == parcelSolar) && + (isSolarHouse == false) && + (billSolar > 200) +} diff --git a/cache/tests/common.go b/cache/tests/common.go new file mode 100644 index 0000000..5d6178f --- /dev/null +++ b/cache/tests/common.go @@ -0,0 +1,62 @@ +package tests + +import ( + "context" + "io/ioutil" + "log" + "testing" + + "github.com/project-flogo/rules/common" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" +) + +func createRuleSession() (model.RuleSession, error) { + rs, _ := ruleapi.GetOrCreateRuleSession("test") + + tupleDescFileAbsPath := common.GetAbsPathForResource("src/github.com/project-flogo/rules/cache/tests/tests.json") + + dat, err := ioutil.ReadFile(tupleDescFileAbsPath) + if err != nil { + log.Fatal(err) + } + err = model.RegisterTupleDescriptors(string(dat)) + if err != nil { + return nil, err + } + return rs, nil +} + +//conditions and actions +func trueCondition(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + return true +} +func falseCondition(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + return false +} +func emptyAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { +} + +func printTuples(t *testing.T, oprn string, tupleMap map[string]map[string]model.Tuple) { + + for k, v := range tupleMap { + t.Logf("%s tuples for type [%s]\n", oprn, k) + for k1, _ := range v { + t.Logf(" tuples key [%s]\n", k1) + } + } +} +func printModified(t *testing.T, modified map[string]map[string]model.RtcModified) { + + for k, v := range modified { + t.Logf("%s tuples for type [%s]\n", "Modified", k) + for k1, _ := range v { + t.Logf(" tuples key [%s]\n", k1) + } + } +} + +type txnCtx struct { + Testing *testing.T + TxnCnt int +} diff --git a/cache/tests/data.txt b/cache/tests/data.txt new file mode 100644 index 0000000..8be6287 --- /dev/null +++ b/cache/tests/data.txt @@ -0,0 +1,4 @@ +HMSET house:parcel:0001 parcel 0001 is_solar true +SADD house house:parcel:0001 +HMSET house:parcel:0002 parcel 0002 is_solar false +SADD house house:parcel:0002 \ No newline at end of file diff --git a/cache/tests/tests.json b/cache/tests/tests.json new file mode 100644 index 0000000..0576625 --- /dev/null +++ b/cache/tests/tests.json @@ -0,0 +1,33 @@ +[ + { + "name":"house", + "ttl" : -1, + "properties":[ + { + "name":"parcel", + "type":"string", + "pk-index":0 + }, + { + "name":"is_solar", + "type":"bool" + } + ], + "persistMode": "ReadOnlyCache" + }, + { + "name":"solar", + "ttl" : 0, + "properties":[ + { + "name":"parcel", + "type":"string", + "pk-index":0 + }, + { + "name":"bill", + "type":"double" + } + ] + } +] \ No newline at end of file diff --git a/examples/flogo/solar/README.md b/examples/flogo/solar/README.md index 7fd3f56..981a96e 100644 --- a/examples/flogo/solar/README.md +++ b/examples/flogo/solar/README.md @@ -2,18 +2,40 @@

-

- Rules read-only tuple cache example -

+## Solar eligibility check -

- - - - -

-## Steps to build and run cache example -Install redis, run build and start the flogo app. Run 'cat data.txt | redis-cli --pipe' to load house tuples to redis cache. Then run -'curl "localhost:7766/solar/eligible?parcel=0002&bill=300"' to fire the solar eligibility action. Running -curl "localhost:7766/solar/eligible?parcel=0001&bill=300" will not fire the action as parcel 0001 already has the solar installed. +This flogo app check if a house a eligible for solar panel marketing promotion. house data that contains parcel number and the status of solar panel installation. solar check event contains a parcel number and monthly spending bill. + +The app loads the house data from redis cache storage and, as solar check events arrives, fires the solar eligibility action depending the condition described as follows: + +If the monthly bill is greater than 200 and the house doesn't have a solar panels installed, +the house with the matching parcel id should be a candidate for solar panel installation promotion. + +house tuples are to be pre-loaded from data.txt +The command in data.txt loads tuples with a tuple key as a redis hash key. It then updates the index named after +the tuple type by associating the hash key to the index. + + +house:parcel:0001 parcel 0001 is_solar true +house:parcel:0002 parcel 0002 is_solar false + +solar events are asserted for a monthly electiricity bill generated. + + +solar:parcel:0001 parcel 0001 bill 300 +solar:parcel:0002 parcel 0002 bill 250 + + +## Steps to build and run example +Install redis, build and start the flogo app.
+Run
+      'cat data.txt | redis-cli --pipe' to load house tuples to redis cache.
+ +Then run
+      curl "localhost:7766/solar/eligible?parcel=0002&bill=300"
+to fire the solar eligibility action.
+ +Running
+      curl "localhost:7766/solar/eligible?parcel=0001&bill=300"
+will not fire the action as parcel 0001 already has the solar installed. From 6a28f242e7b216aa2c2a01782605efe2de47c7e0 Mon Sep 17 00:00:00 2001 From: Jon Park Date: Mon, 3 Jun 2019 09:41:01 -0700 Subject: [PATCH 3/4] Added a check for cache server supported. --- ruleaction/action.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ruleaction/action.go b/ruleaction/action.go index c78ff0d..47107d0 100644 --- a/ruleaction/action.go +++ b/ruleaction/action.go @@ -117,17 +117,24 @@ func (f *ActionFactory) New(cfg *action.Config) (action.Action, error) { //start the rule session here, calls the startup rule function err = ruleAction.rs.Start(nil) + var cm rulecache.CacheManager + //Initialize CacheManager - var rcm *rulecache.RedisCacheManager = &rulecache.RedisCacheManager{} + if rsCfg.CacheConfig.ServerType == "redis" { + cm = &rulecache.RedisCacheManager{} + } else { + return nil, fmt.Errorf("cache server type [%s] not supported", rsCfg.CacheConfig.ServerType) + } + //var rcm *rulecache.RedisCacheManager = &rulecache.RedisCacheManager{} if rsCfg.CacheConfig != nil { - rcm.Init(*rsCfg.CacheConfig) + cm.Init(*rsCfg.CacheConfig) //Load tuples from cache tds := model.GetAllTupleDescriptors() for _, td := range tds { if model.OMModeMap[td.PersistMode] == model.ReadOnlyCache { - err = rcm.LoadTuples(context.TODO(), &td, ruleAction.rs) + err = cm.LoadTuples(context.TODO(), &td, ruleAction.rs) if err != nil { return nil, err } From 0aac3be00a0ecc059cbe35f9e0b0a771d5073eda Mon Sep 17 00:00:00 2001 From: Jon Park Date: Mon, 3 Jun 2019 09:42:00 -0700 Subject: [PATCH 4/4] chore cleanup --- ruleaction/action.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ruleaction/action.go b/ruleaction/action.go index 47107d0..1f899d6 100644 --- a/ruleaction/action.go +++ b/ruleaction/action.go @@ -125,7 +125,6 @@ func (f *ActionFactory) New(cfg *action.Config) (action.Action, error) { } else { return nil, fmt.Errorf("cache server type [%s] not supported", rsCfg.CacheConfig.ServerType) } - //var rcm *rulecache.RedisCacheManager = &rulecache.RedisCacheManager{} if rsCfg.CacheConfig != nil { cm.Init(*rsCfg.CacheConfig)