grule-rule-engine icon indicating copy to clipboard operation
grule-rule-engine copied to clipboard

Getting Max Cycle Reached Error

Open barum opened this issue 2 years ago • 0 comments

Describe the bug I created a sample program that reads structs and changes some totals and subtotals. It works fine. Then I copied your multi threaded text and changed it to reflect my structure and made the changes needed to reflect to the struct and rules. The program executes but I keep getting ERRO[0000] Max cycle reached lib=grule-rule-engine `package=engine`

I have looked at the documentation, the closed issues but cannot figure out what is wrong.

To Reproduce Steps to reproduce the behavior:

  1. I created the sample working program to change totals and subtotals as part of the Init (line 126).
  2. I created the Lib, rb, nb, and created rule from nb.
  3. Ranging to get the claimant address (line 147)
  4. call the beginThread ( line 148)
  5. I have checked the rule conditions and the 2 sample rules cannot both be executed

Expected behavior The value of the fields in the struct should be changed.

Additional context

The code:

// create go file main.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"sync"
	"time"

	"github.com/hyperjumptech/grule-rule-engine/ast"
	"github.com/hyperjumptech/grule-rule-engine/builder"
	"github.com/hyperjumptech/grule-rule-engine/engine"
	"github.com/hyperjumptech/grule-rule-engine/logger"
	"github.com/hyperjumptech/grule-rule-engine/pkg"
	"github.com/sirupsen/logrus"
)

const (
	EligibilityRule = `
rule EligibilityRule1 "Salary More" salience 10 {
	when 
		Claimant.IncomeTotal > 13000 || Claimant.SubTotalIncome > 10000 &&
		Claimant.EligibleIncomeTotal == 0
	then
		Claimant.EligibleIncomeTotal = 1;
		Log("EligibilityRule1 : " + Claimant.Name);


}
rule EligibilityRule2 "Salary Less" salience 10 {
	when 
		Claimant.IncomeTotal < 14000 || Claimant.SubTotalIncome < 14000 && 
		Claimant.EligibleSubTotalIncome==0
	then

		Claimant.EligibleSubTotalIncome=1;
		Log("EligibilityRule2 : " + Claimant.Name);

}
`
)


var (
	// threadFinishMap used to monitor the status of all thread
	threadFinishMap = make(map[string]bool)

	// syncD is a mutex object to protect threadFinishMap from concurrent map read/write
	syncD = sync.Mutex{}

	concurrencyTestlog = logger.Log.WithFields(logrus.Fields{
		"lib":  "grule",
		"file": "examples/Concurrency_test.go",
	})
)

type Claimants struct {
	Claimants []Claimant `json:"Claimant"`
}

// create claimant struct
type Claimant struct {
	Name                   string    `json:"Name"`
	ID                     string    `json:"ID"`
	Incomes                []Incomes `json:"Incomes"`
	SubTotalIncome         float64
	SubTotalExpense        float64
	IncomeTotal            float64
	EligibleIncomeTotal    uint
	EligibleSubTotalIncome uint
}

type Incomes struct {
	Type               string  `json:"Type"`
	Amount             float64 `json:"Amount"`
	BeginDate          string  `json:"BeginDate"`
	EndDate            string  `json:"EndDate"`
	IncomeCategoryCode int     `json:"IncomeCategoryCode"`
}

// create function 	Claimants init
func (c *Claimants) Init(data []byte) {
	// fmt.Printf("starting init [%v] \n", c)
	//loop through claimants

	err := json.Unmarshal(data, c)
	if err != nil {

		panic(err)
	}
	for i := 0; i < len(c.Claimants); i++ {
		claimant := c.Claimants[i]
		// fmt.Printf("claimant [%v] \n", claimant)
		// loop through incomes
		for j := 0; j < len(claimant.Incomes); j++ {
			income := claimant.Incomes[j]
			// fmt.Printf("income [%v] \n", income)
			// update claimant with total income
			if income.IncomeCategoryCode == 13 {
				claimant.SubTotalIncome += income.Amount
			} else {
				claimant.SubTotalExpense += income.Amount
			}
		}
		claimant.IncomeTotal = claimant.SubTotalIncome - claimant.SubTotalExpense
		c.Claimants[i] = claimant
	}
}

