Skip to content

Performance Benchmarks

Comprehensive performance benchmarks for the Ordo rule engine, including core engine microbenchmarks, HTTP server throughput, distributed (NATS sync) mode, and head-to-head comparisons against mainstream rule engines.

Test Environment: Apple M1 Pro (10 cores), 16 GB RAM, macOS Darwin 25.3.0 Tools: Criterion.rs (microbenchmarks), hey (HTTP load testing), Docker (NATS) Date: 2026-03-11


1. Executive Summary

MetricValue
Core engine execution (4-branch rule)573 ns
HTTP single-instance peak QPS62,511
HTTP distributed (Writer + Reader combined)83,082
Batch execution peak throughput3.06 M rules/sec
Memory under full load46.2 MB
vs Zen Engine (Rust competitor) core speed7.3x faster
vs OPA (Go) HTTP throughput1.9x faster
vs json-rules-engine (Node.js) HTTP throughput3.4x faster

2. Core Engine Microbenchmarks

Pure rule evaluation speed measured with Criterion.rs — no HTTP, no middleware, no I/O. Each engine evaluates an equivalent 4-branch decision rule (score >= 90 / 70 / 50 / default) with input {"score": 75} hitting the second branch.

2.1 Rust Engine Comparison

EngineTime per evalRelativeTheoretical single-core QPS
Native Rust if/else (ceiling)3.5 ns285,000,000
Ordo (bytecode VM)573 ns1.0x1,746,000
Rhai (AST interpreter)728 ns0.79x1,374,000
Zen Engine / GoRules (graph eval)4,200 ns0.14x238,000

Key takeaway: Ordo's bytecode VM is 7.3x faster than Zen Engine and 1.27x faster than Rhai at the core evaluation level.

2.2 Ordo Detailed Microbenchmarks

Expression Parsing (one-time cost)

ExpressionTime
age > 18 (simple comparison)~1.0 µs
status == "active" (string equality)~1.0 µs
age > 18 && status == "active" (AND)~2.0 µs
age < 13 || age > 65 (OR)~2.2 µs
user.profile.level == "gold" (nested path)~1.1 µs
status in ["active", "pending"] (set membership)~1.9 µs

Expression Evaluation (per execution, pre-compiled)

ExpressionTime
age > 18~40 ns
status == "active"~54 ns
age > 18 && status == "active"~80 ns
user.profile.level == "gold"~63 ns
status in ["active", "pending"]~80 ns
score * 0.6 + bonus * 0.4 (arithmetic)~67 ns

Rule Execution

ScenarioTime
Minimal 2-step rule (decision → terminal)~361 ns
4-branch compiled execution~573 ns
Compiled binary (.ordo format)~553 ns
Batch 1K executions throughput~2.70 M ops/sec

Schema JIT (Cranelift)

OperationTime
JIT field access (native code)~5 ns
JIT numeric expression~8 ns

3. HTTP Server Benchmarks

All tests use hey with 10-second duration. The server runs in release mode with --log-level error to minimize I/O noise.

3.1 Standalone Mode — Concurrency Sweep

Single Ordo instance, 4-branch decision rule, input {"input":{"score":75}}.

ConcurrencyQPSAvg LatencyP50P95P99MaxCPU%
114,6340.07 ms0.06 ms0.10 ms0.10 ms13.9 ms56%
1047,9600.21 ms0.19 ms0.30 ms0.40 ms10.6 ms423%
2557,2020.44 ms0.37 ms0.70 ms1.60 ms28.5 ms359%
5059,4400.84 ms0.76 ms1.60 ms2.90 ms51.9 ms411%
10061,0571.64 ms1.45 ms3.20 ms4.90 ms31.0 ms311%
20062,5113.18 ms2.78 ms7.00 ms9.90 ms39.2 ms309%
50060,5778.25 ms6.60 ms20.4 ms29.2 ms76.3 ms289%

