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
| Metric | Value |
|---|---|
| Core engine execution (4-branch rule) | 573 ns |
| HTTP single-instance peak QPS | 62,511 |
| HTTP distributed (Writer + Reader combined) | 83,082 |
| Batch execution peak throughput | 3.06 M rules/sec |
| Memory under full load | 46.2 MB |
| vs Zen Engine (Rust competitor) core speed | 7.3x faster |
| vs OPA (Go) HTTP throughput | 1.9x faster |
| vs json-rules-engine (Node.js) HTTP throughput | 3.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
| Engine | Time per eval | Relative | Theoretical single-core QPS |
|---|---|---|---|
Native Rust if/else (ceiling) | 3.5 ns | — | 285,000,000 |
| Ordo (bytecode VM) | 573 ns | 1.0x | 1,746,000 |
| Rhai (AST interpreter) | 728 ns | 0.79x | 1,374,000 |
| Zen Engine / GoRules (graph eval) | 4,200 ns | 0.14x | 238,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)
| Expression | Time |
|---|---|
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)
| Expression | Time |
|---|---|
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
| Scenario | Time |
|---|---|
| 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)
| Operation | Time |
|---|---|
| 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}}.
| Concurrency | QPS | Avg Latency | P50 | P95 | P99 | Max | CPU% |
|---|---|---|---|---|---|---|---|
| 1 | 14,634 | 0.07 ms | 0.06 ms | 0.10 ms | 0.10 ms | 13.9 ms | 56% |
| 10 | 47,960 | 0.21 ms | 0.19 ms | 0.30 ms | 0.40 ms | 10.6 ms | 423% |
| 25 | 57,202 | 0.44 ms | 0.37 ms | 0.70 ms | 1.60 ms | 28.5 ms | 359% |
| 50 | 59,440 | 0.84 ms | 0.76 ms | 1.60 ms | 2.90 ms | 51.9 ms | 411% |
| 100 | 61,057 | 1.64 ms | 1.45 ms | 3.20 ms | 4.90 ms | 31.0 ms | 311% |
| 200 | 62,511 | 3.18 ms | 2.78 ms | 7.00 ms | 9.90 ms | 39.2 ms | 309% |
| 500 | 60,577 | 8.25 ms | 6.60 ms | 20.4 ms | 29.2 ms | 76.3 ms | 289% |
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)
| Concurrency | QPS | Avg | P50 | P99 |
|---|---|---|---|---|
| 1 | 12,904 | 0.08 ms | 0.07 ms | 0.20 ms |
| 10 | 44,855 | 0.22 ms | 0.19 ms | 0.50 ms |
| 50 | 56,255 | 0.89 ms | 0.80 ms | 3.20 ms |
| 100 | 58,542 | 1.71 ms | 1.50 ms | 5.00 ms |
| 200 | 58,596 | 3.41 ms | 2.90 ms | 10.9 ms |
Reader (with NATS subscriber)
| Concurrency | QPS | Avg | P50 | P99 |
|---|---|---|---|---|
| 1 | 14,183 | 0.07 ms | 0.07 ms | 0.10 ms |
| 10 | 45,588 | 0.22 ms | 0.19 ms | 0.50 ms |
| 50 | 58,549 | 0.85 ms | 0.80 ms | 3.00 ms |
| 100 | 59,680 | 1.68 ms | 1.50 ms | 5.10 ms |
| 200 | 60,204 | 3.32 ms | 2.80 ms | 10.7 ms |
NATS sync has zero hot-path overhead — Writer/Reader QPS matches Standalone mode.
Simultaneous Writer + Reader (100 concurrency each)
| Role | QPS | CPU | RSS |
|---|---|---|---|
| Writer | 40,992 | 193% | 30.9 MB |
| Reader | 42,090 | 205% | 23.6 MB |
| Combined | 83,082 | — | 54.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 Size | Requests/sec | Rules Executed/sec | Avg Latency |
|---|---|---|---|
| 10 | 53,018 | 530,180 | 0.9 ms |
| 50 | 33,808 | 1,690,400 | 1.5 ms |
| 100 | 22,049 | 2,204,900 | 2.3 ms |
| 500 | 6,128 | 3,064,000 | 8.2 ms |
3.4 Resource Usage
| Metric | Idle | Full Load (200 concurrency) |
|---|---|---|
| RSS Memory | 24.6 MB | 46.2 MB |
| Threads | 11 | 11 |
| CPU | 0% | ~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.
| Engine | Language | QPS | Avg | P50 | P99 | Max |
|---|---|---|---|---|---|---|
Go net/http (hardcoded) | Go | 70,134 | 0.71 ms | 0.60 ms | 2.30 ms | 16.0 ms |
| Ordo | Rust | 58,374 | 0.86 ms | 0.80 ms | 3.40 ms | 19.8 ms |
| OPA | Go | 30,398 | 1.64 ms | 0.80 ms | 8.50 ms | 51.8 ms |
| json-rules-engine | Node.js | 17,205 | 2.91 ms | 2.60 ms | 6.10 ms | 110.5 ms |
| Grule | Go | 6,547 | 7.63 ms | 7.40 ms | 16.2 ms | 39.0 ms |
Go
net/httphardcoded is the theoretical ceiling (no rule engine, justif/else+ JSON codec). Ordo reaches 83% of it.
At 200 concurrency
| Engine | QPS | Avg | P99 |
|---|---|---|---|
Go net/http (hardcoded) | 69,279 | 2.89 ms | 9.10 ms |
| Ordo | 60,386 | 3.30 ms | 11.1 ms |
| OPA | 34,166 | 5.85 ms | 27.8 ms |
| json-rules-engine | 16,296 | 12.3 ms | 19.5 ms |
| Grule | 8,328 | 24.0 ms | 50.6 ms |
Memory comparison (100 concurrency load)
| Engine | Idle | Under Load | CPU |
|---|---|---|---|
Go net/http | 7.9 MB | 20.1 MB | 354% |
| Ordo | 8.3 MB | 25.7 MB | 545% |
| Grule | 11.8 MB | 30.5 MB | 558% |
| OPA | 26.1 MB | 50.7 MB | 442% |
| json-rules-engine | 32.6 MB | 122.1 MB | 112% |
4.2 Rust Engines — Core Engine Speed (no HTTP)
Measured with Criterion.rs. Each engine evaluates an equivalent 4-branch decision rule.
| Engine | Per-eval time | vs Ordo | Notes |
|---|---|---|---|
| Ordo | 573 ns | 1.0x | Bytecode VM with pre-compiled expressions |
| Rhai | 728 ns | 0.79x | AST tree-walk interpreter |
| Zen Engine (GoRules) | 4,200 ns | 0.14x | Graph traversal + per-eval clone |
Why Ordo is faster at the core level
| Factor | Ordo | Zen Engine | Rhai |
|---|---|---|---|
| Evaluation | Register-based bytecode VM + optional Cranelift JIT | Interpreted graph traversal | AST tree-walk interpretation |
| Rule structure | Pre-compiled step graph, direct jump | Per-eval Arc<DecisionContent> clone + graph walk | Per-eval Scope allocation + String clone |
| Allocations per eval | Near-zero (pre-compiled) | Clone decision graph each time | New Scope + variable copies |
| Expression compilation | One-time compile to bytecode | Per-eval parse + interpret | One-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) |
|---|---|---|---|
| 1 | 13,611 | 16,776 | 18,307 |
| 50 | 57,334 | 105,165 | 113,749 |
| 200 | 61,615 | 122,519 | 128,828 |
At HTTP level, Zen/Rhai show higher raw QPS because:
- Minimal handler — They receive flat JSON, run a trivial eval, return minimal JSON. No middleware, no rule store, no audit, no tenant checks.
- actix-web thread-per-core model — Lower overhead for simple handlers than Axum's work-stealing tokio runtime.
- Ordo does more per request — Rule store lookup (DashMap), middleware chain (tenant, role, audit sampling, request timeout),
duration_ustiming, structured response withoutputfield.
CPU efficiency (100 concurrency):
| Engine | QPS | CPU | QPS/core |
|---|---|---|---|
| Rhai (actix-web) | 119K | 310% | 38,387 |
| Zen (actix-web) | 121K | 348% | 34,770 |
| Ordo (Axum) | 60K | 472% | 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.
| Test | Result |
|---|---|
| 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:
// 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):
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:
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
# Run Ordo's built-in Criterion benchmarks
cargo bench -p ordo-core7.2 Cross-Engine Core Benchmark
Create a Cargo project with the following Cargo.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 = falseBenchmark file benches/engines.rs:
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:
cargo bench7.3 HTTP Server Benchmark
# 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
done7.4 Distributed Mode Test
# 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 nats7.5 Cross-Language HTTP Comparison
# --- 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/execute7.6 json-rules-engine Server (Node.js)
// 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)
// 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)
}