diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml
index c1a7c3a..5eab840 100644
--- a/.github/workflows/development.yml
+++ b/.github/workflows/development.yml
@@ -45,11 +45,6 @@ jobs:
- name: Build with Maven
run: ./mvnw clean verify -U -B -ntp -T4
- # itest
- - name: Run itest
- run: ./mvnw integration-test failsafe:verify -Pitest -ntp -U -B -T4
-
-
# - name: Upload coverage to Codecov
# if: github.event_name == 'push' && github.actor != 'dependabot[bot]'
# uses: codecov/codecov-action@v1.0.2
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 2a0059c..921e863 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -45,7 +45,7 @@ jobs:
# Publish release
- name: Deploy a new release version to Maven Central
- run: ./mvnw clean deploy -B -ntp -DskipTests -DskipExamples -Prelease -Dgpg.keyname="${{ secrets.GPG_KEYNAME }}"
+ run: ./mvnw clean deploy -B -ntp -DskipTests -DskipITests -DskipExamples -Prelease -Dgpg.keyname="${{ secrets.GPG_KEYNAME }}"
env:
OSS_CENTRAL_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
OSS_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
diff --git a/docs/idempotency.md b/docs/idempotency.md
new file mode 100644
index 0000000..6531677
--- /dev/null
+++ b/docs/idempotency.md
@@ -0,0 +1,98 @@
+---
+title: Idempotency Registry
+---
+
+The Idempotency Registry is a feature designed to prevent duplicate worker invocations. It ensures that if a task is processed multiple times (e.g., due to network issues or retries in the process engine), the worker logic is only executed once, and the previous result is returned for subsequent calls.
+
+## How it Works
+
+When a worker is triggered, the process engine worker:
+1. Checks the `IdempotencyRegistry` if a result already exists for the given `taskId`.
+2. If a result exists, it skips the worker execution and returns the stored result.
+3. If no result exists, it executes the worker.
+4. After successful execution, it registers the result in the `IdempotencyRegistry`.
+
+## Implementations
+
+There are three available implementations of the `IdempotencyRegistry`:
+
+| Implementation | Description | Recommended Use |
+| --- | --- | --- |
+| `NoOpIdempotencyRegistry` | Does nothing. No results are stored or retrieved. | Default, use if idempotency is handled elsewhere. |
+| `InMemoryIdempotencyRegistry` | Stores results in a local `ConcurrentHashMap`. | Testing or non-clustered environments. |
+| `JpaIdempotencyRegistry` | Stores results in a database using JPA. | Production, clustered environments. |
+
+## Setup Procedures
+
+### In-Memory Registry
+
+To use the in-memory registry, you need to provide a bean of type `IdempotencyRegistry` in your Spring configuration:
+
+```kotlin
+@Configuration
+class IdempotencyConfiguration {
+
+ @Bean
+ fun idempotencyRegistry(): IdempotencyRegistry = InMemoryIdempotencyRegistry()
+
+}
+```
+
+> **Warning:** The `InMemoryIdempotencyRegistry` is not suitable for clustered environments as the state is not shared between nodes.
+
+### JPA-based Registry
+
+The JPA-based registry is suitable for production environments. It persists the results in the database, allowing multiple instances of the worker to share the same idempotency state.
+
+#### 1. Add Dependency
+
+Add the following dependency to your `pom.xml`:
+
+```xml
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-spring-boot-idempotency-registry-jpa
+ ${process-engine-worker.version}
+
+```
+
+The `JpaIdempotencyAutoConfiguration` will automatically register the `JpaIdempotencyRegistry` if an `EntityManager` is present and no other `IdempotencyRegistry` bean is defined.
+
+#### 2. Database Schema (Liquibase)
+
+The JPA registry requires a table named `task_log_entry_`. You can use the following Liquibase changeSet to create it:
+
+```yaml
+databaseChangeLog:
+ - changeSet:
+ id: create-idempotency-table
+ author: bpm-crafters
+ changes:
+ - createTable:
+ tableName: task_log_entry_
+ columns:
+ - column:
+ name: task_id_
+ type: varchar(100)
+ constraints:
+ nullable: false
+ primaryKey: true
+ primaryKeyName: task_log_entry_pk_
+ - column:
+ name: process_instance_id_
+ type: varchar(100)
+ constraints:
+ nullable: false
+ - column:
+ name: created_at_
+ type: timestamp
+ constraints:
+ nullable: false
+ - column:
+ name: result_
+ type: blob # or bytea for PostgreSQL
+ constraints:
+ nullable: false
+```
+
+> **Note:** The `result_` column type should be suitable for storing binary data (e.g., `blob` for most databases, `bytea` for PostgreSQL).
diff --git a/docs/process-engine-worker.md b/docs/process-engine-worker.md
index 88b1db3..63d50e9 100644
--- a/docs/process-engine-worker.md
+++ b/docs/process-engine-worker.md
@@ -37,6 +37,15 @@ public class MySmartWorker {
}
```
+The `@ProcessEngineWorker` annotation supports the following properties:
+
+| Property | Type | Default | Description |
+|----------------|------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `topic` | `String` | `""` | Topic name to subscribe this worker for. Alias for `value`. |
+| `autoComplete` | `boolean` | `true` | Flag indicating if the task should be automatically completed after the worker execution. If the return type is `Map`, it will overrule this setting and auto-complete with the returned payload. |
+| `completion` | `enum` | `DEFAULT` | Configures when the worker completes a task if `autoComplete` is active. Possible values are `DEFAULT`, `BEFORE_COMMIT`, and `AFTER_COMMIT`. Has no effect if the worker is not transactional. |
+| `lockDuration` | `long` | `-1` | Optional lock duration in seconds for this worker. If set to `-1` (default), the global configuration of the process engine adapter will be used. (Available since `0.8.0`) |
+
## Method parameter resolution
Parameter resolution of the method annotated with `ProcessEngineWorker` is based on a set of strategies
diff --git a/itest/pom.xml b/itest/pom.xml
new file mode 100644
index 0000000..65ddb76
--- /dev/null
+++ b/itest/pom.xml
@@ -0,0 +1,43 @@
+
+ 4.0.0
+
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-root
+ 0.7.2-SNAPSHOT
+ ../pom.xml
+
+
+ process-engine-worker-itest
+ pom
+ ITest: Parent for the integration testing.
+
+
+
+ true
+ true
+ true
+
+
+
+ spring-boot-integration-testing
+ spring-boot-starter-integration-test
+
+
+
+
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-spring-boot-starter
+ ${project.version}
+
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-spring-boot-idempotency-registry-jpa
+ ${project.version}
+
+
+
+
+
diff --git a/itest/spring-boot-integration-testing/pom.xml b/itest/spring-boot-integration-testing/pom.xml
new file mode 100644
index 0000000..3f45a8b
--- /dev/null
+++ b/itest/spring-boot-integration-testing/pom.xml
@@ -0,0 +1,149 @@
+
+ 4.0.0
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-itest
+ 0.7.2-SNAPSHOT
+ ../pom.xml
+
+
+ process-engine-worker-integration-testing
+ ITest: Testing utilities for integration testing of the library.
+
+
+
+
+ org.testcontainers
+ testcontainers-bom
+ 2.0.3
+ import
+ pom
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ import
+ pom
+
+
+
+ org.mockito.kotlin
+ mockito-kotlin
+ ${mockito.version}
+
+
+
+
+
+
+ ${project.groupId}
+ process-engine-worker-spring-boot-starter
+ ${project.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jdk8
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+ dev.bpm-crafters.process-engine-adapters
+ process-engine-adapter-camunda-platform-c7-remote-spring-boot-starter
+ ${process-engine-adapters-c7.version}
+
+
+ io.holunda.c7
+ c7-rest-client-spring-boot-starter-feign
+ ${c7.version}
+
+
+ org.camunda.bpm.springboot
+ camunda-bpm-spring-boot-starter-external-task-client
+ ${camunda-bpm-spring-boot-starter-external-task-client.version}
+
+
+
+ io.micrometer
+ micrometer-core
+
+
+ org.assertj
+ assertj-core
+
+
+ org.awaitility
+ awaitility-kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+
+
+ org.junit.jupiter
+ junit-jupiter
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+
+
+ org.mockito.kotlin
+ mockito-kotlin
+
+
+ org.postgresql
+ postgresql
+
+
+ org.springframework
+ spring-beans
+
+
+ org.springframework
+ spring-context
+
+
+ org.springframework
+ spring-tx
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+ org.testcontainers
+ junit-jupiter
+
+
+ org.testcontainers
+ postgresql
+
+
+
+
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/AbstractTransactionalBehaviorTest.kt b/itest/spring-boot-integration-testing/src/main/kotlin/dev/bpmcrafters/processengine/worker/itest/AbstractBehaviorITestBase.kt
similarity index 54%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/AbstractTransactionalBehaviorTest.kt
rename to itest/spring-boot-integration-testing/src/main/kotlin/dev/bpmcrafters/processengine/worker/itest/AbstractBehaviorITestBase.kt
index 9fb81ca..26cd440 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/AbstractTransactionalBehaviorTest.kt
+++ b/itest/spring-boot-integration-testing/src/main/kotlin/dev/bpmcrafters/processengine/worker/itest/AbstractBehaviorITestBase.kt
@@ -1,42 +1,30 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external
-
-import dev.bpmcrafters.processengine.worker.TestHelper
-import dev.bpmcrafters.processengine.worker.TestHelper.Camunda7RunTestContainer
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.MyEntity
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.MyEntityService
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.TestApplication
-import dev.bpmcrafters.processengineapi.deploy.DeployBundleCommand
+package dev.bpmcrafters.processengine.worker.itest
+
import dev.bpmcrafters.processengineapi.deploy.DeploymentApi
-import dev.bpmcrafters.processengineapi.deploy.NamedResource
import dev.bpmcrafters.processengineapi.process.StartProcessApi
-import dev.bpmcrafters.processengineapi.process.StartProcessByDefinitionCmd
-import org.assertj.core.api.Assertions.assertThat
+import org.camunda.bpm.client.ExternalTaskClient
import org.camunda.community.rest.client.api.ExternalTaskApiClient
import org.camunda.community.rest.client.api.HistoryApiClient
+import org.camunda.community.rest.client.api.IncidentApiClient
import org.camunda.community.rest.client.api.ProcessInstanceApiClient
-import org.camunda.community.rest.client.model.ExternalTaskDto
-import org.camunda.community.rest.client.model.ExternalTaskQueryDto
-import org.camunda.community.rest.client.model.HistoricActivityInstanceDto
-import org.camunda.community.rest.client.model.HistoricActivityInstanceQueryDto
+import org.camunda.community.rest.client.model.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
-@SpringBootTest(classes = [TestApplication::class])
@Testcontainers
@ActiveProfiles("itest")
-abstract class AbstractTransactionalBehaviorTest {
+abstract class AbstractBehaviorITestBase {
companion object {
@Container
@JvmStatic
- val camundaContainer = Camunda7RunTestContainer("run-7.24.0")
+ val camundaContainer = TestHelper.Camunda7RunTestContainer("run-7.24.0")
@Container
@JvmStatic
@@ -53,31 +41,31 @@ abstract class AbstractTransactionalBehaviorTest {
registry.add("feign.client.config.default.url") { "http://localhost:${camundaContainer.firstMappedPort}/engine-rest/" }
}
}
+
@Autowired
- private lateinit var deploymentApi: DeploymentApi
+ protected lateinit var deploymentApi: DeploymentApi
+
@Autowired
- private lateinit var startProcessApi: StartProcessApi
+ protected lateinit var startProcessApi: StartProcessApi
+
@Autowired
- private lateinit var externalTaskApiClient: ExternalTaskApiClient
+ protected lateinit var externalTaskApiClient: ExternalTaskApiClient
+
@Autowired
- private lateinit var historyApiClient: HistoryApiClient
+ protected lateinit var incidentApiClient: IncidentApiClient
+
@Autowired
- private lateinit var processInstanceApiClient: ProcessInstanceApiClient
+ protected lateinit var historyApiClient: HistoryApiClient
+
@Autowired
- private lateinit var myEntityService: MyEntityService
+ protected lateinit var processInstanceApiClient: ProcessInstanceApiClient
+
@Autowired
- private lateinit var camundaTaskClient: org.camunda.bpm.client.ExternalTaskClient
+ protected lateinit var camundaTaskClient: ExternalTaskClient
@BeforeEach
fun setUp() {
camundaTaskClient.start()
- deploymentApi.deploy(
- DeployBundleCommand(
- listOf(NamedResource.fromClasspath("bpmn/example-process.bpmn"))
- )
- ).get().let { deployment ->
- assertThat(deployment).isNotNull
- }
}
@AfterEach
@@ -96,7 +84,7 @@ abstract class AbstractTransactionalBehaviorTest {
true, // active
false, // suspended
false, // with incident
- null, null,null, null, // incident
+ null, null, null, null, // incident
null, null, null, // tenant
null, // activity id in
null, null, // root process instance
@@ -111,6 +99,34 @@ abstract class AbstractTransactionalBehaviorTest {
.body as List
}
+ protected fun unlockExternalTask(taskId: String) {
+ externalTaskApiClient.unlock(taskId)
+ }
+
+ protected fun getIncidents(processInstanceId: String) = incidentApiClient.getIncidents(
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ processInstanceId,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ).body as List
+
protected fun getHistoricActivityInstances(processInstanceId: String, activityId: String): List {
return historyApiClient.queryHistoricActivityInstances(
0,
@@ -121,32 +137,4 @@ abstract class AbstractTransactionalBehaviorTest {
})
.body as List
}
-
- protected fun startProcess(
- name: String,
- verified: Boolean,
- simulateRandomTechnicalError: Boolean = false,
- apiCallShouldFail: Boolean = false
- ): String {
- return startProcessApi.startProcess(
- StartProcessByDefinitionCmd(
- definitionKey = "example_process",
- mapOf(
- "name" to name,
- "verified" to verified,
- "simulateRandomTechnicalError" to simulateRandomTechnicalError,
- "apiCallShouldFail" to apiCallShouldFail
- )
- )
- ).get()
- .instanceId
- }
-
- protected fun entityExistsForName(name: String): Boolean {
- return findEntityByName(name) != null
- }
-
- protected fun findEntityByName(name: String): MyEntity? {
- return myEntityService.findByName(name)
- }
}
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/TestHelper.kt b/itest/spring-boot-integration-testing/src/main/kotlin/dev/bpmcrafters/processengine/worker/itest/TestHelper.kt
similarity index 79%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/TestHelper.kt
rename to itest/spring-boot-integration-testing/src/main/kotlin/dev/bpmcrafters/processengine/worker/itest/TestHelper.kt
index 8692809..3e3509b 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/TestHelper.kt
+++ b/itest/spring-boot-integration-testing/src/main/kotlin/dev/bpmcrafters/processengine/worker/itest/TestHelper.kt
@@ -1,15 +1,18 @@
-package dev.bpmcrafters.processengine.worker
+package dev.bpmcrafters.processengine.worker.itest
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.utility.DockerImageName
-
-
object TestHelper {
- class Camunda7RunTestContainer(tag: String) : GenericContainer("camunda/camunda-bpm-platform:$tag") {
+ /**
+ * Camunda 7 Run test container.
+ */
+ class Camunda7RunTestContainer(tag: String) : GenericContainer(
+ "camunda/camunda-bpm-platform:$tag"
+ ) {
init {
withCommand("./camunda.sh", "--rest")
@@ -23,6 +26,9 @@ object TestHelper {
}
}
+ /**
+ * PostgreSQL container.
+ */
fun postgresContainer() = PostgreSQLContainer(DockerImageName.parse("postgres:16-alpine"))
.withDatabaseName("integration-test")
.withUsername("integration-user")
diff --git a/itest/spring-boot-starter-integration-test/pom.xml b/itest/spring-boot-starter-integration-test/pom.xml
new file mode 100644
index 0000000..75ad873
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/pom.xml
@@ -0,0 +1,60 @@
+
+ 4.0.0
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-itest
+ 0.7.2-SNAPSHOT
+ ../pom.xml
+
+
+ process-engine-worker-spring-boot-starter-integration-test
+ ITest: Integration tests of process-engine-worker
+
+
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-spring-boot-idempotency-registry-jpa
+
+
+
+ ${project.groupId}
+ process-engine-worker-integration-testing
+ ${project.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ *ITest.class
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+ *ITest.class
+
+
+
+
+ integration-tests
+ integration-test
+
+ integration-test
+
+
+
+
+
+
+
+
diff --git a/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/InMemoryIdempotencyRegistryConfiguration.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/InMemoryIdempotencyRegistryConfiguration.kt
new file mode 100644
index 0000000..ed7f5c2
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/InMemoryIdempotencyRegistryConfiguration.kt
@@ -0,0 +1,12 @@
+package dev.bpmcrafters.processengine.worker.fixture
+
+import dev.bpmcrafters.processengine.worker.idempotency.IdempotencyRegistry
+import dev.bpmcrafters.processengine.worker.idempotency.InMemoryIdempotencyRegistry
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Primary
+
+class InMemoryIdempotencyRegistryConfiguration {
+ @Bean
+ @Primary
+ fun inMemIdempotencyRegistry(): IdempotencyRegistry = InMemoryIdempotencyRegistry()
+}
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/TestApplication.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/TestApplication.kt
similarity index 66%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/TestApplication.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/TestApplication.kt
index edadbeb..65731c6 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/TestApplication.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/TestApplication.kt
@@ -1,18 +1,22 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external.application
+package dev.bpmcrafters.processengine.worker.fixture
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.context.annotation.Bean
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import java.text.SimpleDateFormat
@SpringBootApplication
+@EntityScan(basePackageClasses = [MyEntity::class])
+@EnableJpaRepositories(basePackageClasses = [MyEntityRepository::class])
class TestApplication {
@Bean
- fun objectMapper(): ObjectMapper = jacksonObjectMapper().apply{
+ fun objectMapper(): ObjectMapper = jacksonObjectMapper().apply {
registerModule(Jdk8Module())
registerModule(JavaTimeModule())
dateFormat = SimpleDateFormat("yyyy-MM-dd'T'hh:MM:ss.SSSz")
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/exceptions.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/exceptions.kt
similarity index 70%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/exceptions.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/exceptions.kt
index 38aa3b6..0cdfaf1 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/exceptions.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/exceptions.kt
@@ -1,4 +1,4 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external.application
+package dev.bpmcrafters.processengine.worker.fixture
import dev.bpmcrafters.processengine.worker.BpmnErrorOccurred
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/persistence.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/persistence.kt
similarity index 93%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/persistence.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/persistence.kt
index 430392f..37ec55b 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/persistence.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/persistence.kt
@@ -1,4 +1,4 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external.application
+package dev.bpmcrafters.processengine.worker.fixture
import jakarta.persistence.Column
import jakarta.persistence.Entity
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/services.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/services.kt
similarity index 77%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/services.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/services.kt
index a13736a..11d0157 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/services.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/services.kt
@@ -1,4 +1,4 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external.application
+package dev.bpmcrafters.processengine.worker.fixture
import org.springframework.stereotype.Service
@@ -24,7 +24,6 @@ class MyEntityService(private val repository: MyEntityRepository) {
return entity
}
- fun findByName(name: String): MyEntity? {
- return repository.findByName(name)
- }
+ fun findByName(name: String): MyEntity? = repository.findByName(name)
+
}
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/AbstractExampleProcessWorker.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/AbstractExampleProcessWorker.kt
similarity index 92%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/AbstractExampleProcessWorker.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/AbstractExampleProcessWorker.kt
index 0e2b9a5..149dceb 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/application/AbstractExampleProcessWorker.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/AbstractExampleProcessWorker.kt
@@ -1,5 +1,6 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external.application
+package dev.bpmcrafters.processengine.worker.fixture.worker
+import dev.bpmcrafters.processengine.worker.fixture.MyEntityService
import dev.bpmcrafters.processengineapi.CommonRestrictions
import dev.bpmcrafters.processengineapi.task.TaskInformation
import io.github.oshai.kotlinlogging.KotlinLogging
diff --git a/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithFailJobException.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithFailJobException.kt
new file mode 100644
index 0000000..a76c28a
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithFailJobException.kt
@@ -0,0 +1,41 @@
+package dev.bpmcrafters.processengine.worker.fixture.worker
+
+import dev.bpmcrafters.processengine.worker.FailJobException
+import dev.bpmcrafters.processengine.worker.ProcessEngineWorker
+import dev.bpmcrafters.processengine.worker.Variable
+import dev.bpmcrafters.processengine.worker.fixture.MyEntityService
+import dev.bpmcrafters.processengineapi.task.TaskInformation
+import org.camunda.community.rest.client.api.ProcessInstanceApiClient
+import java.time.Duration
+
+class WorkerWithFailJobException(
+ myEntityService: MyEntityService,
+ processInstanceApiClient: ProcessInstanceApiClient,
+) : AbstractExampleProcessWorker(
+ myEntityService = myEntityService,
+ processInstanceApiClient = processInstanceApiClient
+) {
+ @ProcessEngineWorker(
+ topic = "example.create-entity"
+ )
+ override fun createEntity(
+ task: TaskInformation,
+ @Variable(name = "name") name: String,
+ @Variable(name = "verified") verified: Boolean,
+ @Variable(name = "simulateRandomTechnicalError") simulateRandomTechnicalError: Boolean,
+ @Variable(name = "apiCallShouldFail") apiCallShouldFail: Boolean
+ ): Map {
+ if (simulateRandomTechnicalError) {
+ val message = "Simulating a technical error for task ${task.taskId}"
+ throw FailJobException(message = message, retryCount = 3, retryBackoff = Duration.ofSeconds(10))
+ }
+ return super.createEntity(task, name, verified, false, apiCallShouldFail)
+ }
+
+ @ProcessEngineWorker(
+ topic = "example.verify-entity",
+ )
+ override fun verifyEntity(@Variable(name = "id") id: String) {
+ super.verifyEntity(id)
+ }
+}
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/WorkerWithTransactionalAnnotation.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithTransactionalAnnotation.kt
similarity index 57%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/WorkerWithTransactionalAnnotation.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithTransactionalAnnotation.kt
index 8a00553..fdb41ac 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/WorkerWithTransactionalAnnotation.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithTransactionalAnnotation.kt
@@ -1,16 +1,15 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external
+package dev.bpmcrafters.processengine.worker.fixture.worker
import dev.bpmcrafters.processengine.worker.ProcessEngineWorker
import dev.bpmcrafters.processengine.worker.Variable
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.AbstractExampleProcessWorker
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.MyEntityService
+import dev.bpmcrafters.processengine.worker.fixture.MyEntityService
import dev.bpmcrafters.processengineapi.task.TaskInformation
import org.camunda.community.rest.client.api.ProcessInstanceApiClient
import org.springframework.transaction.annotation.Transactional
open class WorkerWithTransactionalAnnotation(
- myEntityService: MyEntityService,
- processInstanceApiClient: ProcessInstanceApiClient,
+ myEntityService: MyEntityService,
+ processInstanceApiClient: ProcessInstanceApiClient,
) : AbstractExampleProcessWorker(myEntityService = myEntityService, processInstanceApiClient = processInstanceApiClient) {
@Transactional
@@ -18,11 +17,11 @@ open class WorkerWithTransactionalAnnotation(
topic = "example.create-entity"
)
override fun createEntity(
- task: TaskInformation,
- @Variable(name = "name") name: String,
- @Variable(name = "verified") verified: Boolean,
- @Variable(name = "simulateRandomTechnicalError") simulateRandomTechnicalError: Boolean,
- @Variable(name = "apiCallShouldFail") apiCallShouldFail: Boolean
+ task: TaskInformation,
+ @Variable(name = "name") name: String,
+ @Variable(name = "verified") verified: Boolean,
+ @Variable(name = "simulateRandomTechnicalError") simulateRandomTechnicalError: Boolean,
+ @Variable(name = "apiCallShouldFail") apiCallShouldFail: Boolean
): Map {
return super.createEntity(task, name, verified, simulateRandomTechnicalError, apiCallShouldFail)
}
diff --git a/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithoutTransactionalAnnotation.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithoutTransactionalAnnotation.kt
new file mode 100644
index 0000000..5457612
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/fixture/worker/WorkerWithoutTransactionalAnnotation.kt
@@ -0,0 +1,36 @@
+package dev.bpmcrafters.processengine.worker.fixture.worker
+
+import dev.bpmcrafters.processengine.worker.ProcessEngineWorker
+import dev.bpmcrafters.processengine.worker.Variable
+import dev.bpmcrafters.processengine.worker.fixture.MyEntityService
+import dev.bpmcrafters.processengineapi.task.TaskInformation
+import org.camunda.community.rest.client.api.ProcessInstanceApiClient
+
+class WorkerWithoutTransactionalAnnotation(
+ myEntityService: MyEntityService,
+ processInstanceApiClient: ProcessInstanceApiClient,
+) : AbstractExampleProcessWorker(
+ myEntityService = myEntityService,
+ processInstanceApiClient = processInstanceApiClient
+) {
+
+ @ProcessEngineWorker(
+ topic = "example.create-entity"
+ )
+ override fun createEntity(
+ task: TaskInformation,
+ @Variable(name = "name") name: String,
+ @Variable(name = "verified") verified: Boolean,
+ @Variable(name = "simulateRandomTechnicalError") simulateRandomTechnicalError: Boolean,
+ @Variable(name = "apiCallShouldFail") apiCallShouldFail: Boolean
+ ): Map {
+ return super.createEntity(task, name, verified, simulateRandomTechnicalError, apiCallShouldFail)
+ }
+
+ @ProcessEngineWorker(
+ topic = "example.verify-entity",
+ )
+ override fun verifyEntity(@Variable(name = "id") id: String) {
+ super.verifyEntity(id)
+ }
+}
diff --git a/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/FixtureITestBase.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/FixtureITestBase.kt
new file mode 100644
index 0000000..755966f
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/FixtureITestBase.kt
@@ -0,0 +1,62 @@
+package dev.bpmcrafters.processengine.worker.itest
+
+import dev.bpmcrafters.processengine.worker.fixture.MyEntity
+import dev.bpmcrafters.processengine.worker.fixture.MyEntityService
+import dev.bpmcrafters.processengine.worker.fixture.TestApplication
+import dev.bpmcrafters.processengineapi.deploy.DeployBundleCommand
+import dev.bpmcrafters.processengineapi.deploy.NamedResource
+import dev.bpmcrafters.processengineapi.process.StartProcessByDefinitionCmd
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.annotation.DirtiesContext
+
+@SpringBootTest(classes = [TestApplication::class])
+@DirtiesContext
+abstract class FixtureITestBase : AbstractBehaviorITestBase() {
+
+ @Autowired
+ private lateinit var myEntityService: MyEntityService
+
+ fun entityExistsForName(name: String): Boolean {
+ return findEntityByName(name) != null
+ }
+
+ fun findEntityByName(name: String): MyEntity? {
+ return myEntityService.findByName(name)
+ }
+
+ @BeforeEach
+ fun deploy() {
+ deploymentApi.deploy(
+ DeployBundleCommand(
+ listOf(NamedResource.Companion.fromClasspath("bpmn/example-process.bpmn"))
+ )
+ ).get().let { deployment ->
+ Assertions.assertThat(deployment).isNotNull
+ }
+ }
+
+ protected fun startProcess(
+ name: String,
+ verified: Boolean,
+ simulateRandomTechnicalError: Boolean = false,
+ apiCallShouldFail: Boolean = false
+ ): String {
+ return startProcessApi.startProcess(
+ StartProcessByDefinitionCmd(
+ definitionKey = "example_process",
+ mapOf(
+ "name" to name,
+ "verified" to verified,
+ "simulateRandomTechnicalError" to simulateRandomTechnicalError,
+ "apiCallShouldFail" to apiCallShouldFail
+ )
+ )
+ ).get()
+ .instanceId
+ }
+
+
+}
diff --git a/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/idempotency/IdempotencyITest.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/idempotency/IdempotencyITest.kt
new file mode 100644
index 0000000..b247b40
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/idempotency/IdempotencyITest.kt
@@ -0,0 +1,84 @@
+package dev.bpmcrafters.processengine.worker.itest.idempotency
+
+import dev.bpmcrafters.processengine.worker.fixture.InMemoryIdempotencyRegistryConfiguration
+import dev.bpmcrafters.processengine.worker.fixture.TestApplication
+import dev.bpmcrafters.processengine.worker.fixture.worker.WorkerWithTransactionalAnnotation
+import dev.bpmcrafters.processengine.worker.fixture.worker.WorkerWithoutTransactionalAnnotation
+import dev.bpmcrafters.processengine.worker.idempotency.IdempotencyRegistry
+import dev.bpmcrafters.processengine.worker.itest.FixtureITestBase
+import dev.bpmcrafters.processengineapi.task.ServiceTaskCompletionApi
+import org.assertj.core.api.Assertions
+import org.awaitility.Awaitility
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.*
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.context.annotation.Import
+import org.springframework.test.annotation.DirtiesContext
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+@TestPropertySource(
+ properties = [
+ "dev.bpm-crafters.process-api.worker.complete-tasks-before-commit=false"
+ ]
+)
+@MockitoSpyBean(types = [IdempotencyRegistry::class])
+@MockitoSpyBean(name = "c7remote-service-task-completion-api", types = [ServiceTaskCompletionApi::class])
+abstract class IdempotencyITest : FixtureITestBase() {
+
+ @Autowired
+ private lateinit var idempotencyRegistry: IdempotencyRegistry
+
+ @Autowired
+ private lateinit var serviceTaskCompletionApi: ServiceTaskCompletionApi
+
+ @Test
+ fun `fetching the same task does not execute business logic again`() {
+ val name = "Big or Lil' Someone ${UUID.randomUUID()}"
+ doThrow(IllegalStateException("Many things have gone wrong while completing a task"))
+ .`when`(serviceTaskCompletionApi)
+ .completeTask(any())
+ val processInstanceId = startProcess(name, verified = true)
+ Assertions.assertThat(processInstanceIsRunning(processInstanceId)).isTrue()
+ Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted {
+ val entity = findEntityByName(name)
+ Assertions.assertThat(entity).isNotNull
+ }
+ doCallRealMethod().`when`(serviceTaskCompletionApi).completeTask(any())
+ unlockExternalTask(getExternalTasks(processInstanceId).first().id!!)
+ print(idempotencyRegistry)
+ Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted {
+ Assertions.assertThat(processInstanceIsRunning(processInstanceId)).isFalse
+ }
+ val inOrder = inOrder(idempotencyRegistry)
+ inOrder.verify(idempotencyRegistry).getTaskResult(any())
+ inOrder.verify(idempotencyRegistry).register(any(), any())
+ inOrder.verify(idempotencyRegistry).getTaskResult(any())
+ inOrder.verify(idempotencyRegistry, never()).register(any(), any())
+ }
+
+
+ @Nested
+ @Import(
+ InMemoryIdempotencyRegistryConfiguration::class,
+ WorkerWithoutTransactionalAnnotation::class
+ )
+ class InMemoryIdempotencyWithoutTransactionITest : IdempotencyITest()
+
+ @Nested
+ @Import(
+ InMemoryIdempotencyRegistryConfiguration::class,
+ WorkerWithTransactionalAnnotation::class
+ )
+ class InMemoryIdempotencyWithTransactionITest : IdempotencyITest()
+
+ @Nested
+ @Import(WorkerWithTransactionalAnnotation::class)
+ class JpaIdempotencyWithTransactionITest : IdempotencyITest()
+
+}
+
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionAfterCommitTest.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionAfterCommitITest.kt
similarity index 72%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionAfterCommitTest.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionAfterCommitITest.kt
index c1ddcba..25c2f83 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionAfterCommitTest.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionAfterCommitITest.kt
@@ -1,21 +1,27 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external
+package dev.bpmcrafters.processengine.worker.itest.registrar
+import dev.bpmcrafters.processengine.worker.fixture.worker.WorkerWithTransactionalAnnotation
+import dev.bpmcrafters.processengine.worker.itest.FixtureITestBase
import org.assertj.core.api.Assertions.assertThat
import org.awaitility.Awaitility.await
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.Import
import org.springframework.test.context.TestPropertySource
import java.util.*
+import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS
+
@Import(WorkerWithTransactionalAnnotation::class)
-@TestPropertySource(properties = [
- "dev.bpm-crafters.process-api.worker.complete-tasks-before-commit=false"
-])
-class ExternalTaskCompletionAfterCommitTest : AbstractTransactionalBehaviorTest() {
+@TestPropertySource(
+ properties = [
+ "dev.bpm-crafters.process-api.worker.complete-tasks-before-commit=false"
+ ]
+)
+class ExternalTaskCompletionAfterCommitITest : FixtureITestBase() {
@Test
- fun `successful worker completes task after of transaction has been committed`() {
+ fun `successful worker completes task after transaction has been committed`() {
val name = "Big or Lil' Someone ${UUID.randomUUID()}"
val pi = startProcess(name = name, verified = true)
assertThat(processInstanceIsRunning(pi)).isTrue()
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionBeforeCommitTest.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionBeforeCommitITest.kt
similarity index 85%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionBeforeCommitTest.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionBeforeCommitITest.kt
index f9fcf39..e15af2e 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionBeforeCommitTest.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionBeforeCommitITest.kt
@@ -1,18 +1,23 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external
+package dev.bpmcrafters.processengine.worker.itest.registrar
+import dev.bpmcrafters.processengine.worker.fixture.worker.WorkerWithTransactionalAnnotation
+import dev.bpmcrafters.processengine.worker.itest.FixtureITestBase
import org.assertj.core.api.Assertions.assertThat
import org.awaitility.Awaitility.await
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.Import
import org.springframework.test.context.TestPropertySource
import java.util.*
+import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS
@Import(WorkerWithTransactionalAnnotation::class)
-@TestPropertySource(properties = [
- "dev.bpm-crafters.process-api.worker.complete-tasks-before-commit=true"
-])
-class ExternalTaskCompletionBeforeCommitTest : AbstractTransactionalBehaviorTest() {
+@TestPropertySource(
+ properties = [
+ "dev.bpm-crafters.process-api.worker.complete-tasks-before-commit=true"
+ ]
+)
+class ExternalTaskCompletionBeforeCommitITest : FixtureITestBase() {
@Test
fun `happy path create two verified valid entity`() {
diff --git a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionWithoutTransactionTest.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionWithoutTransactionITest.kt
similarity index 61%
rename from spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionWithoutTransactionTest.kt
rename to itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionWithoutTransactionITest.kt
index 3422c67..7f71f52 100644
--- a/spring-boot-starter/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/camunda7/external/ExternalTaskCompletionWithoutTransactionTest.kt
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskCompletionWithoutTransactionITest.kt
@@ -1,49 +1,17 @@
-package dev.bpmcrafters.processengine.worker.itest.camunda7.external
+package dev.bpmcrafters.processengine.worker.itest.registrar
-import dev.bpmcrafters.processengine.worker.ProcessEngineWorker
-import dev.bpmcrafters.processengine.worker.Variable
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.AbstractExampleProcessWorker
-import dev.bpmcrafters.processengine.worker.itest.camunda7.external.application.MyEntityService
-import dev.bpmcrafters.processengineapi.task.TaskInformation
+import dev.bpmcrafters.processengine.worker.fixture.worker.WorkerWithoutTransactionalAnnotation
+import dev.bpmcrafters.processengine.worker.itest.FixtureITestBase
import org.assertj.core.api.Assertions.assertThat
import org.awaitility.Awaitility.await
-import org.camunda.community.rest.client.api.ProcessInstanceApiClient
import org.junit.jupiter.api.Test
import org.springframework.context.annotation.Import
import java.util.*
+import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS
-@Import(ExternalTaskCompletionWithoutTransactionTest.WorkerWithoutTransactionalAnnotation::class)
-class ExternalTaskCompletionWithoutTransactionTest : AbstractTransactionalBehaviorTest() {
-
- class WorkerWithoutTransactionalAnnotation(
- myEntityService: MyEntityService,
- processInstanceApiClient: ProcessInstanceApiClient,
- ) : AbstractExampleProcessWorker(
- myEntityService = myEntityService,
- processInstanceApiClient = processInstanceApiClient
- ) {
-
- @ProcessEngineWorker(
- topic = "example.create-entity"
- )
- override fun createEntity(
- task: TaskInformation,
- @Variable(name = "name") name: String,
- @Variable(name = "verified") verified: Boolean,
- @Variable(name = "simulateRandomTechnicalError") simulateRandomTechnicalError: Boolean,
- @Variable(name = "apiCallShouldFail") apiCallShouldFail: Boolean
- ): Map {
- return super.createEntity(task, name, verified, simulateRandomTechnicalError, apiCallShouldFail)
- }
-
- @ProcessEngineWorker(
- topic = "example.verify-entity",
- )
- override fun verifyEntity(@Variable(name = "id") id: String) {
- super.verifyEntity(id)
- }
- }
+@Import(WorkerWithoutTransactionalAnnotation::class)
+class ExternalTaskCompletionWithoutTransactionITest : FixtureITestBase() {
@Test
fun `happy path create two verified valid entity`() {
diff --git a/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskFailJobExceptionITest.kt b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskFailJobExceptionITest.kt
new file mode 100644
index 0000000..d4bb300
--- /dev/null
+++ b/itest/spring-boot-starter-integration-test/src/test/kotlin/dev/bpmcrafters/processengine/worker/itest/registrar/ExternalTaskFailJobExceptionITest.kt
@@ -0,0 +1,36 @@
+package dev.bpmcrafters.processengine.worker.itest.registrar
+
+import dev.bpmcrafters.processengine.worker.fixture.worker.WorkerWithFailJobException
+import dev.bpmcrafters.processengine.worker.itest.FixtureITestBase
+import org.assertj.core.api.Assertions.assertThat
+import org.awaitility.Awaitility.await
+import org.junit.jupiter.api.Test
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.TestPropertySource
+import java.util.*
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeUnit.SECONDS
+import kotlin.time.Duration.Companion.seconds
+
+@Import(WorkerWithFailJobException::class)
+@TestPropertySource(
+ properties = [
+ "dev.bpm-crafters.process-api.worker.complete-tasks-before-commit=true"
+ ]
+)
+class ExternalTaskFailJobExceptionITest : FixtureITestBase() {
+
+ @Test
+ fun `fail job exception will fail job with specified retries`() {
+ val name = "Big or Lil' Someone ${UUID.randomUUID()}"
+ val pi = startProcess(name = name, verified = true, simulateRandomTechnicalError = true)
+ assertThat(processInstanceIsRunning(pi)).isTrue()
+ await().atMost(30, SECONDS).untilAsserted {
+ val task = getExternalTasks(pi)[0]
+ assertThat(task.errorMessage).isEqualTo("Simulating a technical error for task ${task.id}")
+ assertThat(task.retries!!).isEqualTo(3)
+ }
+ assertThat(entityExistsForName(name)).isFalse
+ }
+
+}
diff --git a/spring-boot-starter/src/test/resources/application-itest.yaml b/itest/spring-boot-starter-integration-test/src/test/resources/application-itest.yaml
similarity index 100%
rename from spring-boot-starter/src/test/resources/application-itest.yaml
rename to itest/spring-boot-starter-integration-test/src/test/resources/application-itest.yaml
diff --git a/spring-boot-starter/src/test/resources/bpmn/example-process.bpmn b/itest/spring-boot-starter-integration-test/src/test/resources/bpmn/example-process.bpmn
similarity index 100%
rename from spring-boot-starter/src/test/resources/bpmn/example-process.bpmn
rename to itest/spring-boot-starter-integration-test/src/test/resources/bpmn/example-process.bpmn
diff --git a/pom.xml b/pom.xml
index 3ab5d7b..e9df17a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,8 +30,8 @@
+ spring-boot-idempotency-registry-jpa
spring-boot-starter
-
@@ -103,6 +103,18 @@
examples
+
+
+
+
+ !skipITests
+
+
+ itest
+
+ itest
+
+
@@ -125,6 +137,7 @@
spring
no-arg
+ jpa
all-open
diff --git a/spring-boot-idempotency-registry-jpa/pom.xml b/spring-boot-idempotency-registry-jpa/pom.xml
new file mode 100644
index 0000000..1be0971
--- /dev/null
+++ b/spring-boot-idempotency-registry-jpa/pom.xml
@@ -0,0 +1,38 @@
+
+ 4.0.0
+
+ dev.bpm-crafters.process-engine-worker
+ process-engine-worker-root
+ 0.7.2-SNAPSHOT
+ ../pom.xml
+
+
+ process-engine-worker-spring-boot-idempotency-registry-jpa
+ ${project.artifactId}
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ import
+ pom
+
+
+
+
+
+
+ ${project.groupId}
+ process-engine-worker-spring-boot-starter
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
diff --git a/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/configuration/JpaIdempotencyAutoConfiguration.kt b/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/configuration/JpaIdempotencyAutoConfiguration.kt
new file mode 100644
index 0000000..9f2e410
--- /dev/null
+++ b/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/configuration/JpaIdempotencyAutoConfiguration.kt
@@ -0,0 +1,27 @@
+package dev.bpmcrafters.processengine.worker.configuration
+
+import dev.bpmcrafters.processengine.worker.idempotency.IdempotencyRegistry
+import dev.bpmcrafters.processengine.worker.idempotency.JpaIdempotencyRegistry
+import dev.bpmcrafters.processengine.worker.idempotency.TaskLogEntry
+import dev.bpmcrafters.processengine.worker.idempotency.TaskLogEntryRepository
+import org.springframework.boot.autoconfigure.AutoConfiguration
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.context.annotation.Bean
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+
+/**
+ * Autoconfiguration for JPA-based Idempotency Registry.
+ * @since 0.8.0
+ */
+@AutoConfiguration(before = [ProcessEngineWorkerAutoConfiguration::class])
+@ConditionalOnMissingBean(IdempotencyRegistry::class)
+@EntityScan(basePackageClasses = [TaskLogEntry::class])
+@EnableJpaRepositories(basePackageClasses = [TaskLogEntryRepository::class])
+class JpaIdempotencyAutoConfiguration {
+
+ @Bean
+ fun jpaIdempotencyRegistry(taskLogEntryRepository: TaskLogEntryRepository): IdempotencyRegistry =
+ JpaIdempotencyRegistry(taskLogEntryRepository)
+
+}
diff --git a/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/idempotency/JpaIdempotencyRegistry.kt b/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/idempotency/JpaIdempotencyRegistry.kt
new file mode 100644
index 0000000..ee0444a
--- /dev/null
+++ b/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/idempotency/JpaIdempotencyRegistry.kt
@@ -0,0 +1,32 @@
+package dev.bpmcrafters.processengine.worker.idempotency
+
+import dev.bpmcrafters.processengineapi.CommonRestrictions
+import dev.bpmcrafters.processengineapi.task.TaskInformation
+import org.springframework.data.repository.findByIdOrNull
+import java.time.Clock
+
+/**
+ * JPA Idempotency Registry implementation.
+ * @since 0.8.0
+ */
+class JpaIdempotencyRegistry(
+ val taskLogEntryRepository: TaskLogEntryRepository,
+ val clock: Clock = Clock.systemUTC()
+) : IdempotencyRegistry {
+
+ override fun register(taskInformation: TaskInformation, result: Map) {
+ taskLogEntryRepository.save(
+ TaskLogEntry(
+ taskInformation.taskId,
+ taskInformation.meta[CommonRestrictions.PROCESS_INSTANCE_ID] as String,
+ clock.instant(),
+ result
+ )
+ )
+ }
+
+ override fun getTaskResult(taskInformation: TaskInformation): Map? = taskLogEntryRepository
+ .findByIdOrNull(taskInformation.taskId)
+ ?.result
+
+}
diff --git a/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/idempotency/MapConverter.kt b/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/idempotency/MapConverter.kt
new file mode 100644
index 0000000..8a64775
--- /dev/null
+++ b/spring-boot-idempotency-registry-jpa/src/main/kotlin/dev/bpmcrafters/processengine/worker/idempotency/MapConverter.kt
@@ -0,0 +1,30 @@
+package dev.bpmcrafters.processengine.worker.idempotency
+
+import jakarta.persistence.AttributeConverter
+import jakarta.persistence.Converter
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+
+@Converter(autoApply = false)
+internal class MapConverter : AttributeConverter