Saturation point: ~60K QPS (stable from concurrency 50–200, latency degrades at 500).

3.2 Distributed Mode (Writer + Reader + NATS JetStream)

Writer on :8080, Reader on :8081, NATS on :4222 (Docker, 1 CPU, 256 MB).

Writer (with NATS publisher)

ConcurrencyQPSAvgP50P99
112,9040.08 ms0.07 ms0.20 ms
1044,8550.22 ms0.19 ms0.50 ms
5056,2550.89 ms0.80 ms3.20 ms
10058,5421.71 ms1.50 ms5.00 ms
20058,5963.41 ms2.90 ms10.9 ms

Reader (with NATS subscriber)

ConcurrencyQPSAvgP50P99
114,1830.07 ms0.07 ms0.10 ms
1045,5880.22 ms0.19 ms0.50 ms
5058,5490.85 ms0.80 ms3.00 ms
10059,6801.68 ms1.50 ms5.10 ms
20060,2043.32 ms2.80 ms10.7 ms

NATS sync has zero hot-path overhead — Writer/Reader QPS matches Standalone mode.

Simultaneous Writer + Reader (100 concurrency each)

RoleQPSCPURSS
Writer40,992193%30.9 MB
Reader42,090205%23.6 MB
Combined83,08254.5 MB

On separate machines, each Reader adds ~60K QPS linearly.

3.3 Batch Execution Throughput

50 concurrency on Standalone mode, varying batch sizes.

Batch SizeRequests/secRules Executed/secAvg Latency
1053,018530,1800.9 ms
5033,8081,690,4001.5 ms
10022,0492,204,9002.3 ms
5006,1283,064,0008.2 ms

3.4 Resource Usage

MetricIdleFull Load (200 concurrency)
RSS Memory24.6 MB46.2 MB
Threads1111
CPU0%~550% (5.5 cores)

4. Competitive Benchmarks

4.1 Cross-Language HTTP Comparison (50 concurrency, 10s)

All engines evaluate equivalent 4-branch logic. Each engine runs on its standard HTTP stack.

EngineLanguageQPSAvgP50P99Max
Go net/http (hardcoded)Go70,1340.71 ms0.60 ms2.30 ms16.0 ms
OrdoRust58,3740.86 ms0.80 ms3.40 ms19.8 ms
OPAGo30,3981.64 ms0.80 ms8.50 ms51.8 ms
json-rules-engineNode.js17,2052.91 ms2.60 ms6.10 ms110.5 ms
GruleGo6,5477.63 ms7.40 ms16.2 ms39.0 ms

Go net/http hardcoded is the theoretical ceiling (no rule engine, just if/else + JSON codec). Ordo reaches 83% of it.

At 200 concurrency

EngineQPSAvgP99
Go net/http (hardcoded)69,2792.89 ms9.10 ms
Ordo60,3863.30 ms11.1 ms
OPA34,1665.85 ms27.8 ms
json-rules-engine16,29612.3 ms19.5 ms
Grule8,32824.0 ms50.6 ms

Memory comparison (100 concurrency load)

EngineIdleUnder LoadCPU
Go net/http7.9 MB20.1 MB354%
Ordo8.3 MB25.7 MB545%
Grule11.8 MB30.5 MB558%
OPA26.1 MB50.7 MB442%
json-rules-engine32.6 MB122.1 MB112%

4.2 Rust Engines — Core Engine Speed (no HTTP)

Measured with Criterion.rs. Each engine evaluates an equivalent 4-branch decision rule.

EnginePer-eval timevs OrdoNotes
Ordo573 ns1.0xBytecode VM with pre-compiled expressions
Rhai728 ns0.79xAST tree-walk interpreter
Zen Engine (GoRules)4,200 ns0.14xGraph traversal + per-eval clone

Why Ordo is faster at the core level

