diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0b0315e0..10f45581 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -159,3 +159,15 @@ jobs: - name: Build with Maven run: mvn verify -PcheckFormat -B working-directory: event-processing + build-extended-connector-runtime: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + - name: Build with Maven + run: mvn verify -PcheckFormat -B + working-directory: extended-connector-runtime diff --git a/extended-connector-runtime/README.md b/extended-connector-runtime/README.md new file mode 100644 index 00000000..1d058097 --- /dev/null +++ b/extended-connector-runtime/README.md @@ -0,0 +1,37 @@ +# Extended Connector Runtime + +As the connector runtime lies outside the engine, it can be adjusted. + +This example shows how a custom feel function can be introduced. + +This feel function can then be used on a connector runtime side feel expressions (for example result expression and error expression on outbound connectors). + +## How this works + +As the connector runtime uses a standardized way to bootstrap the feel engine and this way includes using the SPI function provider, this can be used to extend the functions being used. + +The [SPI file](./src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider) points to the function provider that determines on how the function is loaded. + +The provider then resolves the function by its name. + +The function itself is instantiated and gets a static reference to a spring bean injected. This workaround is chosen as there is no way to access the SPI context from within spring. + +## What kind of example is implemented here + +This example adds a function: + +``` +nextExecutionBackoff(backoff: days-time-duration): days-time-duration +``` + +This function then uses the `schedulingService` bean to determine the next timestamp where the execution could happen according to a defined schedule. + +## How can I run it + +As the example comes as spring boot application containing the rest connector, you can configure the connection to camunda and start the application using + +```bash +mvn spring-boot:run +``` + +>Tip: Disable the default connectors coming with your Camunda 8 installation to test out this runtime. diff --git a/extended-connector-runtime/pom.xml b/extended-connector-runtime/pom.xml new file mode 100644 index 00000000..7c284b0a --- /dev/null +++ b/extended-connector-runtime/pom.xml @@ -0,0 +1,174 @@ + + + 4.0.0 + + io.camunda + extended-connector-runtime + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + 8.6.5 + 8.5.9 + 3.4.0 + + + + + + org.springframework.boot + spring-boot-dependencies + ${version.spring-boot} + pom + import + + + io.camunda + zeebe-bom + ${version.camunda} + + + + + + + io.camunda.connector + spring-boot-starter-camunda-connectors + ${version.connectors} + + + io.camunda + zeebe-client-java + ${version.camunda} + + + io.camunda.connector + connector-http-json + ${version.connectors} + + + org.springframework.boot + spring-boot-starter-test + test + + + io.camunda + camunda-process-test-spring + 8.6.6 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + -parameters + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + *.md + .gitignore + + + + + true + 2 + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + org.springframework.boot + spring-boot-maven-plugin + ${version.spring-boot} + + + + repackage + + + + + + + + + + autoFormat + + true + + + + + com.diffplug.spotless + spotless-maven-plugin + + + spotless-format + + apply + + process-sources + + + + + + + + + + checkFormat + + + + com.diffplug.spotless + spotless-maven-plugin + + + spotless-check + + check + + validate + + + + + + + + \ No newline at end of file diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/App.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/App.java new file mode 100644 index 00000000..42f21183 --- /dev/null +++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/App.java @@ -0,0 +1,18 @@ +package com.camunda.consulting.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +@SpringBootApplication +public class App { + public static ApplicationContext applicationContext; + + public static void main(String[] args) { + applicationContext = SpringApplication.run(App.class, args); + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } +} diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/CustomFeelFunctionProvider.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/CustomFeelFunctionProvider.java new file mode 100644 index 00000000..ae1d9521 --- /dev/null +++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/CustomFeelFunctionProvider.java @@ -0,0 +1,26 @@ +package com.camunda.consulting.example; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.camunda.feel.context.JavaFunction; +import org.camunda.feel.context.JavaFunctionProvider; + +public class CustomFeelFunctionProvider extends JavaFunctionProvider { + + @Override + public Optional resolveFunction(String functionName) { + if (NextExecutionTimeslotFeelFunction.NAME.equals(functionName)) { + return Optional.of( + new JavaFunction( + NextExecutionTimeslotFeelFunction.PARAMS, + NextExecutionTimeslotFeelFunction.getInstance())); + } + return Optional.empty(); + } + + @Override + public Collection getFunctionNames() { + return List.of(NextExecutionTimeslotFeelFunction.NAME); + } +} diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/NextExecutionTimeslotFeelFunction.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/NextExecutionTimeslotFeelFunction.java new file mode 100644 index 00000000..6308449d --- /dev/null +++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/NextExecutionTimeslotFeelFunction.java @@ -0,0 +1,53 @@ +package com.camunda.consulting.example; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.function.Function; +import org.camunda.feel.syntaxtree.Val; +import org.camunda.feel.syntaxtree.ValDayTimeDuration; +import org.springframework.stereotype.Component; + +@Component +public class NextExecutionTimeslotFeelFunction implements Function, Val> { + public static final List PARAMS = List.of("backoff"); + public static final String NAME = "nextExecutionBackoff"; + + /** + * This field is required to access a bean from outside the spring context as spi is used for the + * function provider + */ + private static NextExecutionTimeslotFeelFunction instance; + + private final SchedulingService schedulingService; + + public NextExecutionTimeslotFeelFunction(SchedulingService schedulingService) { + this.schedulingService = schedulingService; + instance = this; + } + + public static NextExecutionTimeslotFeelFunction getInstance() { + return instance; + } + + @Override + public Val apply(List vals) { + Val scheduledExecution = vals.get(0); + if (scheduledExecution instanceof ValDayTimeDuration duration) { + Duration value = duration.value(); + return new ValDayTimeDuration(calculateNextExecution(value)); + } else { + throw new IllegalStateException( + "Param 'scheduledExecution' expected to be of type 'date and time'"); + } + } + + private Duration calculateNextExecution(Duration duration) { + // we use the same "now" for both transformations to prevent inconsistencies + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime scheduledExecution = now.plus(duration); + return Duration.between(now, schedulingService.schedule(scheduledExecution)) + .truncatedTo(ChronoUnit.SECONDS); + } +} diff --git a/extended-connector-runtime/src/main/java/com/camunda/consulting/example/SchedulingService.java b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/SchedulingService.java new file mode 100644 index 00000000..9fb23d3f --- /dev/null +++ b/extended-connector-runtime/src/main/java/com/camunda/consulting/example/SchedulingService.java @@ -0,0 +1,12 @@ +package com.camunda.consulting.example; + +import java.time.ZonedDateTime; +import org.springframework.stereotype.Service; + +@Service +public class SchedulingService { + public ZonedDateTime schedule(ZonedDateTime scheduledExecution) { + // you can call your scheduling system here + return scheduledExecution.plusMinutes(10); + } +} diff --git a/extended-connector-runtime/src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider b/extended-connector-runtime/src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider new file mode 100644 index 00000000..bf174769 --- /dev/null +++ b/extended-connector-runtime/src/main/resources/META-INF/services/org.camunda.feel.context.CustomFunctionProvider @@ -0,0 +1 @@ +com.camunda.consulting.example.CustomFeelFunctionProvider \ No newline at end of file diff --git a/extended-connector-runtime/src/main/resources/application.yaml b/extended-connector-runtime/src/main/resources/application.yaml new file mode 100644 index 00000000..abba98c2 --- /dev/null +++ b/extended-connector-runtime/src/main/resources/application.yaml @@ -0,0 +1,8 @@ +camunda: + connector: + polling: + enabled: false + webhook: + enabled: false + client: + mode: simple \ No newline at end of file diff --git a/extended-connector-runtime/src/test/java/com/camunda/consulting/example/AppTest.java b/extended-connector-runtime/src/test/java/com/camunda/consulting/example/AppTest.java new file mode 100644 index 00000000..ec8e0715 --- /dev/null +++ b/extended-connector-runtime/src/test/java/com/camunda/consulting/example/AppTest.java @@ -0,0 +1,40 @@ +package com.camunda.consulting.example; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.camunda.process.test.api.CamundaSpringProcessTest; +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceResult; +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@CamundaSpringProcessTest +public class AppTest { + @Autowired ZeebeClient zeebeClient; + + @BeforeEach + public void setup() { + zeebeClient.newDeployResourceCommand().addResourceFromClasspath("test.bpmn").send().join(); + } + + @Test + void shouldExecute() { + Duration nextExecutionBackoff = Duration.ofMinutes(20); + ProcessInstanceResult result = + zeebeClient + .newCreateInstanceCommand() + .bpmnProcessId("test") + .latestVersion() + .withResult() + .send() + .join(); + assertThat(result).isNotNull(); + assertThat(Duration.parse((CharSequence) result.getVariable("nextExecutionBackoff"))) + .isEqualTo(nextExecutionBackoff); + } +} diff --git a/extended-connector-runtime/src/test/resources/test.bpmn b/extended-connector-runtime/src/test/resources/test.bpmn new file mode 100644 index 00000000..b1c3d1e0 --- /dev/null +++ b/extended-connector-runtime/src/test/resources/test.bpmn @@ -0,0 +1,53 @@ + + + + + Flow_1mof3ja + + + + + + + + + + + + + + + + + + Flow_1mof3ja + Flow_1rh30i0 + + + Flow_1rh30i0 + + + + + + + + + + + + + + + + + + + + + + + + + +