lib-bpmn-engine

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 26, 2022 License: MIT

README

lib-bpmn-engine

Motivation

A BPMN engine, meant to be embedded in Go applications with minimum hurdles, and a pleasant developer experience using it. This approach can increase transparency of code/implementation for non-developers.

This library is meant to be embedded in your application and should not introduce more runtime-dependencies. Hence, there's no database support built nor planned. Also, the engine is not agnostic to any high availability approaches, like multiple instances or similar.

Philosophies around BPMN

The BPMN specification in its core is a set of graphical symbols (rectangles, arrows, etc.) and a standard definition about how to read/interpret them. With this foundation, it's an excellent opportunity to enrich transparency or communication or discussions about implementation details. So BPMN has a great potential to support me as a developer to not write documentation into a wiki but rather expose the business process via well known symbols/graphics.

There's a conceptual similarity in usage between BPMN and OpenAPI/Swagger. As developers, on the one hand side we often use OpenAPI/Swagger to document our endpoints, HTTP methods, and purpose of the (HTTP) interface, our services offer. Hence, we enable others to use and integrate them. With BPMN on the other hand it can be conceptual similar, when it comes to share internal behaviour of our services. I see even larger similarity, when it comes to the question: How do I maintain the documentation? Again, on the one hand side with OpenAPI/Swagger, we tend to either use reflection and code generators or we follow the API spec first approach. The later one is addressed by this library in the BPMN context: Business Process spec first approach

Roadmap
v0.1.0

progress milestone v0.1.0

For the first release I would like to have service tasks and events fully supported.

v0.2.0

progress milestone v0.2.0

With basic element support, I would like to add visualization/monitoring capabilities. If the idea of using Zeebe's exporter protocol is not too complex, that would be ideal. If not, a simple console logger might do the job as well.

v0.3.0

progress milestone v0.3.0

With basic element and visualization support, I would like to add expression language support as well as support for correlation keys

Build status

test action status codecov Documentation Status

Project status

  • very early stage
  • contributors welcome

Documentation

WiP... https://nitram509-lib-bpmn-engine.readthedocs-hosted.com/

GoDoc: https://pkg.go.dev/github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine

Requires Go v1.16+

BPMN Modelling

All these examples are build with Camunda Modeler Community Edition. I would like to send a big "thank you", to Camunda for providing such tool.

Usage Examples

Hello World

Assuming this simple 'Hello World' BPMN example should just print "hello world".
hello_world.png

Then a simple (and verbose) code to execute this looks like this

package main

import (
	"fmt"
	"github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
)

func main() {
	// create a new named engine
	bpmnEngine := bpmn_engine.New("a name")
	// basic example loading a BPMN from file,
	process, err := bpmnEngine.LoadFromFile("simple_task.bpmn")
	if err != nil {
		panic("file \"simple_task.bpmn\" can't be read.")
	}
	// register a handler for a service task by defined task type
	bpmnEngine.AddTaskHandler("hello-world", printContextHandler)
	// setup some variables
	variables := map[string]string{}
	variables["foo"] = "bar"
	// and execute the process
	bpmnEngine.CreateAndRunInstance(process.ProcessKey, variables)
}

func printContextHandler(job bpmn_engine.ActivatedJob) {
	println("< Hello World >")
	println(fmt.Sprintf("ElementId                = %s", job.ElementId))
	println(fmt.Sprintf("BpmnProcessId            = %s", job.BpmnProcessId))
	println(fmt.Sprintf("ProcessDefinitionKey     = %d", job.ProcessDefinitionKey))
	println(fmt.Sprintf("ProcessDefinitionVersion = %d", job.ProcessDefinitionVersion))
	println(fmt.Sprintf("CreatedAt                = %s", job.CreatedAt))
	println(fmt.Sprintf("Variable 'foo'           = %s", job.GetVariable("foo")))
	job.Complete() // don't forget this one, or job.Fail("foobar")
}
A microservice API example

The following example snippet shows how a microservice could use BPMN engine to process orders and provides status feedback to clients.

For this example, we leverage messages and timers, to orchestrate some tasks. hello_world.png

For this microservice, we first define some simple API.

package main

import "net/http"

func initHttpRoutes() {
	http.HandleFunc("/api/order", handleOrder)                                        // POST new or GET existing Order
	http.HandleFunc("/api/receive-payment", handleReceivePayment)                     // webhook for the payment system
	http.HandleFunc("/show-process.html", handleShowProcess)                          // shows the BPMN diagram
	http.HandleFunc("/index.html", handleIndex)                                       // the index page
	http.HandleFunc("/", handleIndex)                                                 // the index page
	http.HandleFunc("/ordering-items-workflow.bpmn", handleOrderingItemsWorkflowBpmn) // the BPMN file, for documentation purpose
}

