From 965b0899acd260d3b90a3fe3016ba01015cff64d Mon Sep 17 00:00:00 2001 From: Martin Kaeser Date: Wed, 11 Oct 2023 17:33:54 +0200 Subject: [PATCH 1/3] feat(orchestrator): Service logic with state machine --- .../orchestrator/api/GoldenRecordTaskApi.kt | 2 +- .../orchestrator/api/model/StepState.kt | 6 +- .../api/model/TaskProcessingStateDto.kt | 5 +- .../config/ApiConfigProperties.kt | 2 +- .../config/TaskConfigProperties.kt | 31 +++++ .../controller/GoldenRecordTaskController.kt | 31 ++--- .../exception/BpdmDuplicateTaskIdException.kt | 29 ++++ .../exception/BpdmIllegalStateException.kt | 31 +++++ .../exception/BpdmTaskNotFoundException.kt | 29 ++++ .../orchestrator/model/GoldenRecordTask.kt | 28 ++++ .../orchestrator/model/TaskProcessingState.kt | 37 +++++ .../service/GoldenRecordTaskService.kt | 127 ++++++++++++++++++ .../service/GoldenRecordTaskStateMachine.kt | 126 +++++++++++++++++ .../service/GoldenRecordTaskStorage.kt | 56 ++++++++ .../bpdm/orchestrator/util/DummyValues.kt | 24 ++-- .../src/main/resources/application.properties | 5 +- .../GoldenRecordTaskControllerIT.kt | 1 + 17 files changed, 534 insertions(+), 36 deletions(-) create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/TaskConfigProperties.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmDuplicateTaskIdException.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmIllegalStateException.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmTaskNotFoundException.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/GoldenRecordTask.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/TaskProcessingState.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachine.kt create mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt diff --git a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/GoldenRecordTaskApi.kt b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/GoldenRecordTaskApi.kt index b969c4dfe..4f579913f 100644 --- a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/GoldenRecordTaskApi.kt +++ b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/GoldenRecordTaskApi.kt @@ -63,7 +63,7 @@ interface GoldenRecordTaskApi { @Operation( summary = "Search for the state of golden record tasks by task identifiers", - description = "Returns the state of golden record tasks based on the provided task identifiers." + description = "Returns the state of golden record tasks based on the provided task identifiers. Unknown task identifiers are ignored." ) @ApiResponses( value = [ diff --git a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/StepState.kt b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/StepState.kt index eb8460b30..8108c6ff2 100644 --- a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/StepState.kt +++ b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/StepState.kt @@ -21,5 +21,7 @@ package org.eclipse.tractusx.orchestrator.api.model enum class StepState { Queued, - Reserved -} \ No newline at end of file + Reserved, + Success, + Error +} diff --git a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/TaskProcessingStateDto.kt b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/TaskProcessingStateDto.kt index 87e1929e0..e363b87a4 100644 --- a/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/TaskProcessingStateDto.kt +++ b/bpdm-orchestrator-api/src/main/kotlin/org/eclipse/tractusx/orchestrator/api/model/TaskProcessingStateDto.kt @@ -45,5 +45,8 @@ data class TaskProcessingStateDto( val createdAt: Instant, @get:Schema(description = "When the task has last been modified", required = true) - val modifiedAt: Instant + val modifiedAt: Instant, + + @get:Schema(description = "The timestamp until the task is removed from the Orchestrator") + val timeout: Instant ) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/ApiConfigProperties.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/ApiConfigProperties.kt index 0a0eb6469..4314d8e9a 100644 --- a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/ApiConfigProperties.kt +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/ApiConfigProperties.kt @@ -23,5 +23,5 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties(prefix = "bpdm.api") data class ApiConfigProperties( - val upsertLimit: Int = 100, + val upsertLimit: Int = 100 ) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/TaskConfigProperties.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/TaskConfigProperties.kt new file mode 100644 index 000000000..f78751607 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/config/TaskConfigProperties.kt @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import java.time.Duration + +@ConfigurationProperties(prefix = "bpdm.task") +data class TaskConfigProperties( + // Duration after which a task is removed from the Orchestrator after creation + val taskTimeout: Duration = Duration.ofHours(3 * 24), + // Duration for which a reservation is valid and results are accepted + val taskReservationTimeout: Duration = Duration.ofHours(24) +) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskController.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskController.kt index ee717f406..b4dc9b312 100644 --- a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskController.kt +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskController.kt @@ -21,52 +21,43 @@ package org.eclipse.tractusx.bpdm.orchestrator.controller import org.eclipse.tractusx.bpdm.common.exception.BpdmUpsertLimitException import org.eclipse.tractusx.bpdm.orchestrator.config.ApiConfigProperties -import org.eclipse.tractusx.bpdm.orchestrator.exception.BpdmEmptyResultException -import org.eclipse.tractusx.bpdm.orchestrator.util.DummyValues +import org.eclipse.tractusx.bpdm.orchestrator.service.GoldenRecordTaskService import org.eclipse.tractusx.orchestrator.api.GoldenRecordTaskApi import org.eclipse.tractusx.orchestrator.api.model.* +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController class GoldenRecordTaskController( - val apiConfigProperties: ApiConfigProperties + val apiConfigProperties: ApiConfigProperties, + val goldenRecordTaskService: GoldenRecordTaskService ) : GoldenRecordTaskApi { override fun createTasks(createRequest: TaskCreateRequest): TaskCreateResponse { if (createRequest.businessPartners.size > apiConfigProperties.upsertLimit) throw BpdmUpsertLimitException(createRequest.businessPartners.size, apiConfigProperties.upsertLimit) - //ToDo: Replace with service logic - return DummyValues.dummyResponseCreateTask + return goldenRecordTaskService.createTasks(createRequest) } override fun reserveTasksForStep(reservationRequest: TaskStepReservationRequest): TaskStepReservationResponse { - if (reservationRequest.amount > apiConfigProperties.upsertLimit) { + if (reservationRequest.amount > apiConfigProperties.upsertLimit) throw BpdmUpsertLimitException(reservationRequest.amount, apiConfigProperties.upsertLimit) - } - //ToDo: Replace with service logic - return when (reservationRequest.step) { - TaskStep.CleanAndSync -> DummyValues.dummyStepReservationResponse - TaskStep.PoolSync -> DummyValues.dummyPoolSyncResponse - TaskStep.Clean -> DummyValues.dummyStepReservationResponse - } + return goldenRecordTaskService.reserveTasksForStep(reservationRequest) } + @ResponseStatus(HttpStatus.NO_CONTENT) override fun resolveStepResults(resultRequest: TaskStepResultRequest) { if (resultRequest.results.size > apiConfigProperties.upsertLimit) throw BpdmUpsertLimitException(resultRequest.results.size, apiConfigProperties.upsertLimit) - resultRequest.results.forEach { resultEntry -> - if (resultEntry.businessPartner == null && resultEntry.errors.isEmpty()) - throw BpdmEmptyResultException(resultEntry.taskId) - } + goldenRecordTaskService.resolveStepResults(resultRequest) } - override fun searchTaskStates(stateRequest: TaskStateRequest): TaskStateResponse { - // ToDo: Replace with service logic - return DummyValues.dummyResponseTaskState + return goldenRecordTaskService.searchTaskStates(stateRequest) } } \ No newline at end of file diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmDuplicateTaskIdException.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmDuplicateTaskIdException.kt new file mode 100644 index 000000000..7f943ea43 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmDuplicateTaskIdException.kt @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.exception + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + + +@ResponseStatus(HttpStatus.BAD_REQUEST) +class BpdmDuplicateTaskIdException( + taskId: String +) : RuntimeException("Duplicate task ID '$taskId'.") \ No newline at end of file diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmIllegalStateException.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmIllegalStateException.kt new file mode 100644 index 000000000..b435eddc5 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmIllegalStateException.kt @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.exception + +import org.eclipse.tractusx.bpdm.orchestrator.model.TaskProcessingState +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + + +@ResponseStatus(HttpStatus.BAD_REQUEST) +class BpdmIllegalStateException( + taskId: String, + state: TaskProcessingState +) : RuntimeException("Task with ID '$taskId' is in illegal state for transition: resultState=${state.resultState}, step=${state.step}, stepState=${state.stepState}") \ No newline at end of file diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmTaskNotFoundException.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmTaskNotFoundException.kt new file mode 100644 index 000000000..d4556cc66 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/exception/BpdmTaskNotFoundException.kt @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.exception + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + + +@ResponseStatus(HttpStatus.BAD_REQUEST) +class BpdmTaskNotFoundException( + taskId: String +) : RuntimeException("Task with ID '$taskId' not found.") \ No newline at end of file diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/GoldenRecordTask.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/GoldenRecordTask.kt new file mode 100644 index 000000000..64cc38f26 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/GoldenRecordTask.kt @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.model + +import org.eclipse.tractusx.orchestrator.api.model.BusinessPartnerFullDto + +data class GoldenRecordTask( + val taskId: String, + var businessPartner: BusinessPartnerFullDto, + val processingState: TaskProcessingState +) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/TaskProcessingState.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/TaskProcessingState.kt new file mode 100644 index 000000000..8a858d565 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/model/TaskProcessingState.kt @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.model + +import org.eclipse.tractusx.orchestrator.api.model.* +import java.time.Instant + +data class TaskProcessingState( + val mode: TaskMode, + var resultState: ResultState, + var errors: List = emptyList(), + + var step: TaskStep, + var stepState: StepState, + var reservationTimeout: Instant?, + + val taskCreatedAt: Instant, + var taskModifiedAt: Instant, + val taskTimeout: Instant, +) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt new file mode 100644 index 000000000..f742efdb2 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.service + +import org.eclipse.tractusx.bpdm.orchestrator.exception.BpdmEmptyResultException +import org.eclipse.tractusx.bpdm.orchestrator.exception.BpdmTaskNotFoundException +import org.eclipse.tractusx.bpdm.orchestrator.model.GoldenRecordTask +import org.eclipse.tractusx.bpdm.orchestrator.model.TaskProcessingState +import org.eclipse.tractusx.orchestrator.api.model.* +import org.springframework.stereotype.Service +import java.time.Instant +import java.util.* + +@Service +class GoldenRecordTaskService( + val taskStorage: GoldenRecordTaskStorage, + val goldenRecordTaskStateMachine: GoldenRecordTaskStateMachine +) { + + fun createTasks(createRequest: TaskCreateRequest): TaskCreateResponse { + return createRequest.businessPartners + .map { businessPartnerGeneric -> taskStorage.addTask(initTask(createRequest, businessPartnerGeneric)) } + .map(::toTaskClientStateDto) + .let { TaskCreateResponse(createdTasks = it) } + } + + fun searchTaskStates(stateRequest: TaskStateRequest): TaskStateResponse { + return stateRequest.taskIds + .mapNotNull { taskId -> taskStorage.getTask(taskId) } // skip missing tasks + .map(::toTaskClientStateDto) + .let { TaskStateResponse(tasks = it) } + } + + fun reserveTasksForStep(reservationRequest: TaskStepReservationRequest): TaskStepReservationResponse { + val now = Instant.now() + + val tasks = taskStorage.getQueuedTasksByStep(reservationRequest.step, reservationRequest.amount) + tasks.forEach { task -> goldenRecordTaskStateMachine.doReserve(task) } + + val reservationTimeout = tasks + .mapNotNull { it.processingState.reservationTimeout } + .minOrNull() + ?: now + + val taskEntries = tasks.map { task -> + TaskStepReservationEntryDto( + taskId = task.taskId, + businessPartner = task.businessPartner + ) + } + + return TaskStepReservationResponse( + reservedTasks = taskEntries, + timeout = reservationTimeout + ) + } + + fun resolveStepResults(resultRequest: TaskStepResultRequest) { + resultRequest.results + .forEach { resultEntry -> + val task = taskStorage.getTask(resultEntry.taskId) + ?: throw BpdmTaskNotFoundException(resultEntry.taskId) + + val errors = resultEntry.errors + val resultBusinessPartner = resultEntry.businessPartner + if (errors.isNotEmpty()) { + goldenRecordTaskStateMachine.doResolveFailed(task, errors) + } else if (resultBusinessPartner != null) { + goldenRecordTaskStateMachine.doResolveSuccessful(task, resultBusinessPartner) + } else { + throw BpdmEmptyResultException(resultEntry.taskId) + } + } + } + + private fun initTask( + createRequest: TaskCreateRequest, + businessPartnerGeneric: BusinessPartnerGenericDto + ) = GoldenRecordTask( + taskId = UUID.randomUUID().toString(), + businessPartner = BusinessPartnerFullDto( + generic = businessPartnerGeneric + ), + processingState = goldenRecordTaskStateMachine.initProcessingState(createRequest.mode) + ) + + private fun toTaskClientStateDto(task: GoldenRecordTask): TaskClientStateDto { + val businessPartnerResult = when (task.processingState.resultState) { + ResultState.Success -> task.businessPartner.generic + else -> null + } + return TaskClientStateDto( + taskId = task.taskId, + processingState = toTaskProcessingStateDto(task.processingState), + businessPartnerResult = businessPartnerResult + ) + } + + private fun toTaskProcessingStateDto(processingState: TaskProcessingState): TaskProcessingStateDto { + return TaskProcessingStateDto( + resultState = processingState.resultState, + step = processingState.step, + stepState = processingState.stepState, + errors = processingState.errors, + createdAt = processingState.taskCreatedAt, + modifiedAt = processingState.taskModifiedAt, + timeout = processingState.taskTimeout + ) + } +} diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachine.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachine.kt new file mode 100644 index 000000000..5b7d2c0f3 --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachine.kt @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.service + +import org.eclipse.tractusx.bpdm.orchestrator.config.TaskConfigProperties +import org.eclipse.tractusx.bpdm.orchestrator.exception.BpdmIllegalStateException +import org.eclipse.tractusx.bpdm.orchestrator.model.GoldenRecordTask +import org.eclipse.tractusx.bpdm.orchestrator.model.TaskProcessingState +import org.eclipse.tractusx.orchestrator.api.model.* +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class GoldenRecordTaskStateMachine( + val taskConfigProperties: TaskConfigProperties +) { + + fun initProcessingState(mode: TaskMode): TaskProcessingState { + val now = Instant.now() + + val initialStep = getInitialStep(mode) + + return TaskProcessingState( + mode = mode, + resultState = ResultState.Pending, + + step = initialStep, + stepState = StepState.Queued, + reservationTimeout = null, + + taskCreatedAt = now, + taskModifiedAt = now, + taskTimeout = now.plus(taskConfigProperties.taskTimeout) + ) + } + + fun doReserve(task: GoldenRecordTask) { + val state = task.processingState + val now = Instant.now() + + if (state.resultState != ResultState.Pending || state.stepState != StepState.Queued) { + throw BpdmIllegalStateException(task.taskId, state) + } + + // reserved for current step, set reservation timeout + state.stepState = StepState.Reserved + state.reservationTimeout = now.plus(taskConfigProperties.taskReservationTimeout) + state.taskModifiedAt = now + } + + fun doResolveSuccessful(task: GoldenRecordTask, resultBusinessPartner: BusinessPartnerFullDto) { + val state = task.processingState + val now = Instant.now() + + if (state.resultState != ResultState.Pending || state.stepState != StepState.Reserved) { + throw BpdmIllegalStateException(task.taskId, state) + } + + val nextStep = getNextStep(state.mode, state.step) + + if (nextStep != null) { + // still steps left to process -> queued for next step, no timeout + state.step = nextStep + state.stepState = StepState.Queued + } else { + // last step finished -> set resultState and stepState to success + state.resultState = ResultState.Success + state.stepState = StepState.Success + } + + // always set taskModifiedAt and reset stepTimeout + state.reservationTimeout = null + state.taskModifiedAt = now + + task.businessPartner = resultBusinessPartner + } + + fun doResolveFailed(task: GoldenRecordTask, errors: List) { + val state = task.processingState + val now = Instant.now() + + if (state.resultState != ResultState.Pending || state.stepState != StepState.Reserved) { + throw BpdmIllegalStateException(task.taskId, state) + } + + state.resultState = ResultState.Error + state.errors = errors + state.stepState = StepState.Error + state.reservationTimeout = null + state.taskModifiedAt = now + } + + private fun getInitialStep(mode: TaskMode): TaskStep { + return getStepsForMode(mode).first() + } + + private fun getNextStep(mode: TaskMode, currentStep: TaskStep): TaskStep? { + return getStepsForMode(mode) + .dropWhile { it != currentStep } // drop steps before currentStep + .drop(1) // then drop currentStep + .firstOrNull() // return next step + } + + private fun getStepsForMode(mode: TaskMode): List = + when (mode) { + TaskMode.UpdateFromSharingMember -> listOf(TaskStep.CleanAndSync, TaskStep.PoolSync) + TaskMode.UpdateFromPool -> listOf(TaskStep.Clean) + } +} diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt new file mode 100644 index 000000000..082e372eb --- /dev/null +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.service + +import org.eclipse.tractusx.bpdm.orchestrator.exception.BpdmDuplicateTaskIdException +import org.eclipse.tractusx.bpdm.orchestrator.model.GoldenRecordTask +import org.eclipse.tractusx.orchestrator.api.model.ResultState +import org.eclipse.tractusx.orchestrator.api.model.StepState +import org.eclipse.tractusx.orchestrator.api.model.TaskStep +import org.springframework.stereotype.Service + +@Service +class GoldenRecordTaskStorage { + + private val tasks: MutableList = mutableListOf() + + fun addTask(task: GoldenRecordTask): GoldenRecordTask { + return task.also { + if (getTask(it.taskId) != null) { + throw BpdmDuplicateTaskIdException(it.taskId) + } + tasks.add(it) + } + } + + fun getTask(taskId: String) = + tasks.firstOrNull { it.taskId == taskId } + + fun getQueuedTasksByStep(step: TaskStep, amount: Int): List { + return tasks + .filter { + val state = it.processingState + state.resultState == ResultState.Pending && + state.stepState == StepState.Queued && + state.step == step + } + .take(amount) + } +} diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt index b9775995c..4ef89f399 100644 --- a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt @@ -22,11 +22,15 @@ package org.eclipse.tractusx.bpdm.orchestrator.util import com.neovisionaries.i18n.CountryCode import org.eclipse.tractusx.bpdm.common.dto.AddressType import org.eclipse.tractusx.orchestrator.api.model.* +import java.time.Duration import java.time.Instant +// TODO migrate to test scope //While we don't have a service logic implementation of the API use a dummy response for the endpoints object DummyValues { + val taskTimeout: Duration = Duration.ofHours(24) + val dummyResponseCreateTask = TaskCreateResponse( listOf( @@ -39,8 +43,9 @@ object DummyValues { stepState = StepState.Queued, errors = emptyList(), createdAt = Instant.now(), - modifiedAt = Instant.now() - ) + modifiedAt = Instant.now(), + timeout = Instant.now().plus(taskTimeout) + ), ), TaskClientStateDto( taskId = "1", @@ -51,8 +56,9 @@ object DummyValues { stepState = StepState.Queued, errors = emptyList(), createdAt = Instant.now(), - modifiedAt = Instant.now() - ) + modifiedAt = Instant.now(), + timeout = Instant.now().plus(taskTimeout) + ), ) ) ) @@ -215,8 +221,9 @@ object DummyValues { stepState = StepState.Queued, errors = emptyList(), createdAt = Instant.now(), - modifiedAt = Instant.now() - ) + modifiedAt = Instant.now(), + timeout = Instant.now().plus(taskTimeout) + ), ), TaskClientStateDto( taskId = "1", @@ -227,8 +234,9 @@ object DummyValues { stepState = StepState.Queued, errors = emptyList(), createdAt = Instant.now(), - modifiedAt = Instant.now() - ) + modifiedAt = Instant.now(), + timeout = Instant.now().plus(taskTimeout) + ), ) ) ) diff --git a/bpdm-orchestrator/src/main/resources/application.properties b/bpdm-orchestrator/src/main/resources/application.properties index 786d5279d..c9f18aabf 100644 --- a/bpdm-orchestrator/src/main/resources/application.properties +++ b/bpdm-orchestrator/src/main/resources/application.properties @@ -41,6 +41,5 @@ springdoc.swagger-ui.csrf.enabled=true management.endpoint.health.probes.enabled=true management.health.livenessState.enabled=true management.health.readinessState.enabled=true - - - +bpdm.task.task-timeout=3d +bpdm.task.task-reservation-timeout=1d diff --git a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt index 80d754585..1238b93eb 100644 --- a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt +++ b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt @@ -30,6 +30,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.web.reactive.function.client.WebClientResponseException +// TODO fix tests @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = ["bpdm.api.upsert-limit=3"]) class GoldenRecordTaskControllerIT @Autowired constructor( val orchestratorClient: OrchestrationApiClient From 0519ee2303467aa3574ecf70d0e0a298c84c2a8a Mon Sep 17 00:00:00 2001 From: Martin Kaeser Date: Thu, 12 Oct 2023 17:11:32 +0200 Subject: [PATCH 2/3] feat(orchestrator): Service logic tests --- .../service/GoldenRecordTaskStorage.kt | 5 + .../bpdm/orchestrator/util/DummyValues.kt | 245 --------- .../GoldenRecordTaskControllerIT.kt | 496 ++++++++++++------ .../service/GoldenRecordTaskStateMachineIT.kt | 199 +++++++ .../BusinessPartnerTestValues.kt | 16 +- 5 files changed, 567 insertions(+), 394 deletions(-) delete mode 100644 bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt create mode 100644 bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachineIT.kt rename bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/{util => testdata}/BusinessPartnerTestValues.kt (97%) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt index 082e372eb..9b5113dd8 100644 --- a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStorage.kt @@ -31,6 +31,11 @@ class GoldenRecordTaskStorage { private val tasks: MutableList = mutableListOf() + // Needed for testing + fun clear() { + tasks.clear() + } + fun addTask(task: GoldenRecordTask): GoldenRecordTask { return task.also { if (getTask(it.taskId) != null) { diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt deleted file mode 100644 index 4ef89f399..000000000 --- a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/DummyValues.kt +++ /dev/null @@ -1,245 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * SPDX-License-Identifier: Apache-2.0 - ******************************************************************************/ - -package org.eclipse.tractusx.bpdm.orchestrator.util - -import com.neovisionaries.i18n.CountryCode -import org.eclipse.tractusx.bpdm.common.dto.AddressType -import org.eclipse.tractusx.orchestrator.api.model.* -import java.time.Duration -import java.time.Instant - -// TODO migrate to test scope -//While we don't have a service logic implementation of the API use a dummy response for the endpoints -object DummyValues { - - val taskTimeout: Duration = Duration.ofHours(24) - - val dummyResponseCreateTask = - TaskCreateResponse( - listOf( - TaskClientStateDto( - taskId = "0", - businessPartnerResult = null, - processingState = TaskProcessingStateDto( - resultState = ResultState.Pending, - step = TaskStep.CleanAndSync, - stepState = StepState.Queued, - errors = emptyList(), - createdAt = Instant.now(), - modifiedAt = Instant.now(), - timeout = Instant.now().plus(taskTimeout) - ), - ), - TaskClientStateDto( - taskId = "1", - businessPartnerResult = null, - processingState = TaskProcessingStateDto( - resultState = ResultState.Pending, - step = TaskStep.CleanAndSync, - stepState = StepState.Queued, - errors = emptyList(), - createdAt = Instant.now(), - modifiedAt = Instant.now(), - timeout = Instant.now().plus(taskTimeout) - ), - ) - ) - ) - - val dummyStepReservationResponse = TaskStepReservationResponse( - timeout = Instant.now().plusSeconds(300), - reservedTasks = listOf( - TaskStepReservationEntryDto( - taskId = "0", - businessPartner = BusinessPartnerFullDto( - generic = BusinessPartnerGenericDto( - nameParts = listOf("Dummy", "Name"), - postalAddress = PostalAddressDto( - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "70771" - ) - ) - ) - ) - ), - TaskStepReservationEntryDto( - taskId = "1", - businessPartner = BusinessPartnerFullDto( - generic = BusinessPartnerGenericDto( - nameParts = listOf("Other", "Name"), - postalAddress = PostalAddressDto( - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "80331" - ) - ) - ) - ) - ) - ) - ) - - - private val businessPartnerFull1 = BusinessPartnerFullDto( - generic = BusinessPartnerGenericDto( - nameParts = listOf("Dummy", "Name"), - postalAddress = PostalAddressDto( - addressType = AddressType.LegalAddress, - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "70771" - ) - ) - ), - legalEntity = LegalEntityDto( - legalName = "Dummy Name", - bpnLReference = BpnReferenceDto( - referenceValue = "request-id-l-1", - referenceType = BpnReferenceType.BpnRequestIdentifier - ), - legalAddress = LogisticAddressDto( - bpnAReference = BpnReferenceDto( - referenceValue = "request-id-a-1", - referenceType = BpnReferenceType.BpnRequestIdentifier - ), - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "70771" - ) - ), - ), - address = LogisticAddressDto( - bpnAReference = BpnReferenceDto( - referenceValue = "request-id-a-1", - referenceType = BpnReferenceType.BpnRequestIdentifier - ), - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "70771" - ) - ) - ) - - private val businessPartnerFull2 = BusinessPartnerFullDto( - generic = BusinessPartnerGenericDto( - nameParts = listOf("Other", "Name"), - postalAddress = PostalAddressDto( - addressType = AddressType.AdditionalAddress, - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "80331" - ) - ) - ), - legalEntity = LegalEntityDto( - legalName = "Other Name", - bpnLReference = BpnReferenceDto( - referenceValue = "BPNL1", - referenceType = BpnReferenceType.Bpn - ), - legalAddress = LogisticAddressDto( - bpnAReference = BpnReferenceDto( - referenceValue = "BPNA1", - referenceType = BpnReferenceType.Bpn - ), - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "80333" - ) - ) - ), - site = SiteDto( - name = "Other Site Name", - bpnSReference = BpnReferenceDto( - referenceValue = "BPNS1", - referenceType = BpnReferenceType.Bpn - ), - mainAddress = LogisticAddressDto( - bpnAReference = BpnReferenceDto( - referenceValue = "BPNA2", - referenceType = BpnReferenceType.Bpn - ), - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "80331" - ) - ) - ), - address = LogisticAddressDto( - bpnAReference = BpnReferenceDto( - referenceValue = "BPNA3", - referenceType = BpnReferenceType.Bpn - ), - physicalPostalAddress = PhysicalPostalAddressDto( - country = CountryCode.DE, - postalCode = "80331" - ) - ) - ) - - val dummyPoolSyncResponse = TaskStepReservationResponse( - timeout = Instant.now().plusSeconds(300), - reservedTasks = listOf( - TaskStepReservationEntryDto( - taskId = "0", - businessPartner = businessPartnerFull1 - ), - TaskStepReservationEntryDto( - taskId = "1", - businessPartner = businessPartnerFull2 - ) - ) - ) - - val dummyResponseTaskState = - TaskStateResponse( - listOf( - TaskClientStateDto( - taskId = "0", - businessPartnerResult = null, - processingState = TaskProcessingStateDto( - resultState = ResultState.Pending, - step = TaskStep.CleanAndSync, - stepState = StepState.Queued, - errors = emptyList(), - createdAt = Instant.now(), - modifiedAt = Instant.now(), - timeout = Instant.now().plus(taskTimeout) - ), - ), - TaskClientStateDto( - taskId = "1", - businessPartnerResult = null, - processingState = TaskProcessingStateDto( - resultState = ResultState.Pending, - step = TaskStep.Clean, - stepState = StepState.Queued, - errors = emptyList(), - createdAt = Instant.now(), - modifiedAt = Instant.now(), - timeout = Instant.now().plus(taskTimeout) - ), - ) - ) - ) - - -} \ No newline at end of file diff --git a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt index 1238b93eb..dfae37951 100644 --- a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt +++ b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/controller/GoldenRecordTaskControllerIT.kt @@ -19,239 +19,439 @@ package org.eclipse.tractusx.bpdm.orchestrator.controller -import org.assertj.core.api.Assertions -import org.eclipse.tractusx.bpdm.orchestrator.util.BusinessPartnerTestValues -import org.eclipse.tractusx.bpdm.orchestrator.util.DummyValues +import org.assertj.core.api.Assertions.* +import org.assertj.core.api.ThrowableAssert +import org.assertj.core.data.TemporalUnitOffset +import org.eclipse.tractusx.bpdm.orchestrator.config.TaskConfigProperties +import org.eclipse.tractusx.bpdm.orchestrator.service.GoldenRecordTaskStorage +import org.eclipse.tractusx.bpdm.orchestrator.testdata.BusinessPartnerTestValues import org.eclipse.tractusx.orchestrator.api.client.OrchestrationApiClient import org.eclipse.tractusx.orchestrator.api.model.* +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 +import org.springframework.http.HttpStatus import org.springframework.web.reactive.function.client.WebClientResponseException - - -// TODO fix tests -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = ["bpdm.api.upsert-limit=3"]) +import java.time.Instant +import java.time.temporal.ChronoUnit + +val WITHIN_ALLOWED_TIME_OFFSET: TemporalUnitOffset = within(10, ChronoUnit.SECONDS) + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = [ + "bpdm.api.upsert-limit=3", + "bpdm.task.task-timeout=12h" + ] +) class GoldenRecordTaskControllerIT @Autowired constructor( - val orchestratorClient: OrchestrationApiClient + val orchestratorClient: OrchestrationApiClient, + val taskConfigProperties: TaskConfigProperties, + val goldenRecordTaskStorage: GoldenRecordTaskStorage ) { + @BeforeEach + fun cleanUp() { + goldenRecordTaskStorage.clear() + } + /** - * Validate create cleaning task endpoint is invokable with request body and returns dummy response + * GIVEN no tasks + * WHEN creating some tasks in UpdateFromSharingMember mode + * THEN expect create response contains correct processingState with step==CleanAndSync + * WHEN checking state + * THEN expect same state as in create response */ @Test - fun `request cleaning task and expect dummy response`() { - val request = TaskCreateRequest( - mode = TaskMode.UpdateFromSharingMember, - businessPartners = listOf(BusinessPartnerTestValues.businessPartner1, BusinessPartnerTestValues.businessPartner2) - ) + fun `request cleaning task`() { + // create tasks and check response + val createdTasks = createTasks().createdTasks - val expected = DummyValues.dummyResponseCreateTask + val expectedTimeout = Instant.now().plus(taskConfigProperties.taskTimeout) - val response = orchestratorClient.goldenRecordTasks.createTasks(request) + assertThat(createdTasks.size).isEqualTo(2) - Assertions.assertThat(response).isEqualTo(expected) + assertThat(createdTasks[0].taskId).isNotEqualTo(createdTasks[1].taskId) + + createdTasks.forEach { stateDto -> + assertThat(stateDto.businessPartnerResult).isNull() + val processingState = stateDto.processingState + assertProcessingStateDto(processingState, ResultState.Pending, TaskStep.CleanAndSync, StepState.Queued) + assertThat(processingState.errors).isEqualTo(emptyList()) + assertThat(processingState.createdAt).isEqualTo(processingState.modifiedAt) + assertThat(processingState.timeout).isCloseTo(expectedTimeout, WITHIN_ALLOWED_TIME_OFFSET) + } + + // check if response is consistent with searchTaskStates response + val statesResponse = searchTaskStates(createdTasks.map { it.taskId }) + assertThat(statesResponse.tasks).isEqualTo(createdTasks) } /** - * Validate reserve cleaning task endpoint is invokable with clean and sync step and returns dummy response + * GIVEN no tasks + * WHEN creating some tasks in UpdateFromPool mode + * THEN expect create response contains correct processingState with step==Clean */ @Test - fun `request reservation for clean and sync and expect dummy response`() { - val request = TaskStepReservationRequest( - amount = 2, - step = TaskStep.CleanAndSync - ) - - val expected = DummyValues.dummyStepReservationResponse + fun `request cleaning task in alternative mode`() { + // create tasks and check response + val createdTasks = createTasks(TaskMode.UpdateFromPool).createdTasks - val response = orchestratorClient.goldenRecordTasks.reserveTasksForStep(request) + assertThat(createdTasks.size).isEqualTo(2) + val processingState = createdTasks[0].processingState - Assertions.assertThat(response).isEqualTo(expected) + // Mode "UpdateFromPool" should trigger "Clean" step + assertProcessingStateDto(processingState, ResultState.Pending, TaskStep.Clean, StepState.Queued) } /** - * Validate reserve cleaning task endpoint is invokable with pool sync step and returns dummy response + * GIVEN some tasks were created in UpdateFromSharingMember mode + * WHEN reserving some tasks in step CleanAndSync + * THEN expect reservation returns the correct number of entries containing the correct business partner information + * WHEN trying to reserve more tasks + * THEN expect no additional results + * WHEN checking state + * THEN expect correct stepState (Reserved) */ @Test - fun `request reservation for pool sync and expect dummy response`() { - val request = TaskStepReservationRequest( - amount = 2, - step = TaskStep.PoolSync - ) - - val expected = DummyValues.dummyPoolSyncResponse + fun `request reservation`() { + // create tasks + val createdTasks = createTasks(TaskMode.UpdateFromSharingMember).createdTasks + assertThat(createdTasks.size).isEqualTo(2) + + val expectedReservationTimeout = Instant.now().plus(taskConfigProperties.taskReservationTimeout) + + // reserve tasks + val reservationResponse1 = reserveTasks(TaskStep.CleanAndSync) + val reservedTasks = reservationResponse1.reservedTasks + + // expect the correct number of entries with the correct timeout + assertThat(reservationResponse1.timeout).isCloseTo(expectedReservationTimeout, WITHIN_ALLOWED_TIME_OFFSET) + assertThat(reservedTasks.size).isEqualTo(2) + assertThat(reservedTasks.map { it.taskId }).isEqualTo(createdTasks.map { it.taskId }) + + // ...and with the correct business partner information + assertThat(reservedTasks[0].businessPartner.generic).isEqualTo(BusinessPartnerTestValues.businessPartner1) + assertThat(reservedTasks[1].businessPartner.generic).isEqualTo(BusinessPartnerTestValues.businessPartner2) + assertThat(reservedTasks[1].businessPartner.legalEntity).isNull() + assertThat(reservedTasks[1].businessPartner.site).isNull() + assertThat(reservedTasks[1].businessPartner.address).isNull() + + // trying to reserve more tasks returns no additional entries + val reservationResponse2 = reserveTasks(TaskStep.CleanAndSync) + assertThat(reservationResponse2.reservedTasks.size).isEqualTo(0) + + // check searchTaskStates response + val statesResponse = searchTaskStates(reservedTasks.map { it.taskId }) + assertThat(statesResponse.tasks.size).isEqualTo(2) + statesResponse.tasks.forEach { stateDto -> + assertThat(stateDto.businessPartnerResult).isNull() + val processingState = stateDto.processingState + // stepState should have changed to Reserved + assertProcessingStateDto(processingState, ResultState.Pending, TaskStep.CleanAndSync, StepState.Reserved) + assertThat(processingState.errors).isEqualTo(emptyList()) + assertThat(processingState.modifiedAt).isAfter(processingState.createdAt) + assertThat(processingState.modifiedAt).isCloseTo(Instant.now(), WITHIN_ALLOWED_TIME_OFFSET) + } + } - val response = orchestratorClient.goldenRecordTasks.reserveTasksForStep(request) + /** + * GIVEN some tasks were created in UpdateFromPool mode + * WHEN reserving some tasks in step CleanAndSync + * THEN expect reservation returns no results + */ + @Test + fun `request reservation for wrong step`() { + // create tasks + createTasks(TaskMode.UpdateFromPool) - Assertions.assertThat(response).isEqualTo(expected) + // try reservation for wrong step + val reservedTasks = reserveTasks(TaskStep.CleanAndSync).reservedTasks + assertThat(reservedTasks.size).isEqualTo(0) } /** - * Validate reserve cleaning task endpoint is invokable with cleaning step and returns dummy response + * GIVEN some tasks were created + * WHEN reserving one task in step CleanAndSync + * THEN expect first task and state to switch to stepState==Reserved + * WHEN resolving this task + * THEN expect state to switch to step==PoolSync and stepState==Queued + * WHEN reserving this task in step PoolSync + * THEN expect state to switch to stepState==Reserved + * WHEN resolving this task + * THEN expect state to switch to resultState==Success and stepState==Success and correct business partner data */ @Test - fun `request reservation for clean and expect dummy response`() { - val request = TaskStepReservationRequest( - amount = 2, - step = TaskStep.Clean + fun `post cleaning results for all steps`() { + // create tasks + createTasks() + + // reserve task for step==CleanAndSync + val reservedTasks1 = reserveTasks(TaskStep.CleanAndSync, 1).reservedTasks + val taskId = reservedTasks1.single().taskId + assertThat(reservedTasks1[0].businessPartner.generic).isEqualTo(BusinessPartnerTestValues.businessPartner1) + + // now in stepState==Reserved + assertProcessingStateDto( + searchTaskStates(listOf(taskId)).tasks.single().processingState, + ResultState.Pending, TaskStep.CleanAndSync, StepState.Reserved + ) + + // resolve task + val businessPartnerFull1 = BusinessPartnerTestValues.businessPartner2Full + val resultEntry1 = TaskStepResultEntryDto( + taskId = taskId, + businessPartner = businessPartnerFull1 + ) + resolveTasks(listOf(resultEntry1)) + + // now in next step and stepState==Queued + assertProcessingStateDto( + searchTaskStates(listOf(taskId)).tasks.single().processingState, + ResultState.Pending, TaskStep.PoolSync, StepState.Queued ) - val expected = DummyValues.dummyStepReservationResponse + // reserve task for step==PoolSync + val reservedTasks2 = reserveTasks(TaskStep.PoolSync, 3).reservedTasks + assertThat(reservedTasks2.size).isEqualTo(1) + assertThat(reservedTasks2.single().businessPartner).isEqualTo(businessPartnerFull1) - val response = orchestratorClient.goldenRecordTasks.reserveTasksForStep(request) + // now in stepState==Queued + val stateDto = searchTaskStates(listOf(taskId)).tasks.single() + assertProcessingStateDto( + stateDto.processingState, + ResultState.Pending, TaskStep.PoolSync, StepState.Reserved + ) + assertThat(stateDto.businessPartnerResult).isNull() - Assertions.assertThat(response).isEqualTo(expected) + // resolve task again + val businessPartnerFull2 = businessPartnerFull1.copy( + generic = businessPartnerFull1.generic.copy( + bpnL = "BPNL-test" + ) + ) + val resultEntry2 = TaskStepResultEntryDto( + taskId = taskId, + businessPartner = businessPartnerFull2 + ) + resolveTasks(listOf(resultEntry2)) + + // final step -> now in stepState==Success + val finalStateDto = searchTaskStates(listOf(taskId)).tasks.single() + assertProcessingStateDto( + finalStateDto.processingState, + ResultState.Success, TaskStep.PoolSync, StepState.Success + ) + // check returned BP + assertThat(finalStateDto.businessPartnerResult).isEqualTo(businessPartnerFull2.generic) } /** - * Validate post cleaning result endpoint is invokable + * GIVEN some tasks were created and reserved + * WHEN resolving this task an error + * THEN expect state to switch to resultState==Error and stepState==Error + * WHEN reserving this task in step PoolSync + * THEN expect state to switch to stepState==Reserved + * WHEN resolving this task + * THEN expect state to switch to resultState==Success and stepState==Success and correct business partner data */ @Test - fun `post cleaning result is invokable`() { - val request = TaskStepResultRequest( - results = listOf( - TaskStepResultEntryDto( - taskId = "0", - businessPartner = BusinessPartnerFullDto( - generic = BusinessPartnerTestValues.businessPartner1, - legalEntity = BusinessPartnerTestValues.legalEntity1, - site = BusinessPartnerTestValues.site1, - address = BusinessPartnerTestValues.logisticAddress1 - ), - errors = emptyList() - ), - TaskStepResultEntryDto( - taskId = "1", - businessPartner = BusinessPartnerFullDto( - generic = BusinessPartnerTestValues.businessPartner2, - legalEntity = BusinessPartnerTestValues.legalEntity2, - site = BusinessPartnerTestValues.site2, - address = BusinessPartnerTestValues.logisticAddress2 - ), - errors = emptyList() - ), - TaskStepResultEntryDto( - taskId = "2", - businessPartner = null, - errors = listOf( - TaskErrorDto(type = TaskErrorType.Unspecified, "Error Description") - ) - ), - ) + fun `post cleaning result with error`() { + // create tasks + createTasks() + + // reserve task for step==CleanAndSync + val taskId = reserveTasks(TaskStep.CleanAndSync, 1).reservedTasks.single().taskId + + // resolve task with error + val errorDto = TaskErrorDto(TaskErrorType.Unspecified, "Unfortunate event") + val resultEntry = TaskStepResultEntryDto( + taskId = taskId, + errors = listOf(errorDto) ) + resolveTasks(listOf(resultEntry)) - orchestratorClient.goldenRecordTasks.resolveStepResults(request) + // now in error state + val stateDto = searchTaskStates(listOf(taskId)).tasks.single() + assertProcessingStateDto( + stateDto.processingState, + ResultState.Error, TaskStep.CleanAndSync, StepState.Error + ) + assertThat(stateDto.businessPartnerResult).isNull() + // expect error in response + assertThat(stateDto.processingState.errors.single()).isEqualTo(errorDto) } /** - * When requesting cleaning of too many business partners (over the upsert limit) - * Then throw exception + * WHEN requesting cleaning of too many business partners (over the upsert limit) + * THEN throw exception */ @Test fun `expect exception on requesting too many cleaning tasks`() { - - //Create entries above the upsert limit of 3 - val request = TaskCreateRequest( - mode = TaskMode.UpdateFromPool, - businessPartners = listOf( - BusinessPartnerTestValues.businessPartner1, - BusinessPartnerTestValues.businessPartner1, - BusinessPartnerTestValues.businessPartner1, - BusinessPartnerTestValues.businessPartner1 - ) + // Create entries above the upsert limit of 3 + val businessPartners = listOf( + BusinessPartnerTestValues.businessPartner1, + BusinessPartnerTestValues.businessPartner1, + BusinessPartnerTestValues.businessPartner1, + BusinessPartnerTestValues.businessPartner1 ) - Assertions.assertThatThrownBy { - orchestratorClient.goldenRecordTasks.createTasks(request) - }.isInstanceOf(WebClientResponseException::class.java) + assertBadRequestException() { + createTasks(businessPartners = businessPartners) + } } /** - * When reserving too many cleaning tasks (over the upsert limit) - * Then throw exception + * WHEN reserving too many cleaning tasks (over the upsert limit) + * THEN throw exception */ @Test fun `expect exception on requesting too many reservations`() { - - //Create entries above the upsert limit of 3 - val request = TaskStepReservationRequest( - amount = 200, - step = TaskStep.CleanAndSync - ) - - Assertions.assertThatThrownBy { - orchestratorClient.goldenRecordTasks.reserveTasksForStep(request) - }.isInstanceOf(WebClientResponseException::class.java) + // Create entries above the upsert limit of 3 + assertBadRequestException { + reserveTasks(TaskStep.CleanAndSync, 200) + } } /** - * When posting too many cleaning results (over the upsert limit) - * Then throw exception + * WHEN posting too many cleaning results (over the upsert limit) + * THEN throw exception */ @Test - fun `expect exception on posting too many cleaning results`() { - - val validCleaningResultEntry = TaskStepResultEntryDto( + fun `expect exception on posting too many task results`() { + val validResultEntry = TaskStepResultEntryDto( taskId = "0", businessPartner = null, errors = listOf(TaskErrorDto(type = TaskErrorType.Unspecified, description = "Description")) ) - //Create entries above the upsert limit of 3 - val request = TaskStepResultRequest( - results = listOf( - validCleaningResultEntry.copy(taskId = "0"), - validCleaningResultEntry.copy(taskId = "1"), - validCleaningResultEntry.copy(taskId = "2"), - validCleaningResultEntry.copy(taskId = "3"), - ) + // Create entries above the upsert limit of 3 + val resultEntries = listOf( + validResultEntry.copy(taskId = "0"), + validResultEntry.copy(taskId = "1"), + validResultEntry.copy(taskId = "2"), + validResultEntry.copy(taskId = "3"), ) - Assertions.assertThatThrownBy { - orchestratorClient.goldenRecordTasks.resolveStepResults(request) - }.isInstanceOf(WebClientResponseException::class.java) + assertBadRequestException { + resolveTasks(resultEntries) + } } /** - * Search for taskId and get dummy response on the test + * GIVEN some resolved tasks + * WHEN trying to resolve a task with different task id + * THEN expect a BAD_REQUEST + * WHEN trying to resolve a task with empty content + * THEN expect a BAD_REQUEST + * WHEN trying to resolve a task twice + * THEN expect a BAD_REQUEST */ - @Test - fun `search cleaning task state and expect dummy response`() { + fun `expect exceptions on posting inconsistent task results`() { + // create tasks + createTasks() + + // reserve tasks + val tasksIds = reserveTasks(TaskStep.CleanAndSync).reservedTasks.map { it.taskId } + assertThat(tasksIds.size).isEqualTo(2) + + // post wrong task ids + assertBadRequestException { + resolveTasks( + listOf( + TaskStepResultEntryDto( + taskId = "WRONG-ID" + ) + ) + ) + } + + // post correct task id but neither empty content + assertBadRequestException { + resolveTasks( + listOf( + TaskStepResultEntryDto( + taskId = tasksIds[0] + ) + ) + ) + } - val request = TaskStateRequest(listOf("0", "1")) + // post correct task id with business partner content + resolveTasks( + listOf( + TaskStepResultEntryDto( + taskId = tasksIds[0], + businessPartner = BusinessPartnerTestValues.businessPartner1Full + ) + ) + ) + // post task twice + assertBadRequestException { + resolveTasks( + listOf( + TaskStepResultEntryDto( + taskId = tasksIds[0], + businessPartner = BusinessPartnerTestValues.businessPartner1Full + ) + ) + ) + } - val expected = DummyValues.dummyResponseTaskState + // post correct task id with error content + resolveTasks( + listOf( + TaskStepResultEntryDto( + tasksIds[1], errors = listOf( + TaskErrorDto(type = TaskErrorType.Unspecified, "ERROR") + ) + ) + ) + ) + } - val response = orchestratorClient.goldenRecordTasks.searchTaskStates(request) + private fun createTasks(mode: TaskMode = TaskMode.UpdateFromSharingMember, businessPartners: List? = null): TaskCreateResponse = + orchestratorClient.goldenRecordTasks.createTasks( + TaskCreateRequest( + mode = mode, + businessPartners = businessPartners ?: listOf(BusinessPartnerTestValues.businessPartner1, BusinessPartnerTestValues.businessPartner2) + ) + ) - // Assert that the response matches the expected value - Assertions.assertThat(response).isEqualTo(expected) - } + private fun reserveTasks(step: TaskStep, amount: Int = 3) = + orchestratorClient.goldenRecordTasks.reserveTasksForStep( + TaskStepReservationRequest( + step = step, + amount = amount + ) + ) - /** - * When posting cleaning result without business partner data and no errors - * Then throw exception - */ - @Test - fun `expect exception on posting empty cleaning result`() { - val request = TaskStepResultRequest( - results = listOf( - TaskStepResultEntryDto( - taskId = "0", - businessPartner = null, - errors = emptyList() - ) + private fun resolveTasks(results: List) = + orchestratorClient.goldenRecordTasks.resolveStepResults( + TaskStepResultRequest( + results = results ) ) - Assertions.assertThatThrownBy { - orchestratorClient.goldenRecordTasks.resolveStepResults(request) - }.isInstanceOf(WebClientResponseException::class.java) - } + private fun searchTaskStates(taskIds: List) = + orchestratorClient.goldenRecordTasks.searchTaskStates( + TaskStateRequest(taskIds) + ) + private fun assertProcessingStateDto(processingStateDto: TaskProcessingStateDto, resultState: ResultState, step: TaskStep, stepState: StepState) { + assertThat(processingStateDto.resultState).isEqualTo(resultState) + assertThat(processingStateDto.step).isEqualTo(step) + assertThat(processingStateDto.stepState).isEqualTo(stepState) + } -} \ No newline at end of file + private fun assertBadRequestException(shouldRaiseThrowable: ThrowableAssert.ThrowingCallable) { + assertThatThrownBy(shouldRaiseThrowable) + .isInstanceOfSatisfying(WebClientResponseException::class.java) { + assertThat(it.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) + } + } +} diff --git a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachineIT.kt b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachineIT.kt new file mode 100644 index 000000000..77fc98547 --- /dev/null +++ b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskStateMachineIT.kt @@ -0,0 +1,199 @@ +/******************************************************************************* + * Copyright (c) 2021,2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.orchestrator.service + +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.assertj.core.data.TemporalUnitOffset +import org.eclipse.tractusx.bpdm.orchestrator.config.TaskConfigProperties +import org.eclipse.tractusx.bpdm.orchestrator.exception.BpdmIllegalStateException +import org.eclipse.tractusx.bpdm.orchestrator.model.GoldenRecordTask +import org.eclipse.tractusx.bpdm.orchestrator.model.TaskProcessingState +import org.eclipse.tractusx.bpdm.orchestrator.testdata.BusinessPartnerTestValues +import org.eclipse.tractusx.orchestrator.api.model.* +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import java.time.Instant +import java.time.temporal.ChronoUnit + +val WITHIN_ALLOWED_TIME_OFFSET: TemporalUnitOffset = Assertions.within(10, ChronoUnit.SECONDS) +val TASK_ID = "TASK-ID" + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = [ + "bpdm.api.upsert-limit=3", + "bpdm.task.task-timeout=12h" + ] +) +class GoldenRecordTaskStateMachineIT @Autowired constructor( + val taskConfigProperties: TaskConfigProperties, + val goldenRecordTaskStateMachine: GoldenRecordTaskStateMachine +) { + + /** + * WHEN creating an initial TaskProcessingState + * THEN expect the correct content + */ + @Test + fun `initial state`() { + val now = Instant.now() + val state = goldenRecordTaskStateMachine.initProcessingState(TaskMode.UpdateFromSharingMember) + + assertProcessingStateDto(state, ResultState.Pending, TaskStep.CleanAndSync, StepState.Queued) + assertThat(state.mode).isEqualTo(TaskMode.UpdateFromSharingMember) + assertThat(state.errors.size).isEqualTo(0) + assertThat(state.reservationTimeout).isNull() + assertThat(state.taskCreatedAt).isCloseTo(now, WITHIN_ALLOWED_TIME_OFFSET) + assertThat(state.taskModifiedAt).isEqualTo(state.taskCreatedAt) + assertThat(state.taskTimeout).isCloseTo(now.plus(taskConfigProperties.taskTimeout), WITHIN_ALLOWED_TIME_OFFSET) + } + + /** + * GIVEN a task with initial TaskProcessingState + * WHEN reserving and resolving + * THEN expect the TaskProcessingState to walk through all the steps/states until final state Success + * WHEN trying to reserve or resolve twice + * THEN expect an error + */ + @Test + fun `walk through all UpdateFromSharingMember steps`() { + // new task + val task = initTask(TaskMode.UpdateFromSharingMember) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.CleanAndSync, StepState.Queued) + + // 1st reserve + goldenRecordTaskStateMachine.doReserve(task) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.CleanAndSync, StepState.Reserved) + + // Can't reserve again! + assertThatThrownBy { + goldenRecordTaskStateMachine.doReserve(task) + }.isInstanceOf(BpdmIllegalStateException::class.java) + + // 1st resolve + goldenRecordTaskStateMachine.doResolveSuccessful(task, BusinessPartnerTestValues.businessPartner1Full) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.PoolSync, StepState.Queued) + assertThat(task.processingState.reservationTimeout).isNull() + + // Can't resolve again! + assertThatThrownBy { + goldenRecordTaskStateMachine.doResolveSuccessful(task, BusinessPartnerTestValues.businessPartner1Full) + }.isInstanceOf(BpdmIllegalStateException::class.java) + + // 2nd reserve + goldenRecordTaskStateMachine.doReserve(task) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.PoolSync, StepState.Reserved) + + // 2nd resolve + goldenRecordTaskStateMachine.doResolveSuccessful(task, BusinessPartnerTestValues.businessPartner1Full) + assertProcessingStateDto(task.processingState, ResultState.Success, TaskStep.PoolSync, StepState.Success) + + // Can't resolve again! + assertThatThrownBy { + goldenRecordTaskStateMachine.doResolveFailed(task, listOf(TaskErrorDto(TaskErrorType.Unspecified, "error"))) + }.isInstanceOf(BpdmIllegalStateException::class.java) + } + + + /** + * GIVEN a task with initial TaskProcessingState + * WHEN reserving and resolving + * THEN expect the TaskProcessingState to walk through all the steps/states and taskModifiedAt and reservationTimeout to be updated + */ + @Test + fun `walk through all UpdateFromPool steps`() { + // new task + val task = initTask(TaskMode.UpdateFromPool) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.Clean, StepState.Queued) + val modified0 = task.processingState.taskModifiedAt + + Thread.sleep(10) + + // reserve + goldenRecordTaskStateMachine.doReserve(task) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.Clean, StepState.Reserved) + assertThat(task.processingState.reservationTimeout) + .isCloseTo(Instant.now().plus(taskConfigProperties.taskReservationTimeout), WITHIN_ALLOWED_TIME_OFFSET) + val modified1 = task.processingState.taskModifiedAt + assertThat(modified1).isAfter(modified0) + + Thread.sleep(10) + + // resolve + goldenRecordTaskStateMachine.doResolveSuccessful(task, BusinessPartnerTestValues.businessPartner1Full) + assertProcessingStateDto(task.processingState, ResultState.Success, TaskStep.Clean, StepState.Success) + val modified2 = task.processingState.taskModifiedAt + assertThat(modified2).isAfter(modified1) + } + + + /** + * GIVEN a task with initial TaskProcessingState + * WHEN reserving and resolving with an error + * THEN expect the TaskProcessingState to reach final state Error + */ + @Test + fun `walk through steps and resolve with error`() { + // new task + val task = initTask(TaskMode.UpdateFromPool) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.Clean, StepState.Queued) + + // reserve + goldenRecordTaskStateMachine.doReserve(task) + assertProcessingStateDto(task.processingState, ResultState.Pending, TaskStep.Clean, StepState.Reserved) + + // resolve with error + val errors = listOf( + TaskErrorDto(TaskErrorType.Unspecified, "Unspecific error"), + TaskErrorDto(TaskErrorType.Timeout, "Timeout") + ) + goldenRecordTaskStateMachine.doResolveFailed(task, errors) + assertProcessingStateDto(task.processingState, ResultState.Error, TaskStep.Clean, StepState.Error) + assertThat(task.processingState.errors).isEqualTo(errors) + + // Can't reserve now! + assertThatThrownBy { + goldenRecordTaskStateMachine.doReserve(task) + }.isInstanceOf(BpdmIllegalStateException::class.java) + + // Can't reserve now! + assertThatThrownBy { + goldenRecordTaskStateMachine.doResolveSuccessful(task, BusinessPartnerTestValues.businessPartner1Full) + }.isInstanceOf(BpdmIllegalStateException::class.java) + } + + private fun assertProcessingStateDto(processingState: TaskProcessingState, resultState: ResultState, step: TaskStep, stepState: StepState) { + assertThat(processingState.resultState).isEqualTo(resultState) + assertThat(processingState.step).isEqualTo(step) + assertThat(processingState.stepState).isEqualTo(stepState) + } + + private fun initTask(mode: TaskMode = TaskMode.UpdateFromSharingMember) = + GoldenRecordTask( + taskId = TASK_ID, + businessPartner = BusinessPartnerFullDto( + generic = BusinessPartnerTestValues.businessPartner1 + ), + processingState = goldenRecordTaskStateMachine.initProcessingState(mode) + ) +} diff --git a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/BusinessPartnerTestValues.kt b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/testdata/BusinessPartnerTestValues.kt similarity index 97% rename from bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/BusinessPartnerTestValues.kt rename to bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/testdata/BusinessPartnerTestValues.kt index e546d4b0c..221e83739 100644 --- a/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/util/BusinessPartnerTestValues.kt +++ b/bpdm-orchestrator/src/test/kotlin/org/eclipse/tractusx/bpdm/orchestrator/testdata/BusinessPartnerTestValues.kt @@ -17,7 +17,7 @@ * SPDX-License-Identifier: Apache-2.0 ******************************************************************************/ -package org.eclipse.tractusx.bpdm.orchestrator.util +package org.eclipse.tractusx.bpdm.orchestrator.testdata import com.neovisionaries.i18n.CountryCode import org.eclipse.tractusx.bpdm.common.dto.* @@ -462,4 +462,18 @@ object BusinessPartnerTestValues { hasChanged = true ) + val businessPartner1Full = BusinessPartnerFullDto( + generic = businessPartner1, + legalEntity = legalEntity1, + site = site1, + address = logisticAddress1 + ) + + val businessPartner2Full = BusinessPartnerFullDto( + generic = businessPartner2, + legalEntity = legalEntity2, + site = site2, + address = logisticAddress2 + ) + } \ No newline at end of file From 85c680074f74c50b25f96a06aee353a4c4f05248 Mon Sep 17 00:00:00 2001 From: Martin Kaeser Date: Mon, 16 Oct 2023 11:15:50 +0200 Subject: [PATCH 3/3] feat(orchestrator): Service logic - synchronize GoldenRecordTaskService methods --- .../bpdm/orchestrator/service/GoldenRecordTaskService.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt index f742efdb2..159da7afd 100644 --- a/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt +++ b/bpdm-orchestrator/src/main/kotlin/org/eclipse/tractusx/bpdm/orchestrator/service/GoldenRecordTaskService.kt @@ -34,6 +34,7 @@ class GoldenRecordTaskService( val goldenRecordTaskStateMachine: GoldenRecordTaskStateMachine ) { + @Synchronized fun createTasks(createRequest: TaskCreateRequest): TaskCreateResponse { return createRequest.businessPartners .map { businessPartnerGeneric -> taskStorage.addTask(initTask(createRequest, businessPartnerGeneric)) } @@ -41,6 +42,7 @@ class GoldenRecordTaskService( .let { TaskCreateResponse(createdTasks = it) } } + @Synchronized fun searchTaskStates(stateRequest: TaskStateRequest): TaskStateResponse { return stateRequest.taskIds .mapNotNull { taskId -> taskStorage.getTask(taskId) } // skip missing tasks @@ -48,6 +50,7 @@ class GoldenRecordTaskService( .let { TaskStateResponse(tasks = it) } } + @Synchronized fun reserveTasksForStep(reservationRequest: TaskStepReservationRequest): TaskStepReservationResponse { val now = Instant.now() @@ -72,6 +75,7 @@ class GoldenRecordTaskService( ) } + @Synchronized fun resolveStepResults(resultRequest: TaskStepResultRequest) { resultRequest.results .forEach { resultEntry ->