diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..6f32695 --- /dev/null +++ b/404.html @@ -0,0 +1,609 @@ + + + +
+ + + + + + + + + + + + + + +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. +
+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"
+ "time"
+
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
+)
+
+func initBpmnEngine() {
+ bpmnEngine = bpmn_engine.New()
+ process, _ = bpmnEngine.LoadFromBytes(OrderingItemsWorkflowBpmn)
+ bpmnEngine.NewTaskHandler().Id("validate-order").Handler(printHandler)
+ bpmnEngine.NewTaskHandler().Id("send-bill").Handler(printHandler)
+ bpmnEngine.NewTaskHandler().Id("send-friendly-reminder").Handler(printHandler)
+ bpmnEngine.NewTaskHandler().Id("update-accounting").Handler(updateAccountingHandler)
+ bpmnEngine.NewTaskHandler().Id("package-and-deliver").Handler(printHandler)
+ bpmnEngine.NewTaskHandler().Id("send-cancellation").Handler(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.Variable("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.FindProcessInstance(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.FindProcessInstance(orderId)
+ if processInstance != nil {
+ vars := map[string]interface{}{
+ "amount": amount,
+ }
+ bpmnEngine.PublishEventForInstance(processInstance.GetInstanceKey(), "payment-received", vars)
+ 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.
+ + + + + + +The lib-bpmn-engine supports working with multiple version of a BPMN process. +Typically, you want to do this, when you have long-running processes instances in-flight, +and you want to e.g. bug-fix or improve a BPMN process. +In such a scenario, you can't alter in-flight instances, but in parallel loading a newer v2 version, +and create new instances from this v2 process. +You could then decide to cancel older v1 process instances or simply wait for them to complete.
+Consider you have two hello world processes ...
+v1 \ +
+v2 \ +
+Both definitions have the same ID=hello-world-process-id
.
+The engine will load both and assign version=2
to the second process.
+Creating new instances is then possible by either knowing the process key (which is returned when loading the BPMN),
+or by knowing the ID. The example below uses the latter.
Hint: the handler will be called twice, since in v2, both service task have the same type.
+ + +package main
+
+import (
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
+)
+
+func main() {
+ // create a new named engine
+ bpmnEngine := bpmn_engine.New()
+ // basic example loading a v1 BPMN from file,
+ _, err := bpmnEngine.LoadFromFile("simple_task.bpmn")
+ if err != nil {
+ panic("file \"simple_task.bpmn\" can't be read. " + err.Error())
+ }
+ // now loading v2, basically with the same process ID
+ _, err = bpmnEngine.LoadFromFile("simple_task_v2.bpmn")
+ if err != nil {
+ panic("file \"simple_task.bpmn\" can't be read. " + err.Error())
+ }
+
+ // register a handler for a service task by defined task type
+ bpmnEngine.NewTaskHandler().Type("hello-world").Handler(printElementIdHandler)
+ // and execute the process, means we will use v2
+ bpmnEngine.CreateAndRunInstanceById("hello-world-process-id", nil)
+}
+
+func printElementIdHandler(job bpmn_engine.ActivatedJob) {
+ println(job.ElementId())
+ job.Complete() // don't forget this one, or job.Fail("foobar")
+}
+
To get the snippet compile, see the full sources in the +examples/timers/ folder.
+ + + + + + +The lib-bpmn-engine supports timer intermediate catch events, +which are very useful to model typical timeout scenarios.
+ +The one above "ask $1 million question" demonstrates a 10 seconds timeout +to give the correct answer or lose the whole "game". +This is a best-practice example, for how to model (business) timeouts or deadlines.
+In BPMN processes, Timer Intermediate Catch events can be used in combination with +Event Based Gateway, to exclusively select one execution path in the process. +When a timer event happens before a message event, then the example $1 million question game is lost.
+The Problem: implementing a timer/scheduler very much depends on your context or non-functional requirements. +E.g. you might run lib-bpmn-engine as part of a single batch job instance OR you have a web service +implement which is running with 3 instances. Both scenarios do require different implementation approaches, +how to deal with long-running processes.
+Choices: Depending on your scenario/use case, you might implement a trivial blocking loop, +like in the example code below. +In multi-instance environments, you might better use a central scheduler, to avoid each instance of the +application (using lib-bpmn-engine) is doing its own un-coordinated timing/scheduling.
+In a nutshell: +The lib-bpmn-engine does create such timer event objects and will pause the process execution. +This means, an external ticker/scheduler is required, to continue the process instance.
+The code snippet below demonstrates a trivial example, how to execute processes with timers. +Here, the execution is blocking until the due time is reached. +This might fit in a scenario, where you have a single instance running in a batch-job like environment.
+Depending on your context, you might choose some external ticker/scheduler, +to check for active scheduled timer events.
+ + +package main
+
+import (
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
+ "time"
+)
+
+func main() {
+ bpmnEngine := bpmn_engine.New()
+ process, err := bpmnEngine.LoadFromFile("timeout-example.bpmn")
+ if err != nil {
+ panic("file \"timeout-example.bpmn\" can't be read.")
+ }
+ // just some dummy handler to complete the tasks/jobs
+ registerDummyTaskHandlers(&bpmnEngine)
+
+ instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
+ println(instance.GetState()) // still ACTIVE at this point
+
+ printScheduledTimerInformation(bpmnEngine.GetTimersScheduled()[0])
+
+ // sleep() for 2 seconds, before trying to continue the process instance
+ // this for-loop essentially will block until the process instance has completed OR an error occurred
+ for ; instance.GetState() == bpmn_engine.Active && err == nil; time.Sleep(2 * time.Second) {
+ println("tick.")
+ // by re-running, the engine will check for active timers and might continue execution,
+ // if timer.DueAt has passed
+ _, err = bpmnEngine.RunOrContinueInstance(instance.GetInstanceKey())
+ }
+
+ println(instance.GetState()) // finally completed
+}
+
To get the snippet compile, see the full sources in the +examples/timers/ folder.
+ + + + + + +Since lib-bpmn-engine runs embedded in your application, +it's a challenge, to "see what's happen inside". Therefore, this library exports +all internal events and you can register various event exporters. +That might be a simple STDOUT exporter, or one that exports to a dedicated visualisation application.
+There's an Open Source Web UI for monitoring BPMN processes Zeebe Simple Monitor. +Fortunately, the authors did design the monitor as an event consumer. +Thus, by exporting Zeebe compatible events, you're able to monitor your processes.
+As shown in the architecture diagram, your App and Zeebe Simple Montor don't +connect to each other, but rather your app needs to connect to a Hazelcast Ringbuffer. +Once connected, the built-in Zeebe Exporter will export events to this Ringbuffer. +The Zeebe Simple Monitor (ZSM) is a Java based application, which connects to the Ringbuffer +as well and fetches events from there. Once fetched, ZSM stores events in it's own database. +The ZSM database can be configured to your preferences.
+Please, keep in mind, this architecture does not support any namespacing. +Means, when multiple of your applications connect to the same Ringbuffer, +all events will be mixed up. That's less an issue technically, but might confuse your users.
+ +As with the first and experimental release, just a handful of events are supported. +This means, in contract to a full flavoured Zeebe cluster, events are missing +and will not be shown. That said, basic functionality in Simple Monitor is given.
+Once you application restarts, workflows are newly deployed and so they are shown +multiple times in Zeebe Simple Monitor (ZSM). This is because of the design/architecture +of lib-bpmn-engine. With a new start of your app, a new ID will be assigned +and ZSM ha no chance to detect former workflows/processes are identical.
+Housekeeping is missing in ZSM, which results in events just stack up and will slow down your database.
+The ringbuffer support in Hazelcast's Go client is not yet officially merged. +So, lib-bpmn-engine uses a feature branch of the client to write to the ringbuffer.
+Here are some hints, how you can quickly spin up a setup for experimenting. +If you aim for a more production ready setup, please read&learn how to do such +with Hazelcast and Zeebe Simple Monitor on their official web sites individually.
+First, you need a running Hazelcast. The simplest way is using Docker... +
+Using Docker, also allows you to start a Zeebe Simple Monitor... +
docker run -p 8082:8082 -e "zeebe.client.worker.hazelcast.connection=$(hostname):5701" ghcr.io/camunda-community-hub/zeebe-simple-monitor:2.4.0```
+
⚠️ The above Docker containers do expose each service without any authentication! +This mean, anyone in your network can connect to it - please, use a proper firewall +or other tools to secure your system.
+package main
+
+import (
+ "context"
+ "fmt"
+ "github.com/hazelcast/hazelcast-go-client"
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine"
+ "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine/exporter/zeebe"
+)
+
+func main() {
+ // create a new named engine
+ bpmnEngine := bpmn_engine.New()
+ // the exporter will require a running Hazelcast cluster at 127.0.0.1:5701
+ ctx := context.TODO()
+ config := hazelcast.Config{}
+ config.Cluster.Network.SetAddresses("localhost:5701")
+ client, err := hazelcast.StartNewClientWithConfig(ctx, config)
+ // create the client
+ exporter, _ := zeebe.NewExporterWithHazelcastClient(client)
+ // register the exporter
+ bpmnEngine.AddEventExporter(&exporter)
+ // 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.NewTaskHandler().Id("hello-world").Handler(printContextHandler)
+ // and execute the process
+ instance, _ := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil)
+
+ println(fmt.Sprintf("instanceKey=%d", instance.GetInstanceKey()))
+}
+
+func printContextHandler(job bpmn_engine.ActivatedJob) {
+ // trivial handler is requires
+ job.Complete()
+}
+
{"use strict";/*!
+ * escape-html
+ * Copyright(c) 2012-2013 TJ Holowaychuk
+ * Copyright(c) 2015 Andreas Lubbe
+ * Copyright(c) 2015 Tiancheng "Timothy" Gu
+ * MIT Licensed
+ */var $a=/["'&<>]/;Un.exports=Ra;function Ra(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,s=0;for(i=r.index;i