Then we initialize the BPMN engine and register a trivial handler, which just prints on STDOUT.

package main

import (
	"fmt"
	"github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
	"time"
)

func initBpmnEngine() {
	bpmnEngine = bpmn_engine.New("Ordering-Microservice")
	process, _ = bpmnEngine.LoadFromBytes(OrderingItemsWorkflowBpmn)
	bpmnEngine.AddTaskHandler("validate-order", printHandler)
	bpmnEngine.AddTaskHandler("send-bill", printHandler)
	bpmnEngine.AddTaskHandler("send-friendly-reminder", printHandler)
	bpmnEngine.AddTaskHandler("update-accounting", updateAccountingHandler)
	bpmnEngine.AddTaskHandler("package-and-deliver", printHandler)
	bpmnEngine.AddTaskHandler("send-cancellation", printHandler)
}

func printHandler(job bpmn_engine.ActivatedJob) {
	// do important stuff here
	println(fmt.Sprintf("%s >>> Executing job '%s", time.Now(), job.ElementId))
	job.Complete()
}

func updateAccountingHandler(job bpmn_engine.ActivatedJob) {
	println(fmt.Sprintf("%s >>> Executing job '%s", time.Now(), job.ElementId))
	println(fmt.Sprintf("%s >>> update ledger revenue account with amount=%s", time.Now(), job.GetVariable("amount")))
	job.Complete()
}

Since the /api/order endpoint can be requested with the GET or POST method, we need to make the handler smart enough to either create an order process instance or respond a status

package main

import (
	_ "embed"
	"fmt"
	"net/http"
	"strconv"
)

func handleOrder(writer http.ResponseWriter, request *http.Request) {
	if request.Method == "POST" {
		createNewOrder(writer, request)
	} else if request.Method == "GET" {
		showOrderStatus(writer, request)
	}
}

func createNewOrder(writer http.ResponseWriter, request *http.Request) {
	instance, _ := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
	redirectUrl := fmt.Sprintf("/show-process.html?orderId=%d", instance.GetInstanceKey())
	http.Redirect(writer, request, redirectUrl, http.StatusFound)
}

func showOrderStatus(writer http.ResponseWriter, request *http.Request) {
	orderIdStr := request.URL.Query()["orderId"][0]
	orderId, _ := strconv.ParseInt(orderIdStr, 10, 64)
	instance := bpmnEngine.FindProcessInstanceById(orderId)
	if instance != nil {
		// we re-use this GET request to ensure we catch up the timers - ideally the service uses internal timers instead
		bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
		bytes, _ := prepareJsonResponse(orderIdStr, instance.GetState(), instance.GetCreatedAt())
		writer.Header().Set("Content-Type", "application/json")
		writer.Write(bytes)
		return
	}
	http.NotFound(writer, request)
}

Also, for the incoming payments, our microservice provides an endpoint so that we get informed by external payment service. This handler sends a message to the process instance and continues.

package main

import (
	_ "embed"
	"net/http"
	"strconv"
)

func handleReceivePayment(writer http.ResponseWriter, request *http.Request) {
	orderIdStr := request.FormValue("orderId")
	amount := request.FormValue("amount")
	if len(orderIdStr) > 0 && len(amount) > 0 {
		orderId, _ := strconv.ParseInt(orderIdStr, 10, 64)
		processInstance := bpmnEngine.FindProcessInstanceById(orderId)
		if processInstance != nil {
			processInstance.SetVariable("amount", amount)
			bpmnEngine.PublishEventForInstance(processInstance.GetInstanceKey(), "payment-received")
			bpmnEngine.RunOrContinueInstance(processInstance.GetInstanceKey())
			http.Redirect(writer, request, "/", http.StatusFound)
			return
		}
	}
	writer.WriteHeader(400)
	writer.Write([]byte("Bad request: the request must contain form data with 'orderId' and 'amount', and the order must exist"))
}

To get the snippet compile, see the other sources in the examples/ordering_microservice/ folder.

Supported BPMN elements

  • Start Event
  • End Event
  • Service Task
    • Get & Set variables from/to context (of the instance)
  • Forks
    • controlled and uncontrolled forks are supported
    • Parallel Gateway supported
  • Joins
    • uncontrolled and exclusive joins are supported
    • parallel joins are supported
  • Message Intermediate Catch Event
    • at the moment, just matching/correlation by name supported
    • TODO: introduce correlation key
  • Timer Intermediate Catch Event

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL