0
votes

Imagine I have the following setup:

  1. A Spring Boot application.
  2. Inside it is a Camunda workflow engine.
  3. There are multiple components (@org.springframework.stereotype.Component) inside that application that are written in Clojure and are used by the Camunda workflow engine.

Setup: Spring Boot application with Camunda engine and multiple components inside it

I heard that allegedly it is possible to modify the code of a Clojure application without restarting it.

So, I want to

  1. modify the code of those components (without restarting the application),
  2. add new components to the application (without restarting the application), and
  3. after I'm done prototyping, download the current version of all components there.

The idea is that I prototype the components from the REPL until they work as designed. This means that the Camunda workflow engine will use the components modified by my actions on the REPL.

Then, I download the current version of the components in the application (so that they are not lost when the application is shut down). This code is then cleaned up, refactored, covered by unit tests and put under version control.

Question:

  1. Is it theoretically possible to implement such workflow with Clojure (not necessarily out of the box)?
  2. Are there any known limitations which would make such workflow absolutely impossible?

Update 1

Found following projects which prima facie allow you to interact with Java code using a REPL:

  1. spring-boot-bugger
  2. spring-repl

However, I don't know whether you can use them to change the code.

2

2 Answers

3
votes

This is a very rough and simple example of how to do this. I have dropped Camunda here because I don't think it's actually relevant.

The approach here:

  • Create Spring services, that delegate to actual Clojure code.
  • Make sure to load a "current good version" of that code into your process
  • Start a "server REPL" to allow overriding

The full project can be found here

Provide a service, that delegates to some Clojure code:

@Service
class ClojureBackedService {
    BigDecimal add(BigDecimal a, BigDecimal b) {
        Clojure.var('net.ofnir.repl', 'add').invoke(a, b)
    }
}

Start a REPL and load some "good initial setup":

@Service
class ClojureRepl {

    @PostConstruct
    def init() {
        def require = Clojure.var('clojure.core', 'require')
        require.invoke(Clojure.read('net.ofnir.repl'))
        Clojure.var('clojure.core.server', 'start-server').invoke(
                Clojure.read("{:port 5555 :name spring-repl :accept clojure.core.server/repl}")
        )
    }

    @PreDestroy
    def destroy() {
        Clojure.var('clojure.core.server', 'stop-server').invoke(
                Clojure.read("{:name spring-repl}")
        )
    }

}

For showing how this works, provide a web endpoint, that adds two numbers:

@RestController
class MathController {
    private final ClojureBackedService backend

    MathController(ClojureBackedService backend) {
        this.backend = backend
    }

    @PostMapping
    def add(BigDecimal a, BigDecimal b) {
        backend.add(a, b)
    }
}

Run the application and do a first test:

$ curl -da=5 -db=5 localhost:8080                                     
10

Looks good. Now connect to the REPL and replace the add function:

$ telnet localhost 5555
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
user=> (ns net.ofnir.repl)
nil
net.ofnir.repl=> (defn add [a b] (* a b))
#'net.ofnir.repl/add

No call the endpoint again:

$ curl -da=5 -db=5 localhost:8080
25

And there you have your spring service changed at runtime.

1
votes

If you requirement is to update the code which gets executed as part of e.g. a service task without restarting the process engine, then you may want to look into decoupling the process engine from this code completely. The external service task pattern allows you to do this: https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/

Instead of deploying the code into the Camunda runtime, you start an external worker in a separate runtime (e.g. JVM). Some benefits include:

  • process engine and worker are decoupled. Engine does not need to know where worker is located. the worker call the engine when it is available.
  • thread management is externalized to worker. Engine thread pool does not need to be scaled.
  • worker can be implemented in programming language of choice
  • worker can be upgraded independently (even different version of the worker could run at the same time, e.g. during rolling upgrades)
  • worker can be scaled independent of process engine

Also see for instance: