Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new example: extended connector runtime #571

Merged
merged 3 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading