Mastering Decision-Making with Grule

We constantly run into tricky problems in software development that need clever solutions. Recently, we faced a challenge in making precise and efficient decisions whenever an activity occurred in the catalog of an e-commerce platform. This is where we discovered Grule, a rule engine library that simplified our approach to managing complex business rules. 

Let us explore how Grule revolutionized our project, optimizing decision-making and making our software system more efficient and maintainable.

What is a Rule Engine?

A rule engine is a powerful tool in software engineering designed to streamline decision-making based on predefined rules. It allows developers to define business rules clearly and separately from the application code, enhancing maintainability. 

By efficiently managing large sets of rules and data, a rule engine's inference engine matches facts against these rules, ensuring precise decision-making. 

This is especially useful for automating decision logic in various software applications. With a modular and scalable architecture, rule engines integrate seamlessly into different projects, providing a robust framework for developers to optimize and simplify complex decision-making processes, leading to efficient and maintainable software systems.

Why Use a Rule Engine?

We must evaluate multiple conditions based on different rules in many situations. Initially, we might work with predefined rules, but these often need to evolve. Updates may be necessary, or specific rules may be required for different users. Managing these updates directly in the code can be tedious and challenging. 

This is where a rule engine comes in, handling the evaluation of rules and executing necessary actions. This blog explores our experience with rule engines, emphasizing their importance in managing dynamic rule evaluation scenarios.

Grule: "Gopher Holds The Rules"

Grule is a rule engine library designed for the Go (Golang) programming language, inspired by the popular JBOSS Drools but with a more straightforward execution. Like Drools, Grule has its own Domain-Specific Language (DSL). 

Below is an example of Drools's DRL (Drools Rule Language) for a "SpeedUp" rule. Grule's GRL (Grule Rule Language) replicates the same functionality with simpler syntax, making rule definitions clearer and easier to use. 

You can find the reference from the official GitHub ReadMe

rule "SpeedUp"
    salience 10
    when
        $TestCar : TestCarClass( speedUp == true && speed < maxSpeed )
        $DistanceRecord : DistanceRecordClass()
    then
        $TestCar.setSpeed($TestCar.Speed + $TestCar.SpeedIncrement);
        update($TestCar);
        $DistanceRecord.setTotalDistance($DistanceRecord.getTotalDistance() + $TestCar.Speed)
        update($DistanceRecord)
end

Equivalent Grule GRL

rule SpeedUp "When testcar speeds up, increase the speed" 
    salience 10  {
when
    TestCar.SpeedUp == true && TestCar.Speed < TestCar.MaxSpeed
then
    TestCar.Speed = TestCar.Speed + TestCar.SpeedIncrement;
    DistanceRecord.TotalDistance = DistanceRecord.TotalDistance + TestCar.Speed;
}

Moreover, Grule also supports JSON-formatted rules, which we found highly beneficial for our project. This feature enhances Grule's flexibility, enabling us to utilize rules in a format known for its simplicity and clarity. Below is an example of how the aforementioned GRL appears in JSON format.

{
  "name": "SpeedUp",
  "desc": "When testcar is speeding up we keep increase the speed.",
  "salience": 10,
  "when": ...,
  "then": [
    ...
  ]
}

Harnessing the Power of Grule in E-commerce Optimization

In a recent project, we encountered a unique scenario within an e-commerce platform where we needed to monitor and respond to specific business triggers. Examples include detecting a 10% price drop in cosmetics, monitoring electronics stock below 50 units, and reacting to tax price changes in fashion. These triggers enable us to swiftly adapt to changes such as new home decor arrivals, clothing color variants, or updated product descriptions in books.

Grule facilitated this process by evaluating these triggers efficiently, ensuring timely actions to maintain competitive advantage. Below, we illustrate how Grule integrates with our e-commerce platform, leveraging JSON to handle and update product data dynamically.

Example 1: Price Drop

In the JSON data below, we notice the price was originally listed as 100 in the old data, but in the updated data, it has dropped to 50, indicating a significant decrease of 50%.

Example 2: Back in Stock

"Back in stock" refers to a scenario where the quantity was 0 in the old data but now exceeds 0 in the updated data, indicating that the product is available again.

null

Converting Statements into JSON Rules

Once we understand how data is received, we can see how user-defined conditions are transformed into Grule rules. In the Journey UI, when a user sets a trigger condition—like initiating communication when a product's price drops by 10%—a rule JSON is dynamically generated. 

This JSON is carefully crafted to match our rule engine's capabilities, ensuring smooth processing.

null

For example, the JSON rule is created as shown for the trigger condition described above.

