Ice Rule Engine Detailed Guide
Comprehensive guide to mastering visual business orchestration with Ice rule engine
Rule Engine Node Development
Ice rule engine uses node-based design where each node represents an independent business logic unit. By combining different node types, you can implement complex business rule orchestration.
Three Leaf Node Types
Ice rule engine provides three leaf node types for different business scenarios:
1. Flow Node - Process Control
- Purpose: Control business flow in the rule engine
- Scenarios: Condition judgment, filtering rules, permission validation
- Return Value: Clear true (pass) or false (fail)
- Examples: User level judgment, amount range validation, time condition filtering
2. Result Node - Result Processing
- Purpose: Execute specific business operations in the rule engine
- Scenarios: Reward distribution, inventory deduction, send notifications
- Return Value: true (execution success) or false (execution failure)
- Examples: Coupon distribution, points reward, balance recharge
3. None Node - Auxiliary Operations
- Purpose: Auxiliary nodes that don't affect business flow
- Scenarios: Data query, logging, information assembly
- Return Value: No return value (none)
- Examples: User info query, event tracking, cache warming
Node Annotations
Ice 2.0 provides node annotations to enrich node metadata:
@IceNode(
name = "Amount Reward",
desc = "Send specified amount to user",
order = 10 // Sort priority, smaller value means higher priority
)
public class AmountResult extends BaseLeafRoamResult {
// ...
}
Field Annotations
@Data
public class AmountResult extends BaseLeafRoamResult {
@IceField(name = "User Key", desc = "Key to get user ID from roam")
private String key;
@IceField(name = "Amount", desc = "Amount value to send", type = "double")
private double value;
@IceIgnore // Ignore this field, not shown in configuration UI
private String internalField;
}
Node Base Class Selection
Ice rule engine provides three base classes for developers to inherit, choose based on parameter type:
- BaseLeaf* - Uses IceContext as method parameter, implement do* method
- BaseLeafPack* - Uses IcePack as method parameter, implement doPack* method
- BaseLeafRoam* - Uses IceRoam as method parameter, implement doRoam* method
Example:
/**
* Issue balance node
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AmountResult extends BaseLeafRoamResult { // The uid to send balance is obtained from roam
@Resource
private SendService sendService; // For Spring applications, use spring beans directly. For non-Spring applications, initialize IceBeanFactory in IceBeanUtils
private String key; // Configurable uidKey
private double value; // Configurable balance value
@Override
protected boolean doRoamResult(IceRoam roam) { // Implement doRoamResult (your business logic)
Integer uid = roam.getMulti(key); // Get the uid from roam
if (uid == null || value <= 0) {
return false;
}
boolean res = sendService.sendAmount(uid, value); // Call third-party API to send balance
roam.put("SEND_AMOUNT", res); // Put result back to roam for subsequent use
return res; // Return result
}
}
Execute Ice
Assemble IcePack
pack is the package that needs to be assembled before executing ice
- iceId The ID of the rule to trigger, corresponds to backend configuration ID, iceId can only trigger one configured rule
- scene The scene to trigger, all rules subscribed to this scene will be triggered
- confId Trigger rule with any node ID as root
- requestTime Request time, default System.currentTimeMillis()
- roam Put the parameters and other information required to execute the rule
- traceId Trace ID, automatically generated by default
- debug Log printing, refer to DebugEnum. The final debug executed is handler.debug|pack.debug
Call the Ice method
- void syncProcess(IcePack pack) Execute synchronously
- List<Future<IceContext>> asyncProcess(IcePack pack) Execute asynchronously and return futures
In business, execution results may be added to roam, and desired data can be obtained from roam after execution.
IceRoam
Roam provides the data source required for node execution or stores execution results for subsequent execution. Roam is an extension of ConcurrentHashMap.
- put/get Rewrites ConcurrentHashMap's put/get, ignoring null pointer exceptions
- putValue/getValue Ignores type matching checks, saves casting operations, be careful about type matching
- putMulti/getMulti Use "." to separate and build hierarchical data structures
- getUnion If the parameter starts with "@", gets data from roam, otherwise returns the parameter itself
roam.putValue("a", 1); //{"a":1}
roam.getValue("a"); //1
roam.putMulti("b.c", 2); //{"a":1,"b":{"c":2}}
roam.putMulti("b.d", 3); //{"a":1,"b":{"c":2,"d":3}}
roam.getMutli("b"); //{"c":2,"d":3}
roam.getMutli("b.c"); //2
roam.getUnion("a"); //"a"
roam.getUnion("@a"); //1
roam.getUnion(1); //1
roam.put("e", "@a");
roam.getUnion("@e");//1
roam.put("e", "a");
roam.getUnion("@e");//"a"
Backend Configuration
app
app is used to distinguish different applications. For example, ice-test with app=1, when ice-test starts, it will pull all configurations for app 1 from the file system according to the configuration
Add ice
- ID iceId can be triggered by iceId
- Name Description
- Scene Subscribed scenes, use "," to separate multiple scenes, triggered when any scene occurs
- Configuration ID Root node ID of the ice tree
- Debug Log printing, refer to DebugEnum, accumulate values for content to print
- Operations
- Edit Edit ice
- View Details View detailed node configuration
- Backup Backup configuration
- Backup History Can restore from historical backups
- Export Export current configuration (including unpublished changes)
Configure Node
Click a node to pop up related operations
- View/Edit Nodes
- Add Child Nodes Only for relationship nodes
- Add Pre-Node Add pre-execution node
- Convert Node Can convert current node to any node
- Move Node Up/Down Move node
- Delete This Node Node deletion is soft delete, just disconnects, not physically deleted, can be added back by node ID
Other Configuration:
- confName Class name of leaf node. First time adding needs full class name, will validate if class exists in client
- Node ID Adding child nodes by node ID is object-level reusability. Nodes with same ID only have one copy in memory
- record Node's debug is only for display in processInfo
- inverse Invert node, if node should return false, returns true after inversion
Publish, Clear, Import, Export
- Publish All changes are only hot updated to client after publishing, unpublished changes have "^" mark
- Clear Clear all changes, revert to last published version
- Import Import configuration
- Export Export current configuration (including unpublished changes)
- Instance Selection
- Server Server configuration, currently only Server mode supports editing
- Client:host/app/uniqueId Real configuration display in client, only supports viewing
Storage Architecture (2.0 New Feature)
Ice 2.0 uses file system storage, completely removing MySQL dependency.
Directory Structure
ice-data/
├── apps/ # Application configurations
│ ├── _id.txt # Application ID generator
│ └── {app}.json # Application configuration file
├── clients/ # Client information
│ └── {app}/ # Grouped by application
│ ├── {address}.json # Client heartbeat file
│ └── _latest.json # Latest client info (O(1) read)
└── {app}/ # Application rule configurations
├── version.txt # Current version number
├── _base_id.txt # Base ID generator
├── _conf_id.txt # Conf ID generator
├── _push_id.txt # Push ID generator
├── bases/ # Base rule configurations
│ └── {baseId}.json
├── confs/ # Conf node configurations
│ └── {confId}.json
├── updates/ # Pending changes
│ └── {iceId}/
│ └── {confId}.json
├── versions/ # Version incremental updates
│ └── {version}_upd.json
└── history/ # Publish history
└── {pushId}.json
Version Sync Mechanism
Server Publishes Configuration:
- Updates configuration files in
bases/andconfs/directories - Generates incremental update file in
versions/{version}_upd.json - Updates
version.txtversion number
- Updates configuration files in
Client Polls for Updates:
- Periodically checks
version.txtversion number - Reads incremental update file when new version is found
- Falls back to full load if incremental file is missing
- Periodically checks
Heartbeat Mechanism:
- Client periodically writes heartbeat file to
clients/{app}/ - Server determines if Client is online via heartbeat file
- Clients without heartbeat updates beyond timeout are marked offline
- Client periodically writes heartbeat file to
Configuration File Format
All configurations stored in JSON format for easy version control and manual review.
Base Configuration Example:
{
"id": 1,
"app": 1,
"name": "User Level Activity",
"scenes": "user_login,user_upgrade",
"confId": 1,
"status": 1,
"debug": 0,
"createAt": 1701234567890,
"updateAt": 1701234567890
}
Conf Configuration Example:
{
"id": 1,
"app": 1,
"confId": 1,
"type": 5,
"confName": "com.ice.test.flow.LevelFlow",
"confField": "{\"level\": 5}",
"sonIds": "2,3",
"status": 1,
"createAt": 1701234567890,
"updateAt": 1701234567890
}
Multi-Instance Deployment
Shared Storage Solution
Multiple Server/Client instances need to share the same storage directory:
# docker-compose.yml example
services:
ice-server-1:
image: waitmoon/ice-server:2.0.0
ports:
- "8121:8121"
volumes:
- /shared/ice-data:/app/ice-data # Shared storage
ice-server-2:
image: waitmoon/ice-server:2.0.0
ports:
- "8122:8121"
volumes:
- /shared/ice-data:/app/ice-data # Same shared storage
Recommended Shared Storage Solutions
- NFS: Network File System, suitable for internal network environments
- Cloud Drives: Alibaba Cloud NAS, AWS EFS, and other cloud storage services
- Distributed File Systems: GlusterFS, CephFS, etc.
Combining with Expression Engine
Ice can integrate various expression engines like Aviator to simplify node configuration.
Example: Using Aviator as an example, if you want to create an Aviator-based Flow node:
@Data
@EqualsAndHashCode(callSuper = true)
public class AviatorFlow extends BaseLeafRoamFlow {
private String exp; // Aviator expression for configuration and hot update
private Expression compiledExpression;
@Override
protected boolean doRoamFlow(IceRoam roam) {
return (boolean) compiledExpression.execute(roam);
}
public void setExp(String exp) { // For better performance, recompile when setting/updating expression
this.exp = exp;
this.compiledExpression = AviatorEvaluator.compile(exp, true);
}
}
Node Error Handling
- Unified Handling: IceErrorHandle
Set the handle instance of IceErrorHandle (inherit IceErrorHandle and implement the handle method), IceErrorHandle.setHandle(IceErrorHandle customHandle) to change unified error handling for all nodes; the default implementation is DefaultIceErrorHandle: directly return NodeRunStateEnum.SHUT_DOWN, do not process and terminate the entire flow.
- Leaf Node Override errorHandle Method
Leaf node rewrites NodeRunStateEnum errorHandle(IceContext cxt, Throwable t) method to handle errors in current leaf node. If NodeRunStateEnum.SHUT_DOWN is returned, the entire flow will be terminated. If leaf node overrides errorHandle method, unified error handling won't be called, but can call super.errorHandle(cxt, t) for unified processing.
- Configuration Handling
Node provides iceErrorStateEnum configuration. If not empty, it has highest priority and will use configuration as return value first. Configuration handling only affects return value, the errorHandle method/unified handle processing method will still be executed.
Server Configuration Details
Complete Ice Server configuration options:
server:
port: 8121
ice:
storage:
# File storage path
path: ./ice-data
# Client inactive timeout (seconds)
# Clients without heartbeat updates beyond this time will be marked offline
client-timeout: 60
# Version file retention count
# Old version incremental files exceeding this count will be cleaned up
version-retention: 1000
Client Configuration Details
Complete Ice Client configuration options:
ice:
# Application ID (required)
app: 1
storage:
# File storage path (required, needs to be shared with Server)
path: ./ice-data
# Leaf node scan package paths
# Multiple packages separated by comma, not configured scans all (slower)
scan: com.ice.test
# Version polling interval (seconds), default 5 seconds
poll-interval: 5
# Heartbeat update interval (seconds), default 10 seconds
heartbeat-interval: 10
pool:
# Thread pool parallelism, default -1 uses ForkJoinPool default configuration
parallelism: -1