FactorOrdoZen EngineRhai
EvaluationRegister-based bytecode VM + optional Cranelift JITInterpreted graph traversalAST tree-walk interpretation
Rule structurePre-compiled step graph, direct jumpPer-eval Arc<DecisionContent> clone + graph walkPer-eval Scope allocation + String clone
Allocations per evalNear-zero (pre-compiled)Clone decision graph each timeNew Scope + variable copies
Expression compilationOne-time compile to bytecodePer-eval parse + interpretOne-time compile to AST

4.3 Rust Engines — HTTP Comparison (actix-web for Zen/Rhai)

To test HTTP-level throughput fairly, Zen and Rhai are wrapped in actix-web (keepalive enabled, same as Ordo's Axum).

Conc.Ordo (Axum)Zen (actix-web)Rhai (actix-web)
113,61116,77618,307
5057,334105,165113,749
20061,615122,519128,828

At HTTP level, Zen/Rhai show higher raw QPS because:

  1. Minimal handler — They receive flat JSON, run a trivial eval, return minimal JSON. No middleware, no rule store, no audit, no tenant checks.
  2. actix-web thread-per-core model — Lower overhead for simple handlers than Axum's work-stealing tokio runtime.
  3. Ordo does more per request — Rule store lookup (DashMap), middleware chain (tenant, role, audit sampling, request timeout), duration_us timing, structured response with output field.

CPU efficiency (100 concurrency):

EngineQPSCPUQPS/core
Rhai (actix-web)119K310%38,387
Zen (actix-web)121K348%34,770
Ordo (Axum)60K472%12,712

The gap is entirely in the HTTP serving layer, not the engine. As shown in §4.2, Ordo's core engine is 7.3x faster than Zen and 1.27x faster than Rhai.


5. Distributed Sync Functional Test Results

Full end-to-end validation of NATS JetStream sync between Writer and Reader instances.

TestResult
Writer/Reader startup + NATS connection
Writer creates rule → Reader auto-syncs
Writer updates rule (v1→v2) → Reader syncs
Writer creates 2nd rule → Reader syncs
Writer deletes rule → Reader syncs deletion
Reader executes synced rule (all branches)
Reader batch execution
Reader rejects write operations (409 Conflict)
Server stays alive indefinitely (no 30s timeout)

6. Bug Fixes During Benchmarking

6.1 Executor Timeout Hot-Path Regression

Root cause: default_timeout_ms was changed from 0 to 5000, causing Instant::elapsed() (a syscall, ~20-30ns on macOS) to be called every step in the executor hot loop.

Fix: Amortized timeout checking — skip the first 16 steps, then check every 16th step:

rust
// crates/ordo-core/src/rule/executor.rs
if timeout_ms > 0
    && depth >= 16
    && depth & 15 == 0
    && start_time.elapsed().as_millis() as u64 >= timeout_ms
{
    return Err(OrdoError::Timeout { timeout_ms });
}

Also applied conditional step timing (only call Instant::now() when tracing is enabled):

rust
let (step_result, step_duration) = if tracing {
    let step_start = Instant::now();
    let result = self.execute_step(step, &mut ctx, &ruleset.config.field_missing)?;
    (result, step_start.elapsed().as_micros() as u64)
} else {
    let result = self.execute_step(step, &mut ctx, &ruleset.config.field_missing)?;
    (result, 0)
};

Same amortized timeout fix applied to compiled_executor.rs.

Impact: minimal_compiled recovered from 398ns → 361ns, batch throughput from 2.50M → 2.70M ops/sec.

6.2 Server 30-Second Auto-Shutdown Bug

Root cause: In main.rs, tokio::time::timeout(shutdown_timeout, join_all(tasks)) started the 30-second countdown from server startup, not from receiving a shutdown signal. The server would unconditionally exit after 30 seconds.

Fix: Restructured to use tokio::select! — wait for either shutdown signal or unexpected task exit, only start the timeout countdown after a signal is received:

rust
let all_tasks = futures::future::join_all(tasks);
tokio::pin!(all_tasks);

tokio::select! {
    _ = shutdown_signal => {
        // Signal received — now start the timeout
        shutdown_tx.send(true).ok();
        match tokio::time::timeout(shutdown_timeout, &mut all_tasks).await {
            Ok(results) => { /* graceful */ }
            Err(_) => { warn!("Graceful shutdown timed out"); }
        }
    }
    results = &mut all_tasks => {
        // Servers exited on their own (crash or error)
        for r in results { r??; }
    }
}

7. Reproducing These Benchmarks

7.1 Core Engine Microbenchmarks

bash
# Run Ordo's built-in Criterion benchmarks
cargo bench -p ordo-core

7.2 Cross-Engine Core Benchmark

Create a Cargo project with the following Cargo.toml:

toml
[package]
name = "engine-bench"
version = "0.1.0"
edition = "2021"

[dependencies]
zen-engine = "0.25"
rhai = "1"
serde_json = "1"
criterion = { version = "0.5", features = ["html_reports"] }
tokio = { version = "1", features = ["rt"] }
ordo-core = { path = "path/to/crates/ordo-core" }

[[bench]]
name = "engines"
harness = false

Benchmark file benches/engines.rs:

rust
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ordo_core::prelude::*;
use serde_json::json;

fn ordo_setup() -> (RuleExecutor, RuleSet) {
    let json_str = r#"{
        "config": { "name": "bench", "entry_step": "s1" },
        "steps": {
            "s1": { "id":"s1","name":"S1","type":"decision","branches":[
                {"condition":"score >= 90","next_step":"high"},
                {"condition":"score >= 70","next_step":"mid"},
                {"condition":"score >= 50","next_step":"low"}
            ],"default_next":"fail"},
            "high": {"id":"high","name":"H","type":"terminal",
                     "result":{"code":"HIGH","message":"high tier"}},
            "mid":  {"id":"mid","name":"M","type":"terminal",
                     "result":{"code":"MID","message":"mid tier"}},
            "low":  {"id":"low","name":"L","type":"terminal",
                     "result":{"code":"LOW","message":"low tier"}},
            "fail": {"id":"fail","name":"F","type":"terminal",
                     "result":{"code":"FAIL","message":"failed"}}
        }
    }"#;
    (RuleExecutor::new(), RuleSet::from_json_compiled(json_str).unwrap())
}