{
   "name":"rule_pd_83451_cc1b011a9c578e8738f07b09e1bb1582_216_279_216",
   "desc":"rule_pd_83451_cc1b011a9c578e8738f07b09e1bb1582_216_279_216",
   "when":{
      "and":[
         {
            "and":[
               {
                  "eq":[
                     "custom_function.DecreasesBy(\"sale_price\", \"relativ=
e\",10.00)",
                     {
                        "const":true
                     }
                  ]
               },
               {
                  "const":true
               }
            ]
         },
         {
            "const":true
         }
      ]
   },
   "then":[
      {
         "call":[
            "Complete"
         ]
      }
   ]
}

For those unfamiliar with coding, this flow chart represents the JSON rule mentioned earlier.

null

Implementing Grule: Rule-Based Event Processing

Let's explore the code. At the core is the `EvaluateEvent` function, pivotal for processing event data through rule analysis. It takes parameters like event details (in JSON format), a trace ID for request tracking, rule definitions (in JSON), and client ID.

Inside, it iterates through rules and event data, creating a tailored data context and knowledge library. Product attributes are included in the context, along with custom functions for better rule evaluation. Using Grule's rule builder, it dynamically constructs rules from JSON and applies them using a rule engine to match against the event data.

The function returns matched rules and any encountered errors. `EvaluateEvent` provides a streamlined approach for making rule-based decisions in event processing.

func EvaluateEvent(eventDetails string, traceId string, ruleJsons []RuleSet, clientId int) (map[string][]string, error) {
	//...SNIPPED...
	for _, rule_json := range ruleJsons {
		for _, product := range csvData.Products {
			//...SNIPPED...
			updatedProductMap := GetUpdatedProductMap(oldProduct, newProduct)
			dataCtx := ast.NewDataContext()
			// Enables product attributes to be used in the rules
			for k, v := range updatedProductMap {
				dataCtx.Add(k, v)
			}
			// Allows custom Functions
			dataCtx.Add("custom_function", &CustomFunctions{Data: updatedProductMap})
			knowledgeLibrary := ast.NewKnowledgeLibrary()
			ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)
			for _, rule := range rule_json.Rules {
				trigger_rule_json_bytes, _ := json.Marshal(rule.TriggerRuleStr)
				underlying := pkg.NewBytesResource(trigger_rule_json_bytes)
				resource, err := pkg.NewJSONResourceFromResource(underlying)
				if err != nil {
					return nil, err
				}
				err = ruleBuilder.BuildRuleFromResource("MtRules", "0.0.1", resource)
				if err != nil {
					return nil, err
				}
				knowledgeBase, _ := knowledgeLibrary.NewKnowledgeBaseInstance("MtRules", "0.0.1")
				ruleEngine := engine.NewGruleEngine()
				matching_rules, err := ruleEngine.FetchMatchingRules(dataCtx, knowledgeBase)
				if err != nil {
					return nil, err
				}
				//...SNIPPED...
			}
		}
	}
	return matching_rules, nil
}

To add custom functionalities to our rule builders, we've created a struct that must be included in the context. This struct contains methods designed to perform different operations as needed.

type CustomFunctions struct {
    Data map[string]interface{}
}

// Example of custom function
func (cf *CustomFunctions) DecreasesBy(key string, diffType string, value float64) bool {
    // Check if the price exists
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic occurred:", r)
            debug.PrintStack()
        }
    }()

    val, ok := cf.Data[key]
    if ok {
        var dfKey string
        if diffType == "relative" {
            dfKey = key + "_rel"
        } else {
            dfKey = key + "_abs"
        }

        v, vok := cf.Data[dfKey]
        if vok {
            fv, fok := v.(float64)
            if fok {
                return fv >= value
            } else {
                fmt.Println("Failed to type cast to float64 : ", v)
            }
        } else {
            fmt.Println("Failed to get value : ", v)
        }
    }
    return false
}

Benchmarking

The benchmarking data showcases the time taken to process varying numbers of rows within a benchmarking table. The table comprises

three distinct datasets: 10,000, 100,000, and 1,000,000 rows.

For 10,000 rows, the processing time was 00:01:22. For 100,000 rows, it increased to 00:03:10. Lastly, processing 1,000,000 rows took

00:09:59.

Visually, the data reveals a consistent trend: as the number of rows grows, so does the processing time. This highlights the significance of

optimizing data processing workflows for scalability and efficiency, particularly when handling larger datasets.

null

Conclusion

Grule stands out as a pivotal tool in our project, significantly streamlining complex decision-making processes and boosting overall efficiency. Its structured approach to defining and managing rules has allowed us to adapt and enhance our software system effortlessly. 

From managing price changes and stock availability to implementing dynamic business triggers, Grule's versatility has played a crucial role in achieving success and maintaining adaptability in our operations.