Skip to content

Commit

Permalink
Merge pull request #197 from camunda-community-hub/enhanced-deploymen…
Browse files Browse the repository at this point in the history
…t-response

fix: Enhanced deployment response
  • Loading branch information
saig0 authored Apr 25, 2023
2 parents e3e7de6 + 0251089 commit 8f91030
Show file tree
Hide file tree
Showing 5 changed files with 439 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,94 @@
package org.camunda.community.zeebe.play.rest

import io.camunda.zeebe.client.ZeebeClient
import io.camunda.zeebe.client.api.response.DeploymentEvent
import io.zeebe.zeeqs.data.entity.DecisionRequirements
import io.zeebe.zeeqs.data.entity.Process
import io.zeebe.zeeqs.data.repository.DecisionRequirementsRepository
import io.zeebe.zeeqs.data.repository.ProcessRepository
import org.camunda.community.zeebe.play.services.ZeebeClockService
import org.springframework.data.repository.findByIdOrNull
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.time.Duration
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit

private val RETRY_INTERVAL = Duration.ofMillis(100)

@RestController
@RequestMapping("/rest/deployments")
class DeploymentsResource(private val zeebeClient: ZeebeClient) {
class DeploymentsResource(
private val zeebeClient: ZeebeClient,
private val zeebeClockService: ZeebeClockService,
private val processRepository: ProcessRepository,
private val decisionRequirementsRepository: DecisionRequirementsRepository
) {

private val executor = Executors.newSingleThreadExecutor()

@RequestMapping(path = ["/"], method = [RequestMethod.POST])
fun deployResources(@RequestParam("resources") resources: Array<MultipartFile>): Long {
// keep this API for backward compatibility with Web Modeler
return deploy(resources).key;
}

@RequestMapping(path = ["/deploy"], method = [RequestMethod.POST])
fun deployResourcesWithMetadata(@RequestParam("resources") resources: Array<MultipartFile>): DeploymentResponse {
// Use the current time for the duplicate check. The isDuplicate property is not available.
val timeBeforeDeploy = zeebeClockService.getCurrentTime()
val deployment = deploy(resources)

// Wait until the resources are imported
val deployedProcesses = deployment.processes.associate {
it.processDefinitionKey to findProcessByKeyAsync(it.processDefinitionKey)
}
val deployedDecisionRequirements = deployment.decisions.associate {
it.decisionRequirementsKey to findDecisionRequirementsByKeyAsync(it.decisionRequirementsKey)
}

return DeploymentResponse(
deploymentKey = deployment.key,
deployedProcesses = deployment.processes.map { process ->
DeployedProcess(
processKey = process.processDefinitionKey,
bpmnProcessId = process.bpmnProcessId,
resourceName = process.resourceName,
isDuplicate = deployedProcesses[process.processDefinitionKey]
?.let {
it.get(
10,
TimeUnit.SECONDS
).deployTime < timeBeforeDeploy.toEpochMilli()
}
?: false
)
},
deployedDecisions = deployment.decisions.map { decision ->
val decisionRequirements =
deployedDecisionRequirements[decision.decisionRequirementsKey]?.get(
10,
TimeUnit.SECONDS
)

DeployedDecision(
decisionKey = decision.decisionKey,
decisionId = decision.dmnDecisionId,
resourceName = decisionRequirements?.resourceName ?: "?",
isDuplicate = decisionRequirements
?.let { it.deployTime < timeBeforeDeploy.toEpochMilli() }
?: false
)
}
)
}

private fun deploy(resources: Array<MultipartFile>): DeploymentEvent {
if (resources.isEmpty()) {
throw RuntimeException("no resources to deploy")
}
Expand All @@ -32,7 +107,56 @@ class DeploymentsResource(private val zeebeClient: ZeebeClient) {
return deployCommand
.send()
.join()
.key;
}

private fun findProcessByKeyAsync(processKey: Long): Future<Process> =
executor.submit(Callable {
findProcessByKey(processKey = processKey)
})

private fun findProcessByKey(processKey: Long): Process {
return processRepository
.findByIdOrNull(processKey)
?: run {
// wait and retry
Thread.sleep(RETRY_INTERVAL.toMillis())
findProcessByKey(processKey)
}
}

private fun findDecisionRequirementsByKeyAsync(decisionRequirementsKey: Long): Future<DecisionRequirements> =
executor.submit(Callable {
findDecisionRequirementsByKey(decisionRequirementsKey = decisionRequirementsKey)
})

private fun findDecisionRequirementsByKey(decisionRequirementsKey: Long): DecisionRequirements {
return decisionRequirementsRepository
.findByIdOrNull(decisionRequirementsKey)
?: run {
// wait and retry
Thread.sleep(RETRY_INTERVAL.toMillis())
findDecisionRequirementsByKey(decisionRequirementsKey)
}
}

data class DeploymentResponse(
val deploymentKey: Long,
val deployedProcesses: List<DeployedProcess>,
val deployedDecisions: List<DeployedDecision>
)

data class DeployedProcess(
val processKey: Long,
val bpmnProcessId: String,
val resourceName: String,
val isDuplicate: Boolean
)

data class DeployedDecision(
val decisionKey: Long,
val decisionId: String,
val resourceName: String,
val isDuplicate: Boolean
)

}
2 changes: 1 addition & 1 deletion src/main/resources/public/js/rest-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function sendTimeTravelRequestWithDateTime(dateTime) {
function deployResources(resources) {
return $.ajax({
type: "POST",
url: "/rest/deployments/",
url: "/rest/deployments/deploy",
data: new FormData(resources),
processData: false,
contentType: false,
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/public/js/view-deployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ function deploymentModal() {
let resources = $("#deploymentForm")[0];

deployResources(resources)
.done(function (deploymentKey) {
const toastId = "new-deployment-" + deploymentKey;
.done(function (deployment) {
const toastId = "new-deployment-" + deployment.deploymentKey;
const content = "New resources deployed";
showNotificationSuccess(toastId, content);

Expand Down
221 changes: 221 additions & 0 deletions src/test/kotlin/org/camunda/community/zeebe/play/DeploymentTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package org.camunda.community.zeebe.play

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.camunda.zeebe.model.bpmn.Bpmn
import io.zeebe.zeeqs.data.repository.DecisionRepository
import io.zeebe.zeeqs.data.repository.DecisionRequirementsRepository
import io.zeebe.zeeqs.data.repository.ProcessRepository
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.tuple
import org.awaitility.kotlin.await
import org.camunda.community.zeebe.play.rest.DeploymentsResource
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInfo
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.mock.web.MockMultipartFile
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class DeploymentTest(
@Autowired private val mvc: MockMvc,
@Autowired private val processRepository: ProcessRepository,
@Autowired private val decisionRepository: DecisionRepository,
@Autowired private val decisionRequirementsRepository: DecisionRequirementsRepository
) {

private val objectMapper = ObjectMapper().registerKotlinModule()

private val process = Bpmn.createExecutableProcess("process")
.startEvent()
.endEvent()
.done()

private val processAsBytes = Bpmn.convertToString(process).toByteArray()

private val decisionAsBytes = javaClass.getResourceAsStream("/rating.dmn")?.readAllBytes()!!

@BeforeEach
fun `clean database`() {
processRepository.deleteAll()
decisionRepository.deleteAll()
decisionRequirementsRepository.deleteAll()
}

@Test
fun `should deploy process without metadata (old API)`(testInfo: TestInfo) {
// given
val resourceName = "process_${testInfo.displayName}.bpmn"

// when
val response = mvc.perform(
multipart("/rest/deployments/")
.file(
MockMultipartFile(
"resources",
resourceName,
"application/bpmn",
processAsBytes
)
)
)
.andExpect(status().isOk())
.andReturn()
.response
.contentAsString;

// then
assertThat(response.toLong()).isPositive()

await.untilAsserted {
assertThat(processRepository.findAll())
.hasSize(1)
.extracting<String> { it.resourceName }
.contains(resourceName)
}
}

@Test
fun `should deploy process`(testInfo: TestInfo) {
// given
val resourceName = "process_${testInfo.displayName}.bpmn"

// when
val deploymentResponse = deployProcess(resourceName)

// then
assertThat(deploymentResponse.deploymentKey).isPositive()
assertThat(deploymentResponse.deployedProcesses)
.hasSize(1)
.extracting({ it.bpmnProcessId }, { it.resourceName }, { it.isDuplicate })
.contains(tuple("process", resourceName, false))
assertThat(deploymentResponse.deployedDecisions).isEmpty()

await.untilAsserted {
assertThat(processRepository.findAll())
.hasSize(1)
.extracting({ it.key }, { it.resourceName })
.contains(
tuple(
deploymentResponse.deployedProcesses[0].processKey,
resourceName
)
)
}
}

@Test
fun `should re-deploy process`(testInfo: TestInfo) {
// given
val resourceName = "process_${testInfo.displayName}.bpmn"

deployProcess(resourceName)

// when
val deploymentResponse = deployProcess(resourceName)

assertThat(deploymentResponse.deploymentKey).isPositive()
assertThat(deploymentResponse.deployedProcesses)
.hasSize(1)
.extracting({ it.bpmnProcessId }, { it.resourceName }, { it.isDuplicate })
.contains(tuple("process", resourceName, true))
assertThat(deploymentResponse.deployedDecisions).isEmpty()
}

@Test
fun `should deploy decision`(testInfo: TestInfo) {
// given
val resourceName = "rating_${testInfo.displayName}.dmn"

// when
val deploymentResponse = deployDecision(resourceName)

// then
assertThat(deploymentResponse.deploymentKey).isPositive()
assertThat(deploymentResponse.deployedDecisions)
.hasSize(2)
.extracting({ it.decisionId }, { it.resourceName }, { it.isDuplicate })
.contains(
tuple("decision_a", resourceName, false),
tuple("decision_b", resourceName, false)
)
assertThat(deploymentResponse.deployedProcesses).isEmpty()

await.untilAsserted {
assertThat(decisionRepository.findAll())
.hasSize(2)
.extracting({ it.key }, { it.decisionId })
.containsAll(
deploymentResponse.deployedDecisions.map {
tuple(
it.decisionKey,
it.decisionId
)
}
)
}
}

@Test
fun `should re-deploy decision`(testInfo: TestInfo) {
// given
val resourceName = "rating_${testInfo.displayName}.dmn"
deployDecision(resourceName)

// when
val deploymentResponse = deployDecision(resourceName)

// then
assertThat(deploymentResponse.deploymentKey).isPositive()
assertThat(deploymentResponse.deployedDecisions)
.hasSize(2)
.extracting({ it.decisionId }, { it.resourceName }, { it.isDuplicate })
.contains(
tuple("decision_a", resourceName, true),
tuple("decision_b", resourceName, true)
)
assertThat(deploymentResponse.deployedProcesses).isEmpty()
}

private fun deployProcess(resourceName: String): DeploymentsResource.DeploymentResponse {
return deploymentResource(
MockMultipartFile(
"resources",
resourceName,
"application/bpmn",
processAsBytes
)
)
}

private fun deployDecision(resourceName: String): DeploymentsResource.DeploymentResponse {
return deploymentResource(
MockMultipartFile(
"resources",
resourceName,
"application/dmn",
decisionAsBytes
)
)
}

private fun deploymentResource(multipartFile: MockMultipartFile): DeploymentsResource.DeploymentResponse {
val response = mvc.perform(
multipart("/rest/deployments/deploy/")
.file(multipartFile)
)
.andExpect(status().isOk())
.andReturn()
.response
.contentAsString

return objectMapper.readValue(response, DeploymentsResource.DeploymentResponse::class.java)
}

}
Loading

0 comments on commit 8f91030

Please sign in to comment.