fn zen_setup() -> (
    zen_engine::DecisionEngine<
        zen_engine::loader::NoopLoader,
        zen_engine::handler::custom_node_adapter::NoopCustomNode,
    >,
    std::sync::Arc<zen_engine::model::DecisionContent>,
) {
    let json_str = r#"{
        "nodes": [
            {"id":"input","type":"inputNode","name":"Input"},
            {"id":"table1","type":"decisionTableNode","name":"Check","content":{
                "inputs":[{"id":"s","name":"S","type":"expression","field":"score"}],
                "outputs":[
                    {"id":"c","name":"C","type":"expression","field":"code"},
                    {"id":"m","name":"M","type":"expression","field":"message"}
                ],
                "rules":[
                    {"_id":"r1","s":">= 90","c":"\"HIGH\"","m":"\"high tier\""},
                    {"_id":"r2","s":">= 70","c":"\"MID\"","m":"\"mid tier\""},
                    {"_id":"r3","s":">= 50","c":"\"LOW\"","m":"\"low tier\""},
                    {"_id":"r4","s":"< 50","c":"\"FAIL\"","m":"\"failed\""}
                ],
                "hitPolicy":"first"
            }},
            {"id":"output","type":"outputNode","name":"Output"}
        ],
        "edges":[
            {"id":"e1","sourceId":"input","targetId":"table1"},
            {"id":"e2","sourceId":"table1","targetId":"output"}
        ]
    }"#;
    let content: zen_engine::model::DecisionContent =
        serde_json::from_str(json_str).unwrap();
    (zen_engine::DecisionEngine::default(), std::sync::Arc::new(content))
}