func main() {

	// read json file from data/judge.json
	claimantsFile, err := os.Open("../../data/judge.json")
	if err != nil {
		panic(err)
	}

	defer claimantsFile.Close()

	claimantsBytes, err := ioutil.ReadAll(claimantsFile)
	if err != nil {
		panic(err)
	}

	//fmt.Printf("%s\n", claimantsBytes)

	// unmarshal json file to claimants struct
	var claimants Claimants
	claimants.Init(claimantsBytes)

	// create map to store claimants
	//claimantsMap := make(map[string]Claimant)

	// store claimants in claimantsMap
	// for _, claimant := range claimants.Claimants {
	// 	claimantsMap[claimant.ID] = claimant

	// }

	// Prepare knowledgebase library and load it with our rule.
	lib := ast.NewKnowledgeLibrary()
	rb := builder.NewRuleBuilder(lib)
	nb := pkg.NewBytesResource([]byte(EligibilityRule))
	err = rb.BuildRuleFromResource("EligibilityRule", "0.0.1", nb)
	// for i := 1; i <= 500; i++ {
	// 	go beginThread(fmt.Sprintf("T%d", i), lib), claimant)
	// }

	for i := range claimants.Claimants {
		claimant := claimants.Claimants[i]
		go beginThread(claimant.ID, lib, &claimant)
	}
	// Wait until all thread finishes
	time.Sleep(1 * time.Second)
	for !isAllThreadFinished() {
		time.Sleep(500 * time.Millisecond)
	}

	for _, claimant := range claimants.Claimants {

		fmt.Printf("[%s]'s SubTotalIncome >>%v<< SubTotalExpense >>%v<< total income is >>%v<<, ER >>%d<< ER2 >>%d<< \n", claimant.Name, claimant.SubTotalIncome, claimant.SubTotalExpense,
			claimant.IncomeTotal, claimant.EligibleIncomeTotal, claimant.EligibleSubTotalIncome)
	}

}

func beginThread(threadName string, lib *ast.KnowledgeLibrary, claimant *Claimant) {
	concurrencyTestlog.Tracef("Beginning thread %s", threadName)

	// Register this thread into our simple finish map check
	startThread(threadName)
	defer finishThread(threadName)

	// Prepare new DataContext to be used in this Thread
	dataContext := ast.NewDataContext()

	// Create our model and add into data context

	err := dataContext.Add("Claimant", claimant)
	if err != nil {
		fmt.Errorf("DataContext error on thread %s, got %s", threadName, err)
	}

	// Create a new engine for this thread
	engine := &engine.GruleEngine{MaxCycle: 100}

	// Get an instance of our KnowledgeBase from KnowledgeLibrary
	kb := lib.NewKnowledgeBaseInstance("EligibilityRule", "0.0.1")

	// Execute the KnowledgeBase against DataContext
	err = engine.Execute(dataContext, kb)
	if err != nil {
		fmt.Errorf("Engine execution error on thread %s, got %s", threadName, err)
	}

}

func startThread(threadName string) {
	syncD.Lock()
	defer syncD.Unlock()
	threadFinishMap[threadName] = false
}

func finishThread(threadName string) {
	syncD.Lock()
	defer syncD.Unlock()
	threadFinishMap[threadName] = true
}

func isAllThreadFinished() bool {
	syncD.Lock()
	defer syncD.Unlock()
	fin := true
	for _, b := range threadFinishMap {
		fin = fin && b
		if !fin {
			return false
		}
	}
	return true
}

Sample Json

{ "Claimant": [ { "Name": "Sally", "ID": "ABCD1011amaladnSally", "Incomes": [ { "Type": "Salary", "Amount": 5000, "BeginDate": "1/1/2022", "EndDate": "5/7/2022", "IncomeCategoryCode": 13 }, { "Type": "Salary", "Amount": 10000, "BeginDate": "5/12/2022", "EndDate": null, "IncomeCategoryCode": 13 }, { "Type": "Rent", "Amount": 1000, "BeginDate": "1/12/2000", "EndDate": null, "IncomeCategoryCode": 2 } ] }, { "Name": "Joe", "ID": "ABCD1011amaladnJoe", "Incomes": [ { "Type": "Salary", "Amount": 4000, "BeginDate": "1/1/2022", "EndDate": "5/7/2022", "IncomeCategoryCode": 13 }, { "Type": "Salary", "Amount": 10000, "BeginDate": "5/12/2022", "EndDate": null, "IncomeCategoryCode": 13 }, { "Type": "Rent", "Amount": 1000, "BeginDate": "1/12/2000", "EndDate": null, "IncomeCategoryCode": 2 } ] }, { "Name": "Bob", "ID": "ABCD1011amaladnBob", "Incomes": [ { "Type": "Salary", "Amount": 3000, "BeginDate": "1/1/2022", "EndDate": "5/7/2022", "IncomeCategoryCode": 13 }, { "Type": "Salary", "Amount": 10000, "BeginDate": "5/12/2022", "EndDate": null, "IncomeCategoryCode": 13 }, { "Type": "Rent", "Amount": 1000, "BeginDate": "1/12/2000", "EndDate": null, "IncomeCategoryCode": 2 } ] } ] }

barum avatar Aug 02 '22 12:08 barum