Skip to content

Commit

Permalink
new example: extended connector runtime (#571)
Browse files Browse the repository at this point in the history
* wip

* adjusted details, created readme, added to build.yaml

* adjusted the function to use durations to align with the jobError function
  • Loading branch information
jonathanlukas authored Jan 22, 2025
1 parent eca7343 commit 0e27f4c
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions extended-connector-runtime/README.md
Original file line number Diff line number Diff line change
@@ -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.
174 changes: 174 additions & 0 deletions extended-connector-runtime/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.camunda</groupId>
<artifactId>extended-connector-runtime</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.connectors>8.6.5</version.connectors>
<version.camunda>8.5.9</version.camunda>
<version.spring-boot>3.4.0</version.spring-boot>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${version.spring-boot}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>zeebe-bom</artifactId>
<version>${version.camunda}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.camunda.connector</groupId>
<artifactId>spring-boot-starter-camunda-connectors</artifactId>
<version>${version.connectors}</version>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>zeebe-client-java</artifactId>
<version>${version.camunda}</version>
</dependency>
<dependency>
<groupId>io.camunda.connector</groupId>
<artifactId>connector-http-json</artifactId>
<version>${version.connectors}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>camunda-process-test-spring</artifactId>
<version>8.6.6</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.43.0</version>
<configuration>
<formats>
<format>
<includes>
<include>*.md</include>
<include>.gitignore</include>
</includes>
<trimTrailingWhitespace/>
<endWithNewline/>
<indent>
<spaces>true</spaces>
<spacesPerTab>2</spacesPerTab>
</indent>
</format>
</formats>
<java>
<googleJavaFormat/>
</java>
<pom/>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${version.spring-boot}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<!-- profile to auto format -->
<profile>
<id>autoFormat</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<executions>
<execution>
<id>spotless-format</id>
<goals>
<goal>apply</goal>
</goals>
<phase>process-sources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

<!-- profile to perform strict validation checks -->
<profile>
<id>checkFormat</id>
<build>
<plugins>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<executions>
<execution>
<id>spotless-check</id>
<goals>
<goal>check</goal>
</goals>
<phase>validate</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<JavaFunction> resolveFunction(String functionName) {
if (NextExecutionTimeslotFeelFunction.NAME.equals(functionName)) {
return Optional.of(
new JavaFunction(
NextExecutionTimeslotFeelFunction.PARAMS,
NextExecutionTimeslotFeelFunction.getInstance()));
}
return Optional.empty();
}

@Override
public Collection<String> getFunctionNames() {
return List.of(NextExecutionTimeslotFeelFunction.NAME);
}
}
Original file line number Diff line number Diff line change
@@ -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<List<Val>, Val> {
public static final List<String> 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<Val> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.camunda.consulting.example.CustomFeelFunctionProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
camunda:
connector:
polling:
enabled: false
webhook:
enabled: false
client:
mode: simple
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 0e27f4c

Please sign in to comment.