fn rhai_setup() -> (rhai::Engine, rhai::AST) {
    let engine = rhai::Engine::new();
    let ast = engine.compile(r#"
        if score >= 90 { code = "HIGH"; message = "high tier"; }
        else if score >= 70 { code = "MID"; message = "mid tier"; }
        else if score >= 50 { code = "LOW"; message = "low tier"; }
        else { code = "FAIL"; message = "failed"; }
    "#).unwrap();
    (engine, ast)
}

fn bench_native(c: &mut Criterion) {
    c.bench_function("native_hardcoded", |b| {
        b.iter(|| {
            let score: f64 = black_box(75.0);
            let r = if score >= 90.0 { ("HIGH","high tier") }
                    else if score >= 70.0 { ("MID","mid tier") }
                    else if score >= 50.0 { ("LOW","low tier") }
                    else { ("FAIL","failed") };
            black_box(r)
        })
    });
}

fn bench_ordo(c: &mut Criterion) {
    let (executor, ruleset) = ordo_setup();
    let input: ordo_core::context::Value =
        serde_json::from_value(json!({"score": 75})).unwrap();
    c.bench_function("ordo_execute", |b| {
        b.iter(|| {
            black_box(
                executor.execute(black_box(&ruleset), black_box(input.clone())).unwrap()
            )
        })
    });
}

fn bench_zen(c: &mut Criterion) {
    let (engine, content) = zen_setup();
    let input = json!({"score": 75});
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all().build().unwrap();
    c.bench_function("zen_evaluate", |b| {
        b.iter(|| {
            let decision = engine.create_decision(content.clone().into());
            black_box(rt.block_on(decision.evaluate(black_box(&input))).unwrap())
        })
    });
}

fn bench_rhai(c: &mut Criterion) {
    let (engine, ast) = rhai_setup();
    c.bench_function("rhai_run_ast", |b| {
        b.iter(|| {
            let mut scope = rhai::Scope::new();
            scope.push("score", black_box(75.0_f64));
            scope.push("code", String::new());
            scope.push("message", String::new());
            let _ = engine.run_ast_with_scope(&mut scope, &ast);
            black_box(scope.get_value::<String>("code").unwrap())
        })
    });
}

criterion_group!(benches, bench_native, bench_ordo, bench_zen, bench_rhai);
criterion_main!(benches);

Run:

bash
cargo bench

7.3 HTTP Server Benchmark

bash
# Build server with NATS sync support
cargo build --release -p ordo-server --features nats-sync

# --- Standalone mode ---
./target/release/ordo-server \
  --role standalone --rules-dir /tmp/ordo-bench \
  -p 8080 --log-level error &

# Create test rule
curl -s -X POST http://localhost:8080/api/v1/rulesets \
  -H "Content-Type: application/json" \
  -d '{
    "config": {"name": "bench-rule", "entry_step": "s1", "version": "1.0.0"},
    "steps": {
      "s1": {"id":"s1","name":"S1","type":"decision","branches":[
        {"condition":"score >= 90","next_step":"high"},
        {"condition":"score >= 70","next_step":"mid"},
        {"condition":"score >= 50","next_step":"low"}
      ],"default_next":"fail"},
      "high": {"id":"high","name":"H","type":"terminal",
               "result":{"code":"HIGH","message":"high tier"}},
      "mid":  {"id":"mid","name":"M","type":"terminal",
               "result":{"code":"MID","message":"mid tier"}},
      "low":  {"id":"low","name":"L","type":"terminal",
               "result":{"code":"LOW","message":"low tier"}},
      "fail": {"id":"fail","name":"F","type":"terminal",
               "result":{"code":"FAIL","message":"failed"}}
    }
  }'

