CompileFlow Documentation

repository·master·Indexed 24 days ago

https://github.com/alibaba/compileflow

A high-performance process engine that transforms BPMN 2.0 and TBBPM business process definitions into optimized Java code for native-speed, stateless, in-memory execution. It features a compile-then-execute model, support for Spring Boot integration via a dedicated starter, and tools for process preheating, debugging via ProcessToolingService, and custom ClassLoader configuration.

Tokens
19.2K
Snippets
36
Records
51
Agent score
84%

What's inside CompileFlow

  1. What is CompileFlow?

    master

    CompileFlow is a high-performance, compile-then-execute process engine designed for stateless, in-memory execution. It works by converting process files (BPMN 2.0 or TBBPM) into optimized Java code, which is then compiled and executed at native speeds.

    Key characteristics:

    • Ultra-High Performance: Achieves native Java performance via code generation.
    • Type-Safe: Provides compile-time validation.
    • Multi-Standard: Supports both BPMN 2.0 and TBBPM specifications.
    • Visual Design: Compatible with an IntelliJ IDEA plugin for modeling.
  2. Define a process using BPM XML

    master

    Processes are defined in XML files (e.g., .bpm) located in the classpath. Each process must have a unique code attribute in the root <bpm> element. This code is used to identify the process at runtime via ProcessSource.fromCode("...").

    Key XML elements include:

    • <var>: Defines process variables. The name must match the field names in your Java DTOs. inOutType can be param (input) or return (output).
    • <start>: The entry point of the process.
    • <decision>: A decision node that uses expression attributes on <transition> elements to route flow based on data.
    • <scriptTask>: Executes logic using an <action> (e.g., type ql).
    • <actionHandle>: Contains the logic expression and maps the result back to a process variable via <var>.
    <bpm code="bpm.ktv.quickstart" name="KTV Billing Process (Quickstart)" type="process">
        <var name="price" dataType="java.lang.Integer" inOutType="return"/>
        <var name="pList" dataType="java.util.List<java.lang.String>" inOutType="param"/>
    
        <start id="start" name="Start">
            <transition to="checkGroupSize"/>
        </start>
    
        <decision id="checkGroupSize" name="Large Group?">
            <transition to="calculateDiscountedPrice" name="Yes" expression="pList.size() > 3"/>
            <transition to="calculateStandardPrice" name="No"/>
        </decision>
    
        <scriptTask id="calculateStandardPrice" name="Calculate Standard Price">
            <transition to="end"/>
            <action type="ql">
                <actionHandle expression="pList.size() * 30">
                    <var name="price" dataType="java.lang.Integer" contextVarName="price" inOutType="return"/>
                </actionHandle>
            </action>
        </scriptTask>
    
        <end id="end" name="End"/>
    </bpm>
  3. Understand ProcessEngine resource costs

    master

    A ProcessEngine instance is a heavyweight object that manages multiple internal thread pools, including Compilation, Execution, Event, and Schedule pools. A single engine instance can manage 16-64 background threads.

    Warning: Creating a new ProcessEngine for every request or inside a loop will cause severe resource leaks and application instability. Always aim for a single singleton instance per application lifecycle.

  4. Implement process version management

    master

    CompileFlow is stateless and does not manage versions automatically. You must implement versioning at the application level using one of these three strategies:

    1. Distinct Codes: Use different identifiers for different versions (e.g., bpm.order.process.v1 and bpm.order.process.v2).
    2. Dynamic Content Overwrites: Load new content for the same code using ProcessSource.fromContent(code, content). This overwrites the existing cached entry for that code.
    3. Distinct File Paths: Use ProcessSource.fromFile(code, path). Note that since the code is used as the cache key, using the same code with different file paths will cause the latter to overwrite the former. To run parallel versions from files, use distinct codes.
    // Strategy 1: Use different codes to distinguish versions
    ProcessSource v1 = ProcessSource.fromCode("bpm.order.process.v1");
    ProcessSource v2 = ProcessSource.fromCode("bpm.order.process.v2");
    engine.execute(v2, ...);
    
    // Strategy 2: Load different content for the same code
    String contentV2 = loadFlowWithVersionFromDatabase("bpm.order.process", "v2");
    ProcessSource source = ProcessSource.fromContent("bpm.order.process", contentV2);
    engine.execute(source, ...); // Overwrites the cache entry for "bpm.order.process"
    
    // Strategy 3: Use different file paths
    ProcessSource v1File = ProcessSource.fromFile("bpm.order.process", "/flows/v1/order.bpm");
    ProcessSource v2File = ProcessSource.fromFile("bpm.order.process", "/flows/v2/order.bpm");
    // Note: fromFile uses the code to key the cache, so v2File will overwrite v1File.
  5. Choosing the right CompileFlow extension mechanism

    master

    CompileFlow provides three distinct ways to extend its functionality depending on your goal:

    1. Event Listeners: Use these to observe and react to engine events (e.g., for logging, metrics, or auditing) without modifying core logic.
    2. Extension Points: Use these to customize or replace specific engine behaviors (e.g., providing different strategy implementations like pricing rules).
    3. Service Providers: Use these to add major new components (e.g., implementing a new ProcessEngineProvider for a custom process type) that the engine must discover at startup.
  6. Understand CompileFlow Configuration Priority

    master

    CompileFlow uses a layered configuration model where settings are applied in a specific order of precedence. If a property is defined in multiple places, the source with the highest priority wins.

    Priority Order (Highest to Lowest):

    1. Programmatic Configuration: Settings applied via ProcessEngineConfig.builder() in your Java code.
    2. Spring Boot (application.yml): Properties defined in your Spring configuration files.
    3. Java System Properties: JVM startup arguments (e.g., -Dcompileflow.cache.runtime-max-size=5000).
    4. Internal Defaults: The engine's built-in default values.
  7. How hot deployment works in CompileFlow

    master

    CompileFlow's hot deployment allows for zero-downtime updates by enabling real-time modifications to business logic, bug fixes, or process flow adjustments without application restarts.

    It supports two primary modes:

    1. Manual Reload: Using ProcessAdminService.deploy() to explicitly trigger a re-lookup and re-compilation of a process.
    2. Automatic Deployment: Using a FlowHotDeployer combined with a detector (like FileSystemChangeDetector or NacosChangeDetector) to monitor external sources and trigger deployments automatically when changes are detected.

    This capability is commonly used for externalizing business rules, performing canary releases (by deploying new versions with distinct codes), and executing emergency rollbacks by redeploying known stable XML content.

  8. How CompileFlow monitoring works via SPI and Events

    master

    CompileFlow uses an event-driven architecture based on Java's Service Provider Interface (SPI) to enable monitoring without modifying the core engine.

    1. ProcessEventListener Interface: The primary extension point. You implement this to react to engine events.
    2. Engine Events: The ProcessEngine publishes events (e.g., process start, completion, or failure) at critical lifecycle points.
    3. SPI Registration: Custom listeners are discovered at startup by placing a file in META-INF/extensions/ containing the fully qualified name of your listener class.
  9. Configure CompileFlow for Spring Boot

    master

    For most Spring Boot applications, you can configure the engine by adding properties to your application.yml.

    Recommended Primary Settings:

    • compileflow.model-type: Choose TBBPM (optimized for enterprise) or BPMN (international standard).
    • compileflow.executor.compilation-threads: Number of threads for compiling flows (Recommended: 1-4).
    • compileflow.executor.execution-threads: Number of threads for executing flows (Recommended: Start with CPU core count).
    • compileflow.cache.runtime-max-size: Max compiled flows in memory (Recommended: 2000-10000 for production).
    • compileflow.observability.enabled: Enable monitoring features like metrics and tracing (Essential for production).
    compileflow:
      model-type: TBBPM
      executor:
        compilation-threads: 2
        execution-threads: 16
      cache:
        runtime-max-size: 2000
      observability:
        enabled: true
  10. Use ProcessEngine with Spring Boot (Recommended)

    master

    If using Spring Boot with the compileflow-spring-boot-starter, you do not need to manually manage the engine lifecycle. The starter automatically configures a singleton ProcessEngine. You can simply inject it into your services using @Autowired.

    import com.alibaba.compileflow.engine.ProcessEngine;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class OrderService {
        
        // ✅ CORRECT: Inject the singleton engine managed by Spring.
        @Autowired
        private ProcessEngine<TbbpmModel> engine;
        
        public void processOrder(OrderRequest request) {
            // Safe and efficient: all requests share the same engine and thread pools.
            engine.execute(ProcessSource.fromCode("bpm.order.process"), request, OrderResponse.class);
        }
    }
  11. Handle execution errors in ProcessResult

    master

    When executing a process, always check ProcessResult.isSuccess() before accessing data. If it returns false, use getErrorMessage() and getTraceId() to diagnose the failure. The error message often contains keywords like compilation or timeout which can be used to trigger specific error-handling logic.

    ProcessResult<Map<String, Object>> result = engine.execute(source, context);
    
    if (!result.isSuccess()) {
        String errorMessage = result.getErrorMessage();
        String traceId = result.getTraceId();
    
        // Log the error with a trace ID for correlation
        logger.error("Process execution failed, trace ID: {}, error message: {}", traceId, errorMessage);
    
        // Optionally, handle different error types
        if (errorMessage.contains("compilation")) {
            // Handle compilation errors, maybe trigger an alert
        } else if (errorMessage.contains("timeout")) {
            // Handle timeout errors
        }
    }