# Concurrency sweep
for C in 1 10 50 100 200 500; do
  echo "--- Concurrency: $C ---"
  hey -z 10s -c $C -m POST \
    -H "Content-Type: application/json" \
    -d '{"input":{"score":75}}' \
    http://localhost:8080/api/v1/execute/bench-rule
done

7.4 Distributed Mode Test

bash
# Start NATS
docker run -d --name nats -p 4222:4222 nats:latest -js

# Writer
./target/release/ordo-server \
  --role writer --rules-dir /tmp/ordo-writer \
  --nats-url nats://localhost:4222 --instance-id writer-1 \
  -p 8080 --grpc-port 50051 --log-level error &

# Reader
./target/release/ordo-server \
  --role reader --rules-dir /tmp/ordo-reader \
  --nats-url nats://localhost:4222 --instance-id reader-1 \
  --writer-addr http://localhost:8080 \
  -p 8081 --grpc-port 50052 --log-level error &

sleep 3

# Create rule on writer
curl -s -X POST http://localhost:8080/api/v1/rulesets \
  -H "Content-Type: application/json" \
  -d '{"config":{"name":"bench-rule","entry_step":"s1","version":"1.0.0"},"steps":{"s1":{"id":"s1","name":"S1","type":"decision","branches":[{"condition":"score >= 90","next_step":"high"},{"condition":"score >= 70","next_step":"mid"},{"condition":"score >= 50","next_step":"low"}],"default_next":"fail"},"high":{"id":"high","name":"H","type":"terminal","result":{"code":"HIGH","message":"high tier"}},"mid":{"id":"mid","name":"M","type":"terminal","result":{"code":"MID","message":"mid tier"}},"low":{"id":"low","name":"L","type":"terminal","result":{"code":"LOW","message":"low tier"}},"fail":{"id":"fail","name":"F","type":"terminal","result":{"code":"FAIL","message":"failed"}}}}'

# Wait for NATS sync
sleep 3

# Verify sync
curl -s http://localhost:8081/api/v1/rulesets  # Should show bench-rule

# Execute on reader
curl -s -X POST http://localhost:8081/api/v1/execute/bench-rule \
  -H "Content-Type: application/json" \
  -d '{"input":{"score":75}}'

# Benchmark writer and reader simultaneously
hey -z 10s -c 100 -m POST \
  -H "Content-Type: application/json" \
  -d '{"input":{"score":75}}' \
  http://localhost:8080/api/v1/execute/bench-rule &

hey -z 10s -c 100 -m POST \
  -H "Content-Type: application/json" \
  -d '{"input":{"score":85}}' \
  http://localhost:8081/api/v1/execute/bench-rule &

wait

# Cleanup
docker stop nats && docker rm nats

7.5 Cross-Language HTTP Comparison

bash
# --- OPA ---
brew install opa
cat > /tmp/credit.rego << 'EOF'
package credit
default result := {"code": "FAIL", "message": "failed"}
result := {"code": "VIP", "message": "VIP tier"} if { input.score >= 90 }
result := {"code": "HIGH", "message": "high tier"} if { input.score >= 70; input.score < 90 }
result := {"code": "MID", "message": "mid tier"} if { input.score >= 50; input.score < 70 }
EOF
opa run --server --addr :9000 /tmp/credit.rego &
hey -z 10s -c 50 -m POST -H "Content-Type: application/json" \
  -d '{"input":{"score":75}}' http://localhost:9000/v1/data/credit/result

# --- json-rules-engine (Node.js) ---
# See Section 7.6 for the full server code
cd /tmp/json-rules-bench && npm install && node server.js &
hey -z 10s -c 50 -m POST -H "Content-Type: application/json" \
  -d '{"score":75}' http://localhost:9001/execute

# --- Grule (Go) ---
# See Section 7.7 for the full server code
cd /tmp/grule-bench && go build -o server . && ./server &
hey -z 10s -c 50 -m POST -H "Content-Type: application/json" \
  -d '{"score":75}' http://localhost:9003/execute

7.6 json-rules-engine Server (Node.js)

javascript
// server.js
const { Engine } = require('json-rules-engine');
const fastify = require('fastify')({ logger: false });

function createEngine() {
  const engine = new Engine();
  engine.addRule({
    conditions: { all: [{ fact: 'score', operator: 'greaterThanInclusive', value: 90 }] },
    event: { type: 'VIP', params: { code: 'VIP', message: 'VIP tier' } },
    priority: 4,
  });
  engine.addRule({
    conditions: {
      all: [
        { fact: 'score', operator: 'greaterThanInclusive', value: 70 },
        { fact: 'score', operator: 'lessThan', value: 90 },
      ],
    },
    event: { type: 'HIGH', params: { code: 'HIGH', message: 'high tier' } },
    priority: 3,
  });
  engine.addRule({
    conditions: {
      all: [
        { fact: 'score', operator: 'greaterThanInclusive', value: 50 },
        { fact: 'score', operator: 'lessThan', value: 70 },
      ],
    },
    event: { type: 'MID', params: { code: 'MID', message: 'mid tier' } },
    priority: 2,
  });
  engine.addRule({
    conditions: { all: [{ fact: 'score', operator: 'lessThan', value: 50 }] },
    event: { type: 'FAIL', params: { code: 'FAIL', message: 'failed' } },
    priority: 1,
  });
  return engine;
}

const engine = createEngine();

fastify.post('/execute', async (request) => {
  const { score } = request.body;
  const { events } = await engine.run({ score });
  const top = events[0];
  return top ? top.params : { code: 'UNKNOWN', message: 'no match' };
});

fastify.get('/health', async () => ({ status: 'ok' }));
fastify.listen({ port: 9001, host: '0.0.0.0' });

7.7 Grule Server (Go)

go
// main.go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "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/pkg"
)

const grl = `
rule VIP "VIP" salience 4 { when Input.Score >= 90 then Output.Code = "VIP"; Output.Message = "VIP tier"; Retract("VIP"); }
rule High "High" salience 3 { when Input.Score >= 70 && Input.Score < 90 then Output.Code = "HIGH"; Output.Message = "high tier"; Retract("High"); }
rule Mid "Mid" salience 2 { when Input.Score >= 50 && Input.Score < 70 then Output.Code = "MID"; Output.Message = "mid tier"; Retract("Mid"); }
rule Fail "Fail" salience 1 { when Input.Score < 50 then Output.Code = "FAIL"; Output.Message = "failed"; Retract("Fail"); }
`

type InputFact struct{ Score float64 }
type OutputFact struct{ Code, Message string }

var knowledgeLibrary *ast.KnowledgeLibrary

func init() {
    knowledgeLibrary = ast.NewKnowledgeLibrary()
    rb := builder.NewRuleBuilder(knowledgeLibrary)
    _ = rb.BuildRuleFromResource("Rules", "0.1.0", pkg.NewBytesResource([]byte(grl)))
}

func execute(w http.ResponseWriter, r *http.Request) {
    var req struct{ Score float64 `json:"score"` }
    json.NewDecoder(r.Body).Decode(&req)
    input := &InputFact{Score: req.Score}
    output := &OutputFact{}
    kb, _ := knowledgeLibrary.NewKnowledgeBaseInstance("Rules", "0.1.0")
    dataCtx := ast.NewDataContext()
    dataCtx.Add("Input", input)
    dataCtx.Add("Output", output)
    eng := &engine.GruleEngine{MaxCycle: 10}
    eng.Execute(dataCtx, kb)
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"code": output.Code, "message": output.Message})
}

func main() {
    http.HandleFunc("/execute", execute)
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"status":"ok"}`)) })
    fmt.Println("grule listening on :9003")
    http.ListenAndServe(":9003", nil)
}

Released under the MIT License.