diff --git a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessor.java b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/Processor.java similarity index 96% rename from core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessor.java rename to core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/Processor.java index 817d6d33fca..fdd0e895a4f 100644 --- a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessor.java +++ b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/Processor.java @@ -20,7 +20,7 @@ * */ @FunctionalInterface -public interface StateProcessor { +public interface Processor { /** * Process states diff --git a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/ProcessorImpl.java b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/ProcessorImpl.java new file mode 100644 index 00000000000..d99f3f62e4e --- /dev/null +++ b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/ProcessorImpl.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.statemachine; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static java.util.function.Predicate.isEqual; + +/** + * Describes the processing flow applied by a state machine. The entities are provided by a supplier. + * A process is a function that returns a boolean that indicates if the entity has been processed or not in + * the scope of the function. + * The run method returns the processed state count, this is used by the state machine to decide + * to apply the wait strategy or not. + *

+ * An {@link Guard} can be registered, if its predicate is verified, the guard processor is executed instead of the standard one. + * + * @param the entity that is processed + */ +public class ProcessorImpl implements Processor { + + private final Supplier> entities; + private Function process; + private Guard guard = Guard.noop(); + + private ProcessorImpl(Supplier> entitiesSupplier) { + entities = entitiesSupplier; + } + + @Override + public Long process() { + return entities.get().stream() + .map(entity -> guard.predicate().test(entity) + ? guard.process().apply(entity) : + process.apply(entity)) + .filter(isEqual(true)) + .count(); + } + + public static class Builder { + + private final ProcessorImpl processor; + + public Builder(Supplier> entitiesSupplier) { + processor = new ProcessorImpl<>(entitiesSupplier); + } + + public static Builder newInstance(Supplier> entitiesSupplier) { + return new Builder<>(entitiesSupplier); + } + + public Builder process(Function process) { + processor.process = process; + return this; + } + + public Builder guard(Predicate predicate, Function process) { + processor.guard = new Guard<>(predicate, process); + return this; + } + + public ProcessorImpl build() { + Objects.requireNonNull(processor.process); + + return processor; + } + } + + private record Guard(Predicate predicate, Function process) { + static Guard noop() { + return new Guard<>(e -> false, e -> false); + } + } +} diff --git a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateMachineManager.java b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateMachineManager.java index 7af112b8f2b..476769468b1 100644 --- a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateMachineManager.java +++ b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateMachineManager.java @@ -38,7 +38,7 @@ */ public class StateMachineManager { - private final List processors = new ArrayList<>(); + private final List processors = new ArrayList<>(); private final ScheduledExecutorService executor; private final AtomicBoolean active = new AtomicBoolean(); private final WaitStrategy waitStrategy; @@ -65,7 +65,7 @@ private StateMachineManager(String name, Monitor monitor, ExecutorInstrumentatio */ public Future start() { active.set(true); - return submit(0L); + return scheduleNextIterationIn(0L); } /** @@ -95,41 +95,37 @@ public boolean isActive() { return active.get(); } - @NotNull - private Future submit(long delayMillis) { - return executor.schedule(loop(), delayMillis, MILLISECONDS); - } - private Runnable loop() { return () -> { if (active.get()) { - long delay = performLogic(); - - // Submit next execution after delay - submit(delay); + performLogic(); } }; } - private long performLogic() { + private void performLogic() { try { var processed = processors.stream() - .mapToLong(StateProcessor::process) + .mapToLong(Processor::process) .sum(); waitStrategy.success(); - if (processed == 0) { - return waitStrategy.waitForMillis(); - } + var delay = processed == 0 ? waitStrategy.waitForMillis() : 0; + + scheduleNextIterationIn(delay); } catch (Error e) { active.set(false); monitor.severe(format("StateMachineManager [%s] unrecoverable error", name), e); } catch (Throwable e) { monitor.severe(format("StateMachineManager [%s] error caught", name), e); - return waitStrategy.retryInMillis(); + scheduleNextIterationIn(waitStrategy.retryInMillis()); } - return 0; + } + + @NotNull + private Future scheduleNextIterationIn(long delayMillis) { + return executor.schedule(loop(), delayMillis, MILLISECONDS); } public static class Builder { @@ -144,7 +140,7 @@ public static Builder newInstance(String name, Monitor monitor, ExecutorInstrume return new Builder(name, monitor, instrumentation, waitStrategy); } - public Builder processor(StateProcessor processor) { + public Builder processor(Processor processor) { loop.processors.add(processor); return this; } diff --git a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessorImpl.java b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessorImpl.java index dbfd062f33e..7d6c3f2e2d2 100644 --- a/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessorImpl.java +++ b/core/common/state-machine/src/main/java/org/eclipse/edc/statemachine/StateProcessorImpl.java @@ -29,8 +29,10 @@ * to apply the wait strategy or not. * * @param the entity that is processed + * @deprecated please use {@link ProcessorImpl} */ -public class StateProcessorImpl implements StateProcessor { +@Deprecated(since = "0.1.3") +public class StateProcessorImpl implements Processor { private final Supplier> entities; private final Function process; diff --git a/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/ProcessorImplTest.java b/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/ProcessorImplTest.java new file mode 100644 index 00000000000..c0cda0b25b1 --- /dev/null +++ b/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/ProcessorImplTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - Initial implementation + * + */ + +package org.eclipse.edc.statemachine; + +import org.eclipse.edc.statemachine.retry.TestEntity; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +class ProcessorImplTest { + + @Test + void shouldReturnTheProcessedCount() { + var entity = TestEntity.Builder.newInstance().id("id").build(); + var processor = ProcessorImpl.Builder.newInstance(() -> List.of(entity)) + .process(e -> true) + .build(); + + var count = processor.process(); + + assertThat(count).isEqualTo(1); + } + + @Test + void shouldNotCountUnprocessedEntities() { + var entity = TestEntity.Builder.newInstance().id("id").build(); + var processor = ProcessorImpl.Builder.newInstance(() -> List.of(entity)) + .process(e -> false) + .build(); + + var count = processor.process(); + + assertThat(count).isEqualTo(0); + } + + @Test + void shouldExecuteGuard_whenItsPredicateMatches() { + var entity = TestEntity.Builder.newInstance().id("id").build(); + Function process = mock(); + Function guardProcess = mock(); + when(guardProcess.apply(any())).thenReturn(true); + var processor = ProcessorImpl.Builder.newInstance(() -> List.of(entity)) + .guard(e -> true, guardProcess) + .process(process) + .build(); + + var count = processor.process(); + + assertThat(count).isEqualTo(1); + verify(guardProcess).apply(entity); + verifyNoInteractions(process); + } + + @Test + void shouldExecuteDefaultProcessor_whenGuardPredicateDoesNotMatch() { + var entity = TestEntity.Builder.newInstance().id("id").build(); + Function process = mock(); + Function guardProcess = mock(); + when(process.apply(any())).thenReturn(true); + var processor = ProcessorImpl.Builder.newInstance(() -> List.of(entity)) + .guard(e -> false, guardProcess) + .process(process) + .build(); + + var count = processor.process(); + + assertThat(count).isEqualTo(1); + verify(process).apply(entity); + verifyNoInteractions(guardProcess); + } +} diff --git a/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/StateMachineManagerTest.java b/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/StateMachineManagerTest.java index 3de672201f2..f304095a457 100644 --- a/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/StateMachineManagerTest.java +++ b/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/StateMachineManagerTest.java @@ -46,7 +46,7 @@ void setUp() { @Test void shouldExecuteProcessorsAsyncAndCanBeStopped() throws InterruptedException { - var processor = mock(StateProcessor.class); + var processor = mock(Processor.class); when(processor.process()).thenAnswer(i -> { Thread.sleep(100L); return 1L; @@ -68,7 +68,7 @@ void shouldExecuteProcessorsAsyncAndCanBeStopped() throws InterruptedException { @Test void shouldNotWaitForSomeTimeIfTheresAtLeastOneProcessedEntity() throws InterruptedException { - var processor = mock(StateProcessor.class); + var processor = mock(Processor.class); when(processor.process()).thenReturn(1L); doAnswer(i -> { return 1L; @@ -87,7 +87,7 @@ void shouldNotWaitForSomeTimeIfTheresAtLeastOneProcessedEntity() throws Interrup @Test void shouldWaitForSomeTimeIfNoEntityIsProcessed() throws InterruptedException { - var processor = mock(StateProcessor.class); + var processor = mock(Processor.class); when(processor.process()).thenReturn(0L); var waitStrategy = mock(WaitStrategy.class); doAnswer(i -> { @@ -107,7 +107,7 @@ void shouldWaitForSomeTimeIfNoEntityIsProcessed() throws InterruptedException { @Test void shouldExitWithAnExceptionIfProcessorExitsWithAnUnrecoverableError() { - var processor = mock(StateProcessor.class); + var processor = mock(Processor.class); when(processor.process()).thenThrow(new Error("unrecoverable")); var stateMachine = StateMachineManager.Builder.newInstance("test", monitor, instrumentation, waitStrategy) .processor(processor) @@ -119,7 +119,7 @@ void shouldExitWithAnExceptionIfProcessorExitsWithAnUnrecoverableError() { @Test void shouldWaitRetryTimeWhenAnExceptionIsThrownByAnProcessor() throws InterruptedException { - var processor = mock(StateProcessor.class); + var processor = mock(Processor.class); when(processor.process()).thenThrow(new EdcException("exception")).thenReturn(0L); when(waitStrategy.retryInMillis()).thenAnswer(i -> { return 1L; diff --git a/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/retry/TestEntity.java b/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/retry/TestEntity.java index f9bac55b300..592203a0a35 100644 --- a/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/retry/TestEntity.java +++ b/core/common/state-machine/src/test/java/org/eclipse/edc/statemachine/retry/TestEntity.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.eclipse.edc.spi.entity.StatefulEntity; -class TestEntity extends StatefulEntity { +public class TestEntity extends StatefulEntity { @Override public TestEntity copy() { return this; @@ -47,7 +47,7 @@ public Builder self() { } @Override - protected TestEntity build() { + public TestEntity build() { return super.build(); } } diff --git a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractCoreExtension.java b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractCoreExtension.java index 8963944e467..0a6d843e029 100644 --- a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractCoreExtension.java +++ b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractCoreExtension.java @@ -20,22 +20,18 @@ import org.eclipse.edc.connector.contract.listener.ContractNegotiationEventListener; import org.eclipse.edc.connector.contract.negotiation.ConsumerContractNegotiationManagerImpl; import org.eclipse.edc.connector.contract.negotiation.ProviderContractNegotiationManagerImpl; -import org.eclipse.edc.connector.contract.observe.ContractNegotiationObservableImpl; -import org.eclipse.edc.connector.contract.offer.ContractDefinitionResolverImpl; -import org.eclipse.edc.connector.contract.policy.PolicyArchiveImpl; import org.eclipse.edc.connector.contract.policy.PolicyEquality; import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; +import org.eclipse.edc.connector.contract.spi.negotiation.ContractNegotiationPendingGuard; import org.eclipse.edc.connector.contract.spi.negotiation.NegotiationWaitStrategy; import org.eclipse.edc.connector.contract.spi.negotiation.ProviderContractNegotiationManager; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.offer.ContractDefinitionResolver; -import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; import org.eclipse.edc.connector.contract.spi.validation.ContractValidationService; import org.eclipse.edc.connector.contract.validation.ContractExpiryCheckFunction; import org.eclipse.edc.connector.contract.validation.ContractValidationServiceImpl; -import org.eclipse.edc.connector.policy.spi.store.PolicyArchive; import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; @@ -67,8 +63,8 @@ import static org.eclipse.edc.policy.model.OdrlNamespace.ODRL_SCHEMA; @Provides({ - ContractValidationService.class, ConsumerContractNegotiationManager.class, PolicyArchive.class, - ProviderContractNegotiationManager.class, ContractNegotiationObservable.class, ContractDefinitionResolver.class + ContractValidationService.class, ConsumerContractNegotiationManager.class, + ProviderContractNegotiationManager.class }) @CoreExtension @Extension(value = ContractCoreExtension.NAME) @@ -109,9 +105,6 @@ public class ContractCoreExtension implements ServiceExtension { @Inject private AssetIndex assetIndex; - @Inject - private ContractDefinitionStore contractDefinitionStore; - @Inject private RemoteMessageDispatcherRegistry dispatcherRegistry; @@ -148,6 +141,18 @@ public class ContractCoreExtension implements ServiceExtension { @Inject private ProtocolWebhook protocolWebhook; + @Inject + private ContractDefinitionResolver contractDefinitionResolver; + + @Inject + private ContractNegotiationObservable observable; + + @Inject + private ContractNegotiationPendingGuard pendingGuard; + + @Inject + private ExecutorInstrumentation executorInstrumentation; + @Override public String name() { return NAME; @@ -177,13 +182,10 @@ public void shutdown() { } private void registerServices(ServiceExtensionContext context) { - var definitionService = new ContractDefinitionResolverImpl(monitor, contractDefinitionStore, policyEngine, policyStore); - context.registerService(ContractDefinitionResolver.class, definitionService); - var participantId = context.getParticipantId(); var policyEquality = new PolicyEquality(typeManager); - var validationService = new ContractValidationServiceImpl(participantId, agentService, definitionService, assetIndex, policyStore, policyEngine, policyEquality); + var validationService = new ContractValidationServiceImpl(participantId, agentService, contractDefinitionResolver, assetIndex, policyStore, policyEngine, policyEquality); context.registerService(ContractValidationService.class, validationService); // bind/register rule to evaluate contract expiry @@ -193,16 +195,11 @@ private void registerServices(ServiceExtensionContext context) { var function = new ContractExpiryCheckFunction(); policyEngine.registerFunction(TRANSFER_SCOPE, Permission.class, CONTRACT_EXPIRY_EVALUATION_KEY, function); - var iterationWaitMillis = context.getSetting(NEGOTIATION_STATE_MACHINE_ITERATION_WAIT_MILLIS, DEFAULT_ITERATION_WAIT); var waitStrategy = context.hasService(NegotiationWaitStrategy.class) ? context.getService(NegotiationWaitStrategy.class) : new ExponentialWaitStrategy(iterationWaitMillis); - var observable = new ContractNegotiationObservableImpl(); observable.registerListener(new ContractNegotiationEventListener(eventRouter, clock)); - context.registerService(ContractNegotiationObservable.class, observable); - context.registerService(PolicyArchive.class, new PolicyArchiveImpl(store)); - consumerNegotiationManager = ConsumerContractNegotiationManagerImpl.Builder.newInstance() .participantId(participantId) .waitStrategy(waitStrategy) @@ -211,12 +208,13 @@ private void registerServices(ServiceExtensionContext context) { .observable(observable) .clock(clock) .telemetry(telemetry) - .executorInstrumentation(context.getService(ExecutorInstrumentation.class)) + .executorInstrumentation(executorInstrumentation) .store(store) .policyStore(policyStore) .batchSize(context.getSetting(NEGOTIATION_CONSUMER_STATE_MACHINE_BATCH_SIZE, DEFAULT_BATCH_SIZE)) .entityRetryProcessConfiguration(consumerEntityRetryProcessConfiguration(context)) .protocolWebhook(protocolWebhook) + .pendingGuard(pendingGuard) .build(); providerNegotiationManager = ProviderContractNegotiationManagerImpl.Builder.newInstance() @@ -227,12 +225,13 @@ private void registerServices(ServiceExtensionContext context) { .observable(observable) .clock(clock) .telemetry(telemetry) - .executorInstrumentation(context.getService(ExecutorInstrumentation.class)) + .executorInstrumentation(executorInstrumentation) .store(store) .policyStore(policyStore) .batchSize(context.getSetting(NEGOTIATION_PROVIDER_STATE_MACHINE_BATCH_SIZE, DEFAULT_BATCH_SIZE)) .entityRetryProcessConfiguration(providerEntityRetryProcessConfiguration(context)) .protocolWebhook(protocolWebhook) + .pendingGuard(pendingGuard) .build(); context.registerService(ConsumerContractNegotiationManager.class, consumerNegotiationManager); diff --git a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractNegotiationDefaultServicesExtension.java b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractNegotiationDefaultServicesExtension.java new file mode 100644 index 00000000000..7f32d58231b --- /dev/null +++ b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/ContractNegotiationDefaultServicesExtension.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.contract; + +import org.eclipse.edc.connector.contract.observe.ContractNegotiationObservableImpl; +import org.eclipse.edc.connector.contract.offer.ContractDefinitionResolverImpl; +import org.eclipse.edc.connector.contract.policy.PolicyArchiveImpl; +import org.eclipse.edc.connector.contract.spi.negotiation.ContractNegotiationPendingGuard; +import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; +import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; +import org.eclipse.edc.connector.contract.spi.offer.ContractDefinitionResolver; +import org.eclipse.edc.connector.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.policy.spi.store.PolicyArchive; +import org.eclipse.edc.connector.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +/** + * Contract Negotiation Default Services Extension + */ +@Extension(value = ContractNegotiationDefaultServicesExtension.NAME) +public class ContractNegotiationDefaultServicesExtension implements ServiceExtension { + + public static final String NAME = "Contract Negotiation Default Services"; + + @Inject + private ContractDefinitionStore contractDefinitionStore; + + @Inject + private PolicyEngine policyEngine; + + @Inject + private PolicyDefinitionStore policyStore; + + @Inject + private ContractNegotiationStore store; + + @Provider + public ContractDefinitionResolver contractDefinitionResolver(ServiceExtensionContext context) { + return new ContractDefinitionResolverImpl(context.getMonitor(), contractDefinitionStore, policyEngine, policyStore); + } + + @Provider + public ContractNegotiationObservable observable() { + return new ContractNegotiationObservableImpl(); + } + + @Provider + public PolicyArchive policyArchive() { + return new PolicyArchiveImpl(store); + } + + @Provider(isDefault = true) + public ContractNegotiationPendingGuard pendingGuard() { + return it -> false; + } +} diff --git a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListener.java b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListener.java index b7e7c2d21f8..2ae1e10f3e3 100644 --- a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListener.java +++ b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListener.java @@ -16,9 +16,7 @@ import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationAccepted; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationAgreed; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationDeclined; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationEvent; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFailed; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFinalized; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationInitiated; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationOffered; @@ -71,18 +69,6 @@ public void terminated(ContractNegotiation negotiation) { publish(event); } - @Override - public void declined(ContractNegotiation negotiation) { - var event = baseBuilder(ContractNegotiationDeclined.Builder.newInstance(), negotiation).build(); - publish(event); - } - - @Override - public void failed(ContractNegotiation negotiation) { - var event = baseBuilder(ContractNegotiationFailed.Builder.newInstance(), negotiation).build(); - publish(event); - } - @Override public void agreed(ContractNegotiation negotiation) { var event = baseBuilder(ContractNegotiationAgreed.Builder.newInstance(), negotiation).build(); diff --git a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/AbstractContractNegotiationManager.java b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/AbstractContractNegotiationManager.java index 1dcf8950d79..5f3ecf3814d 100644 --- a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/AbstractContractNegotiationManager.java +++ b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/AbstractContractNegotiationManager.java @@ -14,6 +14,7 @@ package org.eclipse.edc.connector.contract.negotiation; +import org.eclipse.edc.connector.contract.spi.negotiation.ContractNegotiationPendingGuard; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationObservable; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; @@ -23,21 +24,27 @@ import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.protocol.ProtocolWebhook; +import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.retry.ExponentialWaitStrategy; import org.eclipse.edc.spi.retry.WaitStrategy; import org.eclipse.edc.spi.system.ExecutorInstrumentation; import org.eclipse.edc.spi.telemetry.Telemetry; +import org.eclipse.edc.statemachine.Processor; +import org.eclipse.edc.statemachine.ProcessorImpl; import org.eclipse.edc.statemachine.retry.EntityRetryProcessConfiguration; import org.eclipse.edc.statemachine.retry.EntityRetryProcessFactory; import org.jetbrains.annotations.NotNull; import java.time.Clock; import java.util.Objects; +import java.util.function.Function; import static org.eclipse.edc.connector.contract.ContractCoreExtension.DEFAULT_BATCH_SIZE; import static org.eclipse.edc.connector.contract.ContractCoreExtension.DEFAULT_ITERATION_WAIT; import static org.eclipse.edc.connector.contract.ContractCoreExtension.DEFAULT_SEND_RETRY_BASE_DELAY; import static org.eclipse.edc.connector.contract.ContractCoreExtension.DEFAULT_SEND_RETRY_LIMIT; +import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; +import static org.eclipse.edc.spi.persistence.StateEntityStore.isNotPending; public abstract class AbstractContractNegotiationManager { protected String participantId; @@ -54,6 +61,23 @@ public abstract class AbstractContractNegotiationManager { protected EntityRetryProcessFactory entityRetryProcessFactory; protected EntityRetryProcessConfiguration entityRetryProcessConfiguration = defaultEntityRetryProcessConfiguration(); protected ProtocolWebhook protocolWebhook; + protected ContractNegotiationPendingGuard pendingGuard = it -> false; + + abstract ContractNegotiation.Type type(); + + protected Processor processNegotiationsInState(ContractNegotiationStates state, Function function) { + var filter = new Criterion[] { hasState(state.code()), isNotPending(), new Criterion("type", "=", type().name()) }; + return ProcessorImpl.Builder.newInstance(() -> negotiationStore.nextNotLeased(batchSize, filter)) + .process(telemetry.contextPropagationMiddleware(function)) + .guard(pendingGuard, this::setPending) + .build(); + } + + private boolean setPending(ContractNegotiation contractNegotiation) { + contractNegotiation.setPending(true); + update(contractNegotiation); + return true; + } protected void transitionToInitial(ContractNegotiation negotiation) { negotiation.transitionInitial(); @@ -231,6 +255,11 @@ public Builder protocolWebhook(ProtocolWebhook protocolWebhook) { return this; } + public Builder pendingGuard(ContractNegotiationPendingGuard pendingGuard) { + manager.pendingGuard = pendingGuard; + return this; + } + public T build() { Objects.requireNonNull(manager.participantId, "participantId"); Objects.requireNonNull(manager.monitor, "monitor"); diff --git a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImpl.java b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImpl.java index 05e4dd522fc..90838da4512 100644 --- a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImpl.java +++ b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImpl.java @@ -25,17 +25,13 @@ import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementMessage; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementVerificationMessage; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; -import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequest; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractRequestMessage; -import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.statemachine.StateMachineManager; -import org.eclipse.edc.statemachine.StateProcessorImpl; import java.util.UUID; -import java.util.function.Function; import static java.lang.String.format; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.CONSUMER; @@ -45,7 +41,6 @@ import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.REQUESTING; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.TERMINATING; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.VERIFYING; -import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; /** * Implementation of the {@link ConsumerContractNegotiationManager}. @@ -104,6 +99,11 @@ public StatusResult initiate(ContractRequest request) { return StatusResult.success(negotiation); } + @Override + ContractNegotiation.Type type() { + return CONSUMER; + } + /** * Processes {@link ContractNegotiation} in state INITIAL. Transition ContractNegotiation to REQUESTING. * @@ -254,11 +254,6 @@ private boolean processTerminating(ContractNegotiation negotiation) { .execute("[Consumer] send rejection"); } - private StateProcessorImpl processNegotiationsInState(ContractNegotiationStates state, Function function) { - var filter = new Criterion[]{ hasState(state.code()), new Criterion("type", "=", CONSUMER.name()) }; - return new StateProcessorImpl<>(() -> negotiationStore.nextNotLeased(batchSize, filter), telemetry.contextPropagationMiddleware(function)); - } - /** * Builder for ConsumerContractNegotiationManagerImpl. */ diff --git a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImpl.java b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImpl.java index 08a3a36f8a9..2c3be0ec06d 100644 --- a/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImpl.java +++ b/core/control-plane/contract-core/src/main/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImpl.java @@ -25,14 +25,9 @@ import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreementMessage; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractNegotiationEventMessage; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; -import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractOfferMessage; -import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.statemachine.StateMachineManager; -import org.eclipse.edc.statemachine.StateProcessorImpl; - -import java.util.function.Function; import static java.lang.String.format; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation.Type.PROVIDER; @@ -42,7 +37,6 @@ import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.REQUESTED; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.TERMINATING; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.VERIFIED; -import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; /** * Implementation of the {@link ProviderContractNegotiationManager}. @@ -72,9 +66,9 @@ public void stop() { } } - private StateProcessorImpl processNegotiationsInState(ContractNegotiationStates state, Function function) { - var filter = new Criterion[]{hasState(state.code()), new Criterion("type", "=", PROVIDER.name())}; - return new StateProcessorImpl<>(() -> negotiationStore.nextNotLeased(batchSize, filter), telemetry.contextPropagationMiddleware(function)); + @Override + protected ContractNegotiation.Type type() { + return PROVIDER; } /** diff --git a/core/control-plane/contract-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/control-plane/contract-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 6192e04bdfc..8f9b132b59e 100644 --- a/core/control-plane/contract-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/core/control-plane/contract-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -15,3 +15,4 @@ org.eclipse.edc.connector.contract.ContractCoreExtension org.eclipse.edc.connector.contract.ContractNegotiationCommandExtension +org.eclipse.edc.connector.contract.ContractNegotiationDefaultServicesExtension diff --git a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/ContractCoreExtensionTest.java b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/ContractCoreExtensionTest.java index a2921ea2c50..4c498a40681 100644 --- a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/ContractCoreExtensionTest.java +++ b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/ContractCoreExtensionTest.java @@ -14,35 +14,23 @@ package org.eclipse.edc.connector.contract; -import org.eclipse.edc.connector.contract.policy.PolicyArchiveImpl; -import org.eclipse.edc.connector.policy.spi.store.PolicyArchive; +import org.eclipse.edc.connector.contract.spi.negotiation.ConsumerContractNegotiationManager; +import org.eclipse.edc.connector.contract.spi.negotiation.ProviderContractNegotiationManager; import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; -import org.eclipse.edc.spi.system.ExecutorInstrumentation; import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.spi.system.injection.ObjectFactory; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; @ExtendWith(DependencyInjectionExtension.class) class ContractCoreExtensionTest { - private ContractCoreExtension extension; - - @BeforeEach - void setUp(ObjectFactory factory, ServiceExtensionContext context) { - context.registerService(ExecutorInstrumentation.class, mock(ExecutorInstrumentation.class)); - extension = factory.constructInstance(ContractCoreExtension.class); - } - @Test - void shouldProvidePolicyArchive(ServiceExtensionContext context) { + void shouldProvideManagers(ContractCoreExtension extension, ServiceExtensionContext context) { extension.initialize(context); - assertThat(context.hasService(PolicyArchive.class)).isTrue(); - assertThat(context.getService(PolicyArchive.class)).isNotNull().isInstanceOf(PolicyArchiveImpl.class); + assertThat(context.hasService(ConsumerContractNegotiationManager.class)).isTrue(); + assertThat(context.hasService(ProviderContractNegotiationManager.class)).isTrue(); } } diff --git a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListenerTest.java b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListenerTest.java index be8e0ff0d1e..e75542de45f 100644 --- a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListenerTest.java +++ b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/listener/ContractNegotiationEventListenerTest.java @@ -16,9 +16,7 @@ import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationAccepted; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationAgreed; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationDeclined; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationEvent; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFailed; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFinalized; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationInitiated; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationOffered; @@ -72,7 +70,6 @@ void initiated_shouldDispatchEvent() { .build(); assertEvent(eventPayload); - } @Test @@ -151,45 +148,6 @@ void terminated_shouldDispatchEvent() { } - @Test - void declined_shouldDispatchEvent() { - var listener = new ContractNegotiationEventListener(router, clock); - var negotiation = getNegotiation("id"); - - listener.declined(negotiation); - - var eventPayload = ContractNegotiationDeclined.Builder.newInstance() - .contractNegotiationId(negotiation.getId()) - .counterPartyAddress(negotiation.getCounterPartyAddress()) - .protocol(negotiation.getProtocol()) - .callbackAddresses(negotiation.getCallbackAddresses()) - .counterPartyId(negotiation.getCounterPartyId()) - .build(); - - assertEvent(eventPayload); - - } - - @Test - void failed_shouldDispatchEvent() { - var listener = new ContractNegotiationEventListener(router, clock); - var negotiation = getNegotiation("id"); - - listener.failed(negotiation); - - var eventPayload = ContractNegotiationFailed.Builder.newInstance() - .contractNegotiationId(negotiation.getId()) - .counterPartyAddress(negotiation.getCounterPartyAddress()) - .protocol(negotiation.getProtocol()) - .callbackAddresses(negotiation.getCallbackAddresses()) - .counterPartyId(negotiation.getCounterPartyId()) - .build(); - - assertEvent(eventPayload); - - } - - @Test void agreed_shouldDispatchEvent() { var listener = new ContractNegotiationEventListener(router, clock); @@ -257,7 +215,6 @@ void finalized_shouldDispatchEvent() { } - private ContractNegotiation getNegotiation(String id) { return getNegotiationBuilder(id).build(); } diff --git a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java index b655eb5204e..6e5e316b469 100644 --- a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java +++ b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ConsumerContractNegotiationManagerImplTest.java @@ -16,6 +16,7 @@ package org.eclipse.edc.connector.contract.negotiation; import org.eclipse.edc.connector.contract.observe.ContractNegotiationObservableImpl; +import org.eclipse.edc.connector.contract.spi.negotiation.ContractNegotiationPendingGuard; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationListener; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; @@ -68,6 +69,7 @@ import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.VERIFIED; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.VERIFYING; import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; +import static org.eclipse.edc.spi.persistence.StateEntityStore.isNotPending; import static org.eclipse.edc.spi.response.ResponseStatus.FATAL_ERROR; import static org.mockito.AdditionalMatchers.and; import static org.mockito.AdditionalMatchers.aryEq; @@ -92,7 +94,8 @@ class ConsumerContractNegotiationManagerImplTest { private final ContractNegotiationListener listener = mock(); private final ProtocolWebhook protocolWebhook = mock(); private final String protocolWebhookUrl = "http://protocol.webhook/url"; - private ConsumerContractNegotiationManagerImpl negotiationManager; + private final ContractNegotiationPendingGuard pendingGuard = mock(); + private ConsumerContractNegotiationManagerImpl manager; @BeforeEach void setUp() { @@ -101,7 +104,7 @@ void setUp() { var observable = new ContractNegotiationObservableImpl(); observable.registerListener(listener); - negotiationManager = ConsumerContractNegotiationManagerImpl.Builder.newInstance() + manager = ConsumerContractNegotiationManagerImpl.Builder.newInstance() .participantId(PARTICIPANT_ID) .dispatcherRegistry(dispatcherRegistry) .monitor(mock(Monitor.class)) @@ -110,6 +113,7 @@ void setUp() { .policyStore(policyStore) .entityRetryProcessConfiguration(new EntityRetryProcessConfiguration(RETRY_LIMIT, () -> new ExponentialWaitStrategy(0L))) .protocolWebhook(protocolWebhook) + .pendingGuard(pendingGuard) .build(); } @@ -127,7 +131,7 @@ void initiate_shouldSaveNewNegotiationInInitialState() { .build())) .build(); - var result = negotiationManager.initiate(request); + var result = manager.initiate(request); assertThat(result.succeeded()).isTrue(); verify(store).save(argThat(negotiation -> @@ -148,7 +152,7 @@ void initial_shouldTransitionRequesting() { var negotiation = contractNegotiationBuilder().state(INITIAL.code()).build(); when(store.nextNotLeased(anyInt(), stateIs(INITIAL.code()))).thenReturn(List.of(negotiation)).thenReturn(emptyList()); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == REQUESTING.code())); @@ -163,7 +167,7 @@ void requesting_shouldSendOfferAndTransitionRequested() { when(store.findById(negotiation.getId())).thenReturn(negotiation); when(protocolWebhook.url()).thenReturn(protocolWebhookUrl); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == REQUESTED.code())); @@ -179,7 +183,7 @@ void accepting_shouldSendAgreementAndTransitionToApproved() { when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == ACCEPTED.code())); @@ -194,7 +198,7 @@ void agreed_shouldTransitionToVerifying() { when(store.nextNotLeased(anyInt(), stateIs(AGREED.code()))).thenReturn(List.of(negotiation)).thenReturn(emptyList()); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == VERIFYING.code())); @@ -209,7 +213,7 @@ void verifying_shouldSendMessageAndTransitionToVerified() { when(store.findById(negotiation.getId())).thenReturn(negotiation); when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == VERIFIED.code())); @@ -225,7 +229,7 @@ void terminating_shouldSendRejectionAndTransitionTerminated() { when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == TERMINATED.code())); @@ -234,6 +238,24 @@ void terminating_shouldSendRejectionAndTransitionTerminated() { }); } + @Test + void pendingGuard_shouldSetTheTransferPending_whenPendingGuardMatches() { + when(pendingGuard.test(any())).thenReturn(true); + var process = contractNegotiationBuilder().state(VERIFYING.code()).build(); + when(store.nextNotLeased(anyInt(), stateIs(VERIFYING.code()))).thenReturn(List.of(process)).thenReturn(emptyList()); + + manager.start(); + + await().untilAsserted(() -> { + verify(pendingGuard).test(any()); + var captor = ArgumentCaptor.forClass(ContractNegotiation.class); + verify(store).save(captor.capture()); + var saved = captor.getValue(); + assertThat(saved.getState()).isEqualTo(VERIFYING.code()); + assertThat(saved.isPending()).isTrue(); + }); + } + @ParameterizedTest @ArgumentsSource(DispatchFailureArguments.class) void dispatchException(ContractNegotiationStates starting, ContractNegotiationStates ending, CompletableFuture> result, UnaryOperator builderEnricher) { @@ -242,7 +264,7 @@ void dispatchException(ContractNegotiationStates starting, ContractNegotiationSt when(dispatcherRegistry.dispatch(any(), any())).thenReturn(result); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { var captor = ArgumentCaptor.forClass(ContractNegotiation.class); @@ -255,7 +277,7 @@ void dispatchException(ContractNegotiationStates starting, ContractNegotiationSt } private Criterion[] stateIs(int state) { - return aryEq(new Criterion[]{ hasState(state), new Criterion("type", "=", "CONSUMER") }); + return aryEq(new Criterion[]{ hasState(state), isNotPending(), new Criterion("type", "=", "CONSUMER") }); } private ContractNegotiation.Builder contractNegotiationBuilder() { diff --git a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImplTest.java b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImplTest.java index 7534e9ec02b..c5999525d05 100644 --- a/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImplTest.java +++ b/core/control-plane/contract-core/src/test/java/org/eclipse/edc/connector/contract/negotiation/ProviderContractNegotiationManagerImplTest.java @@ -17,6 +17,7 @@ import org.eclipse.edc.connector.contract.observe.ContractNegotiationObservableImpl; import org.eclipse.edc.connector.contract.spi.ContractId; +import org.eclipse.edc.connector.contract.spi.negotiation.ContractNegotiationPendingGuard; import org.eclipse.edc.connector.contract.spi.negotiation.observe.ContractNegotiationListener; import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; @@ -68,6 +69,7 @@ import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.TERMINATING; import static org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiationStates.VERIFIED; import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; +import static org.eclipse.edc.spi.persistence.StateEntityStore.isNotPending; import static org.eclipse.edc.spi.response.ResponseStatus.FATAL_ERROR; import static org.mockito.AdditionalMatchers.and; import static org.mockito.AdditionalMatchers.aryEq; @@ -89,13 +91,14 @@ class ProviderContractNegotiationManagerImplTest { private final RemoteMessageDispatcherRegistry dispatcherRegistry = mock(RemoteMessageDispatcherRegistry.class); private final PolicyDefinitionStore policyStore = mock(PolicyDefinitionStore.class); private final ContractNegotiationListener listener = mock(ContractNegotiationListener.class); - private ProviderContractNegotiationManagerImpl negotiationManager; + private final ContractNegotiationPendingGuard pendingGuard = mock(); + private ProviderContractNegotiationManagerImpl manager; @BeforeEach void setUp() { var observable = new ContractNegotiationObservableImpl(); observable.registerListener(listener); - negotiationManager = ProviderContractNegotiationManagerImpl.Builder.newInstance() + manager = ProviderContractNegotiationManagerImpl.Builder.newInstance() .participantId(PROVIDER_ID) .dispatcherRegistry(dispatcherRegistry) .monitor(mock(Monitor.class)) @@ -103,6 +106,7 @@ void setUp() { .store(store) .policyStore(policyStore) .entityRetryProcessConfiguration(new EntityRetryProcessConfiguration(RETRY_LIMIT, () -> new ExponentialWaitStrategy(0L))) + .pendingGuard(pendingGuard) .build(); } @@ -113,7 +117,7 @@ void offering_shouldSendOfferAndTransitionToOffered() { when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == OFFERED.code())); @@ -128,7 +132,7 @@ void requested_shouldTransitionToAgreeing() { when(store.nextNotLeased(anyInt(), stateIs(REQUESTED.code()))).thenReturn(List.of(negotiation)).thenReturn(emptyList()); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == AGREEING.code())); @@ -142,7 +146,7 @@ void verified_shouldTransitionToFinalizing() { when(store.nextNotLeased(anyInt(), stateIs(VERIFIED.code()))).thenReturn(List.of(negotiation)).thenReturn(emptyList()); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == FINALIZING.code())); @@ -162,7 +166,7 @@ void agreeing_shouldSendAgreementAndTransitionToConfirmed() { when(store.findById(negotiation.getId())).thenReturn(negotiation); when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).id("policyId").build()); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == AGREED.code())); @@ -178,7 +182,7 @@ void finalizing_shouldSendMessageAndTransitionToFinalized() { when(store.findById(negotiation.getId())).thenReturn(negotiation); when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == FINALIZED.code())); @@ -194,7 +198,7 @@ void terminating_shouldSendMessageAndTransitionTerminated() { when(dispatcherRegistry.dispatch(any(), any())).thenReturn(completedFuture(StatusResult.success("any"))); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { verify(store).save(argThat(p -> p.getState() == TERMINATED.code())); @@ -203,6 +207,24 @@ void terminating_shouldSendMessageAndTransitionTerminated() { }); } + @Test + void pendingGuard_shouldSetTheTransferPending_whenPendingGuardMatches() { + when(pendingGuard.test(any())).thenReturn(true); + var process = contractNegotiationBuilder().state(AGREEING.code()).build(); + when(store.nextNotLeased(anyInt(), stateIs(AGREEING.code()))).thenReturn(List.of(process)).thenReturn(emptyList()); + + manager.start(); + + await().untilAsserted(() -> { + verify(pendingGuard).test(any()); + var captor = ArgumentCaptor.forClass(ContractNegotiation.class); + verify(store).save(captor.capture()); + var saved = captor.getValue(); + assertThat(saved.getState()).isEqualTo(AGREEING.code()); + assertThat(saved.isPending()).isTrue(); + }); + } + @ParameterizedTest @ArgumentsSource(DispatchFailureArguments.class) void dispatchException(ContractNegotiationStates starting, ContractNegotiationStates ending, CompletableFuture> result, UnaryOperator builderEnricher) { @@ -211,7 +233,7 @@ void dispatchException(ContractNegotiationStates starting, ContractNegotiationSt when(dispatcherRegistry.dispatch(any(), any())).thenReturn(result); when(store.findById(negotiation.getId())).thenReturn(negotiation); - negotiationManager.start(); + manager.start(); await().untilAsserted(() -> { var captor = ArgumentCaptor.forClass(ContractNegotiation.class); @@ -253,7 +275,7 @@ private ContractOffer contractOffer() { } private Criterion[] stateIs(int state) { - return aryEq(new Criterion[]{ hasState(state), new Criterion("type", "=", "PROVIDER") }); + return aryEq(new Criterion[]{ hasState(state), isNotPending(), new Criterion("type", "=", "PROVIDER") }); } private static class DispatchFailureArguments implements ArgumentsProvider { diff --git a/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferCoreExtension.java b/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferCoreExtension.java index 51d309fa915..4de2ebee189 100644 --- a/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferCoreExtension.java +++ b/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferCoreExtension.java @@ -20,16 +20,12 @@ import org.eclipse.edc.connector.transfer.command.handlers.DeprovisionCompleteCommandHandler; import org.eclipse.edc.connector.transfer.edr.DataAddressToEndpointDataReferenceTransformer; import org.eclipse.edc.connector.transfer.edr.EndpointDataReferenceReceiverRegistryImpl; -import org.eclipse.edc.connector.transfer.flow.DataFlowManagerImpl; import org.eclipse.edc.connector.transfer.listener.TransferProcessEventListener; -import org.eclipse.edc.connector.transfer.observe.TransferProcessObservableImpl; -import org.eclipse.edc.connector.transfer.process.StatusCheckerRegistryImpl; import org.eclipse.edc.connector.transfer.process.TransferProcessManagerImpl; import org.eclipse.edc.connector.transfer.provision.DeprovisionResponsesHandler; -import org.eclipse.edc.connector.transfer.provision.ProvisionManagerImpl; import org.eclipse.edc.connector.transfer.provision.ProvisionResponsesHandler; -import org.eclipse.edc.connector.transfer.provision.ResourceManifestGeneratorImpl; import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; +import org.eclipse.edc.connector.transfer.spi.TransferProcessPendingGuard; import org.eclipse.edc.connector.transfer.spi.edr.EndpointDataReferenceReceiverRegistry; import org.eclipse.edc.connector.transfer.spi.event.TransferProcessStarted; import org.eclipse.edc.connector.transfer.spi.flow.DataFlowManager; @@ -42,7 +38,6 @@ import org.eclipse.edc.connector.transfer.spi.types.DataRequest; import org.eclipse.edc.connector.transfer.spi.types.DeprovisionedResource; import org.eclipse.edc.connector.transfer.spi.types.ProvisionedContentResource; -import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.runtime.metamodel.annotation.CoreExtension; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -62,6 +57,7 @@ import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.statemachine.retry.EntityRetryProcessConfiguration; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.jetbrains.annotations.NotNull; import java.time.Clock; @@ -69,9 +65,7 @@ * Provides core data transfer services to the system. */ @CoreExtension -@Provides({ StatusCheckerRegistry.class, ResourceManifestGenerator.class, TransferProcessManager.class, - TransferProcessObservable.class, DataFlowManager.class, ProvisionManager.class, - EndpointDataReferenceReceiverRegistry.class }) +@Provides({ TransferProcessManager.class, EndpointDataReferenceReceiverRegistry.class }) @Extension(value = TransferCoreExtension.NAME) public class TransferCoreExtension implements ServiceExtension { @@ -97,6 +91,21 @@ public class TransferCoreExtension implements ServiceExtension { @Inject private TransferProcessStore transferProcessStore; + @Inject + private DataFlowManager dataFlowManager; + + @Inject + private StatusCheckerRegistry statusCheckerRegistry; + + @Inject + private ResourceManifestGenerator resourceManifestGenerator; + + @Inject + private ProvisionManager provisionManager; + + @Inject + private TransferProcessObservable observable; + @Inject private PolicyArchive policyArchive; @@ -118,9 +127,6 @@ public class TransferCoreExtension implements ServiceExtension { @Inject private Clock clock; - @Inject - private PolicyEngine policyEngine; - @Inject private TypeManager typeManager; @@ -133,6 +139,9 @@ public class TransferCoreExtension implements ServiceExtension { @Inject private ProtocolWebhook protocolWebhook; + @Inject + private TransferProcessPendingGuard pendingGuard; + private TransferProcessManagerImpl processManager; @Override @@ -146,18 +155,6 @@ public void initialize(ServiceExtensionContext context) { registerTypes(typeManager); - var dataFlowManager = new DataFlowManagerImpl(); - context.registerService(DataFlowManager.class, dataFlowManager); - - var manifestGenerator = new ResourceManifestGeneratorImpl(policyEngine); - context.registerService(ResourceManifestGenerator.class, manifestGenerator); - - var statusCheckerRegistry = new StatusCheckerRegistryImpl(); - context.registerService(StatusCheckerRegistry.class, statusCheckerRegistry); - - var provisionManager = new ProvisionManagerImpl(monitor); - context.registerService(ProvisionManager.class, provisionManager); - var iterationWaitMillis = context.getSetting(TRANSFER_STATE_MACHINE_ITERATION_WAIT_MILLIS, DEFAULT_ITERATION_WAIT); var waitStrategy = context.hasService(TransferWaitStrategy.class) ? context.getService(TransferWaitStrategy.class) : new ExponentialWaitStrategy(iterationWaitMillis); @@ -166,24 +163,17 @@ public void initialize(ServiceExtensionContext context) { var endpointDataReferenceReceiverRegistry = new EndpointDataReferenceReceiverRegistryImpl(typeTransformerRegistry); context.registerService(EndpointDataReferenceReceiverRegistry.class, endpointDataReferenceReceiverRegistry); - - // Integration with the new DSP protocol eventRouter.register(TransferProcessStarted.class, endpointDataReferenceReceiverRegistry); - var observable = new TransferProcessObservableImpl(); - context.registerService(TransferProcessObservable.class, observable); - observable.registerListener(new TransferProcessEventListener(eventRouter, clock)); - var retryLimit = context.getSetting(TRANSFER_SEND_RETRY_LIMIT, DEFAULT_SEND_RETRY_LIMIT); - var retryBaseDelay = context.getSetting(TRANSFER_SEND_RETRY_BASE_DELAY_MS, DEFAULT_SEND_RETRY_BASE_DELAY); - var entityRetryProcessConfiguration = new EntityRetryProcessConfiguration(retryLimit, () -> new ExponentialWaitStrategy(retryBaseDelay)); + var entityRetryProcessConfiguration = getEntityRetryProcessConfiguration(context); var provisionResponsesHandler = new ProvisionResponsesHandler(observable, monitor, vault, typeManager); var deprovisionResponsesHandler = new DeprovisionResponsesHandler(observable, monitor, vault); processManager = TransferProcessManagerImpl.Builder.newInstance() .waitStrategy(waitStrategy) - .manifestGenerator(manifestGenerator) + .manifestGenerator(resourceManifestGenerator) .dataFlowManager(dataFlowManager) .provisionManager(provisionManager) .dispatcherRegistry(dispatcherRegistry) @@ -202,6 +192,7 @@ public void initialize(ServiceExtensionContext context) { .protocolWebhook(protocolWebhook) .provisionResponsesHandler(provisionResponsesHandler) .deprovisionResponsesHandler(deprovisionResponsesHandler) + .pendingGuard(pendingGuard) .build(); context.registerService(TransferProcessManager.class, processManager); @@ -222,6 +213,13 @@ public void shutdown() { } } + @NotNull + private EntityRetryProcessConfiguration getEntityRetryProcessConfiguration(ServiceExtensionContext context) { + var retryLimit = context.getSetting(TRANSFER_SEND_RETRY_LIMIT, DEFAULT_SEND_RETRY_LIMIT); + var retryBaseDelay = context.getSetting(TRANSFER_SEND_RETRY_BASE_DELAY_MS, DEFAULT_SEND_RETRY_BASE_DELAY); + return new EntityRetryProcessConfiguration(retryLimit, () -> new ExponentialWaitStrategy(retryBaseDelay)); + } + private void registerTypes(TypeManager typeManager) { typeManager.registerTypes(DataRequest.class); typeManager.registerTypes(ProvisionedContentResource.class); diff --git a/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferProcessDefaultServicesExtension.java b/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferProcessDefaultServicesExtension.java new file mode 100644 index 00000000000..1705313a4d1 --- /dev/null +++ b/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/TransferProcessDefaultServicesExtension.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.transfer; + +import org.eclipse.edc.connector.transfer.flow.DataFlowManagerImpl; +import org.eclipse.edc.connector.transfer.observe.TransferProcessObservableImpl; +import org.eclipse.edc.connector.transfer.process.StatusCheckerRegistryImpl; +import org.eclipse.edc.connector.transfer.provision.ProvisionManagerImpl; +import org.eclipse.edc.connector.transfer.provision.ResourceManifestGeneratorImpl; +import org.eclipse.edc.connector.transfer.spi.TransferProcessPendingGuard; +import org.eclipse.edc.connector.transfer.spi.flow.DataFlowManager; +import org.eclipse.edc.connector.transfer.spi.observe.TransferProcessObservable; +import org.eclipse.edc.connector.transfer.spi.provision.ProvisionManager; +import org.eclipse.edc.connector.transfer.spi.provision.ResourceManifestGenerator; +import org.eclipse.edc.connector.transfer.spi.status.StatusCheckerRegistry; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +@Extension(value = TransferProcessDefaultServicesExtension.NAME) +public class TransferProcessDefaultServicesExtension implements ServiceExtension { + + public static final String NAME = "Transfer Process Default Services"; + + @Inject + private PolicyEngine policyEngine; + + @Override + public String name() { + return NAME; + } + + @Provider + public DataFlowManager dataFlowManager() { + return new DataFlowManagerImpl(); + } + + @Provider + public ResourceManifestGenerator resourceManifestGenerator() { + return new ResourceManifestGeneratorImpl(policyEngine); + } + + @Provider + public StatusCheckerRegistry statusCheckerRegistry() { + return new StatusCheckerRegistryImpl(); + } + + @Provider + public ProvisionManager provisionManager(ServiceExtensionContext context) { + return new ProvisionManagerImpl(context.getMonitor()); + } + + @Provider + public TransferProcessObservable transferProcessObservable() { + return new TransferProcessObservableImpl(); + } + + @Provider(isDefault = true) + public TransferProcessPendingGuard pendingGuard() { + return it -> false; + } + +} diff --git a/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImpl.java b/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImpl.java index a01f21ebb6a..9dd7683e7fa 100644 --- a/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImpl.java +++ b/core/control-plane/transfer-core/src/main/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImpl.java @@ -21,6 +21,7 @@ import org.eclipse.edc.connector.transfer.provision.DeprovisionResponsesHandler; import org.eclipse.edc.connector.transfer.provision.ProvisionResponsesHandler; import org.eclipse.edc.connector.transfer.spi.TransferProcessManager; +import org.eclipse.edc.connector.transfer.spi.TransferProcessPendingGuard; import org.eclipse.edc.connector.transfer.spi.flow.DataFlowManager; import org.eclipse.edc.connector.transfer.spi.observe.TransferProcessObservable; import org.eclipse.edc.connector.transfer.spi.observe.TransferProcessStartedData; @@ -45,6 +46,7 @@ import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.protocol.ProtocolWebhook; +import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.response.StatusResult; import org.eclipse.edc.spi.retry.ExponentialWaitStrategy; import org.eclipse.edc.spi.retry.WaitStrategy; @@ -52,8 +54,9 @@ import org.eclipse.edc.spi.system.ExecutorInstrumentation; import org.eclipse.edc.spi.telemetry.Telemetry; import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.statemachine.Processor; +import org.eclipse.edc.statemachine.ProcessorImpl; import org.eclipse.edc.statemachine.StateMachineManager; -import org.eclipse.edc.statemachine.StateProcessorImpl; import org.eclipse.edc.statemachine.retry.EntityRetryProcessConfiguration; import org.eclipse.edc.statemachine.retry.EntityRetryProcessFactory; import org.jetbrains.annotations.NotNull; @@ -83,6 +86,7 @@ import static org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates.STARTING; import static org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates.TERMINATING; import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; +import static org.eclipse.edc.spi.persistence.StateEntityStore.isNotPending; import static org.eclipse.edc.spi.types.domain.DataAddress.EDC_DATA_ADDRESS_SECRET; /** @@ -129,6 +133,7 @@ public class TransferProcessManagerImpl implements TransferProcessManager { private ProtocolWebhook protocolWebhook; private ProvisionResponsesHandler provisionResponsesHandler; private DeprovisionResponsesHandler deprovisionResponsesHandler; + private TransferProcessPendingGuard pendingGuard = tp -> false; private TransferProcessManagerImpl() { } @@ -520,9 +525,18 @@ private boolean processDeprovisioning(TransferProcess process) { .execute("deprovisioning"); } - private StateProcessorImpl processTransfersInState(TransferProcessStates state, Function function) { - var functionWithTraceContext = telemetry.contextPropagationMiddleware(function); - return new StateProcessorImpl<>(() -> transferProcessStore.nextNotLeased(batchSize, hasState(state.code())), functionWithTraceContext); + private Processor processTransfersInState(TransferProcessStates state, Function function) { + var filter = new Criterion[] { hasState(state.code()), isNotPending() }; + return ProcessorImpl.Builder.newInstance(() -> transferProcessStore.nextNotLeased(batchSize, filter)) + .process(telemetry.contextPropagationMiddleware(function)) + .guard(pendingGuard, this::setPending) + .build(); + } + + private boolean setPending(TransferProcess transferProcess) { + transferProcess.setPending(true); + update(transferProcess); + return true; } private void transitionToProvisioning(TransferProcess process) { @@ -732,6 +746,11 @@ public Builder deprovisionResponsesHandler(DeprovisionResponsesHandler deprovisi return this; } + public Builder pendingGuard(TransferProcessPendingGuard pendingGuard) { + manager.pendingGuard = pendingGuard; + return this; + } + public TransferProcessManagerImpl build() { Objects.requireNonNull(manager.manifestGenerator, "manifestGenerator cannot be null"); Objects.requireNonNull(manager.provisionManager, "provisionManager cannot be null"); diff --git a/core/control-plane/transfer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/control-plane/transfer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 03668de5575..65bd1b5ffe3 100644 --- a/core/control-plane/transfer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/core/control-plane/transfer-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -14,4 +14,5 @@ org.eclipse.edc.connector.transfer.TransferCoreExtension org.eclipse.edc.connector.transfer.TransferProcessCommandExtension +org.eclipse.edc.connector.transfer.TransferProcessDefaultServicesExtension diff --git a/core/control-plane/transfer-core/src/test/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImplTest.java b/core/control-plane/transfer-core/src/test/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImplTest.java index cda9966968b..c16e12cba40 100644 --- a/core/control-plane/transfer-core/src/test/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImplTest.java +++ b/core/control-plane/transfer-core/src/test/java/org/eclipse/edc/connector/transfer/process/TransferProcessManagerImplTest.java @@ -17,13 +17,13 @@ package org.eclipse.edc.connector.transfer.process; import org.eclipse.edc.connector.policy.spi.store.PolicyArchive; -import org.eclipse.edc.connector.transfer.TestProvisionedContentResource; import org.eclipse.edc.connector.transfer.TestProvisionedDataDestinationResource; import org.eclipse.edc.connector.transfer.TestResourceDefinition; import org.eclipse.edc.connector.transfer.TokenTestProvisionResource; import org.eclipse.edc.connector.transfer.observe.TransferProcessObservableImpl; import org.eclipse.edc.connector.transfer.provision.DeprovisionResponsesHandler; import org.eclipse.edc.connector.transfer.provision.ProvisionResponsesHandler; +import org.eclipse.edc.connector.transfer.spi.TransferProcessPendingGuard; import org.eclipse.edc.connector.transfer.spi.flow.DataFlowManager; import org.eclipse.edc.connector.transfer.spi.observe.TransferProcessListener; import org.eclipse.edc.connector.transfer.spi.provision.ProvisionManager; @@ -48,7 +48,6 @@ import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.asset.DataAddressResolver; import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry; -import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.protocol.ProtocolWebhook; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.response.ResponseStatus; @@ -96,6 +95,7 @@ import static org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates.TERMINATED; import static org.eclipse.edc.connector.transfer.spi.types.TransferProcessStates.TERMINATING; import static org.eclipse.edc.spi.persistence.StateEntityStore.hasState; +import static org.eclipse.edc.spi.persistence.StateEntityStore.isNotPending; import static org.eclipse.edc.spi.response.ResponseStatus.FATAL_ERROR; import static org.eclipse.edc.spi.types.domain.DataAddress.EDC_DATA_ADDRESS_SECRET; import static org.mockito.AdditionalMatchers.aryEq; @@ -139,6 +139,7 @@ class TransferProcessManagerImplTest { private final ProvisionResponsesHandler provisionResponsesHandler = mock(); private final DeprovisionResponsesHandler deprovisionResponsesHandler = mock(); private final String protocolWebhookUrl = "http://protocol.webhook/url"; + private final TransferProcessPendingGuard pendingGuard = mock(); private TransferProcessManagerImpl manager; @@ -156,7 +157,7 @@ void setup() { .batchSize(TRANSFER_MANAGER_BATCHSIZE) .dispatcherRegistry(dispatcherRegistry) .manifestGenerator(manifestGenerator) - .monitor(mock(Monitor.class)) + .monitor(mock()) .clock(clock) .statusCheckerRegistry(statusCheckerRegistry) .observable(observable) @@ -168,6 +169,7 @@ void setup() { .protocolWebhook(protocolWebhook) .provisionResponsesHandler(provisionResponsesHandler) .deprovisionResponsesHandler(deprovisionResponsesHandler) + .pendingGuard(pendingGuard) .build(); } @@ -716,6 +718,23 @@ void deprovisioning_shouldNotInvokePostActions_whenResponsesHandlerCannotHandle( }); } + @Test + void pendingGuard_shouldSetTheTransferPending_whenPendingGuardMatches() { + when(pendingGuard.test(any())).thenReturn(true); + var process = createTransferProcessBuilder(STARTING).build(); + when(transferProcessStore.nextNotLeased(anyInt(), stateIs(STARTING.code()))).thenReturn(List.of(process)).thenReturn(emptyList()); + + manager.start(); + + await().untilAsserted(() -> { + var captor = ArgumentCaptor.forClass(TransferProcess.class); + verify(transferProcessStore).save(captor.capture()); + var saved = captor.getValue(); + assertThat(saved.getState()).isEqualTo(STARTING.code()); + assertThat(saved.isPending()).isTrue(); + }); + } + @ParameterizedTest @ArgumentsSource(DispatchFailureArguments.class) void dispatchFailure(TransferProcessStates starting, TransferProcessStates ending, CompletableFuture> result, UnaryOperator builderEnricher) { @@ -737,18 +756,7 @@ void dispatchFailure(TransferProcessStates starting, TransferProcessStates endin } private Criterion[] stateIs(int state) { - return aryEq(new Criterion[]{ hasState(state) }); - } - - private TestProvisionedContentResource createTestProvisionedContentResource(String resourceDefinitionId) { - return TestProvisionedContentResource.Builder.newInstance() - .resourceName("test") - .id("1") - .transferProcessId("2") - .resourceDefinitionId(resourceDefinitionId) - .dataAddress(DataAddress.Builder.newInstance().type("test").build()) - .hasToken(true) - .build(); + return aryEq(new Criterion[]{ hasState(state), isNotPending() }); } private DataFlowResponse createDataFlowResponse() { diff --git a/docs/developer/state-machine.md b/docs/developer/state-machine.md index d335a8e51f1..7987ecd83ad 100644 --- a/docs/developer/state-machine.md +++ b/docs/developer/state-machine.md @@ -6,65 +6,102 @@ The framework currently manages a single execution thread. ## Collaborators -- The class which defines state machine instances. The `StatefulEntity` base class can be used to derive state machine entity classes. For example, a `ContractNegotiation` is a `StatefulEntity` for an EDC contract negotiation. -- The `StateMachineManager` which manages an execution thread that periodically gives a chance to state machines to progress their state. -- The state-machine specific Manager which instantiates the `StateMachineManager` and defines processors for each state for a given state machine class. For example, `ConsumerContractNegotiationManagerImpl` manages `ContractNegotiation`s in which the connector is a consumer. +- The class which defines state machine instances. The `StatefulEntity` base class can be used to derive state machine + entity classes. For example, a `ContractNegotiation` is a `StatefulEntity` for an EDC contract negotiation. +- The `StateMachineManager` which manages an execution thread that periodically gives a chance to state machines to + progress their state. +- The state-machine specific Manager which instantiates the `StateMachineManager` and defines processors for each state + for a given state machine class. For example, `ConsumerContractNegotiationManagerImpl` manages `ContractNegotiation`s + in which the connector is a consumer. - The `ServiceExtension` which manages the Manager's lifecycle. -- The Store which manages `StatefulEntity` persistence. `InMemoryStatefulEntityStore` provides a utility class to back in-memory implementations for testing. +- The Store which manages `StatefulEntity` persistence. `InMemoryStatefulEntityStore` provides a utility class to back + in-memory implementations for testing. ## State-machine specific Manager -The Manager manages the `StateMachineManager`'s lifecycle and defines the state machine's behavior, while the `StatefulEntity` is only concerned with the state machine's data. +The Manager manages the `StateMachineManager`'s lifecycle and defines the state machine's behavior, while the `StatefulEntity` +is only concerned with the state machine's data. Here's a prototypical Manager implementation: ```java -public void start() { // Called from ServiceExtension start() method - stateMachineManager = StateMachineManager.Builder.newInstance("state-machine-name", ...) - // Processors for non-terminal states - .processor(processEntitiesInState(STATE1, this::processState1)) - .processor(processEntitiesInState(STATE2, this::processState2)) - .processor(processEntitiesInState(STATE3, this::processState3)) - .processor(onCommands(this::processCommand)) - .build(); - stateMachineManager.start(); -} +public class EntityManager { + + public void start() { // Called from ServiceExtension start() method + stateMachineManager = StateMachineManager.Builder.newInstance("state-machine-name", monitor, executorInstrumentation, waitStrategy) + // Processors for non-terminal states + .processor(processEntitiesInState(STATE1, this::processState1)) + .processor(processEntitiesInState(STATE2, this::processState2)) + .processor(processEntitiesInState(STATE3, this::processState3)) + .build(); + + stateMachineManager.start(); + } -public void stop() { // Called from ServiceExtension shutdown() method - stateMachineManager.stop(); -} + public void stop() { // Called from ServiceExtension shutdown() method + if (stateMachineManager != null) { + stateMachineManager.stop(); + } + } + + private Processor processEntitiesInState(State state, Function function) { + var filter = new Criterion[] { hasState(state.code()), isNotPending() }; + return ProcessorImpl.Builder.newInstance(() -> transferProcessStore.nextNotLeased(batchSize, filter)) + .process(telemetry.contextPropagationMiddleware(function)) + .guard(pendingGuard, this::setPending) // a guard can be added to, e.g. put in pending certain entities based on the `pendingGuard` predicate + .build(); + } + + // Processor functions should return true only if the state machine has been updated + private boolean processState1(StatefulEntityImpl sm) { + if (conditionsForTransitionFromState1ToState2Apply(sm)) { + sm.transitionState2(); + store.save(sm); + return true; + } + return false; + } -private StateProcessorImpl processEntitiesInState(State state, Function function) { - return new StateProcessorImpl<>(() -> store.nextNotLeased(batchSize, hasState(state.code())), function); } +``` -// Processor functions should return true only if the state machine has been updated -private boolean processState1(StatefulEntityImpl sm) { - if (conditionsForTransitionFromState1ToState2Apply(sm)) { - sm.transitionState2(); - store.save(sm); - return true; +### Guards +On a state machine `Processor` a `Guard` can be specified. that's a way to have a custom flow based on a predicate that can +be extended, for example, to enable "external interactions" in the state machine, as user interactions. A `Guard` predicate +can be set on the processor with a specific process to be executed. This way when the predicate matches the entity can be +set to pending, making it "invisible" for the state machine, but still accessible and modifiable by users or external systems. + +`Guard` example: +```java +class EntityPendingGuard implements PendingGuard { + + // custom collaborators as other services + + boolean test(Entity entity) { + // additional logic + return entity.getState() = SPECIFIC_STATE.code() && otherCondition; // if true, the entity will be set as pending } - return false; + } -... ``` ## State-machine store -The Store which manages `StatefulEntity` persistence must persist entities in a storage system. In-memory implementations are provided for testing. +The Store which manages `StatefulEntity` persistence must persist entities in a storage system. In-memory implementations +are provided for testing. -Stores using persistent implementations must manage leases to support EDC clustered deployment. This ensures an entity is processed by only one EDC instance at a time (assuming processing is quicker than lease expiration). +Stores using persistent implementations must manage leases to support EDC clustered deployment. This ensures an entity is +processed by only one EDC instance at a time (assuming processing is quicker than lease expiration). ```java -public void save(StatefulEntityImpl instance) { - // persist instance - // release lease -} - -List nextNotLeased(int max, Criterion... criteria); { - // retrieve and lease at most limit instances that satisfy criteria +class EntityStore { + public void save(StatefulEntityImpl instance) { + // persist instance + // release lease + } + + List nextNotLeased(int max, Criterion... criteria); { + // retrieve and lease at most limit instances that satisfy criteria + } } - - ``` diff --git a/extensions/common/sql/sql-core/src/main/java/org/eclipse/edc/sql/statement/ColumnEntry.java b/extensions/common/sql/sql-core/src/main/java/org/eclipse/edc/sql/statement/ColumnEntry.java new file mode 100644 index 00000000000..0e59c7ca515 --- /dev/null +++ b/extensions/common/sql/sql-core/src/main/java/org/eclipse/edc/sql/statement/ColumnEntry.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.sql.statement; + +/** + * Represent a column and its value into a JDBC prepared statement. + * + * @param columnName the name of the column. + * @param value the value of the column, by default it will be '?' but it could be different. + */ +public record ColumnEntry(String columnName, String value) { + + public static ColumnEntry standardColumn(String columnName) { + return new ColumnEntry(columnName, "?"); + } + + public static ColumnEntry jsonColumn(String columnName, String jsonOperator) { + return new ColumnEntry(columnName, "?" + jsonOperator); + } + + public ColumnEntry append(ColumnEntry other) { + return new ColumnEntry(columnName + ", " + other.columnName, value + ", " + other.value); + } +} diff --git a/extensions/common/sql/sql-core/src/main/java/org/eclipse/edc/sql/statement/SqlExecuteStatement.java b/extensions/common/sql/sql-core/src/main/java/org/eclipse/edc/sql/statement/SqlExecuteStatement.java new file mode 100644 index 00000000000..c242103910a --- /dev/null +++ b/extensions/common/sql/sql-core/src/main/java/org/eclipse/edc/sql/statement/SqlExecuteStatement.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.sql.statement; + +import java.util.ArrayList; +import java.util.List; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static org.eclipse.edc.sql.statement.ColumnEntry.standardColumn; + +/** + * Helps in constructing SQL insert/update statements without having to deal with the SQL syntax directly + */ +public class SqlExecuteStatement { + + private final List columnEntries = new ArrayList<>(); + private final String jsonCastOperator; + + public SqlExecuteStatement(String jsonCastOperator) { + this.jsonCastOperator = jsonCastOperator; + } + + /** + * Create a new instance with the JSON cast operator + * + * @param jsonCastOperator the json cast operator. + * @return a new {@link SqlExecuteStatement}. + */ + public static SqlExecuteStatement newInstance(String jsonCastOperator) { + return new SqlExecuteStatement(jsonCastOperator); + } + + /** + * Add a new columnEntry + * + * @param columnEntry the columnEntry. + * @return the {@link SqlExecuteStatement}. + */ + public SqlExecuteStatement add(ColumnEntry columnEntry) { + columnEntries.add(columnEntry); + return this; + } + + /** + * Add a new standard column + * + * @param columnName the column name. + * @return the {@link SqlExecuteStatement}. + */ + public SqlExecuteStatement column(String columnName) { + columnEntries.add(standardColumn(columnName)); + return this; + } + + /** + * Add a new json column + * + * @param columnName the column name. + * @return the {@link SqlExecuteStatement}. + */ + public SqlExecuteStatement jsonColumn(String columnName) { + columnEntries.add(ColumnEntry.jsonColumn(columnName, jsonCastOperator)); + return this; + } + + /** + * Gives a SQL insert statement. + * + * @param tableName the table name. + * @return sql insert statement. + */ + public String insertInto(String tableName) { + var columnValues = columnEntries.stream().reduce(ColumnEntry::append).orElseThrow(); + + return format("INSERT INTO %s (%s) VALUES (%s);", tableName, columnValues.columnName(), columnValues.value()); + } + + + /** + * Gives a SQL update statement. + * + * @param tableName the table name. + * @param where the update field condition + * @return sql update statement. + */ + public String update(String tableName, ColumnEntry where) { + var statement = columnEntries.stream() + .map(it -> it.columnName() + "=" + it.value()) + .collect(joining(",")); + + return format("UPDATE %s SET %s WHERE %s=%s", tableName, statement, where.columnName(), where.value()); + } +} diff --git a/extensions/control-plane/api/control-plane-api-client/src/test/java/org/eclipse/edc/connector/api/client/transferprocess/TransferProcessHttpClientIntegrationTest.java b/extensions/control-plane/api/control-plane-api-client/src/test/java/org/eclipse/edc/connector/api/client/transferprocess/TransferProcessHttpClientIntegrationTest.java index d280a17bf47..f5a6f69d145 100644 --- a/extensions/control-plane/api/control-plane-api-client/src/test/java/org/eclipse/edc/connector/api/client/transferprocess/TransferProcessHttpClientIntegrationTest.java +++ b/extensions/control-plane/api/control-plane-api-client/src/test/java/org/eclipse/edc/connector/api/client/transferprocess/TransferProcessHttpClientIntegrationTest.java @@ -83,7 +83,7 @@ void setUp(EdcExtension extension) { void shouldCallTransferProcessApiWithComplete(TransferProcessStore store, DataPlaneManager manager, ControlPlaneApiUrl callbackUrl) { when(service.transfer(any())).thenReturn(completedFuture(StreamResult.success())); var id = "tp-id"; - store.updateOrCreate(createTransferProcess(id)); + store.save(createTransferProcess(id)); var dataFlowRequest = createDataFlowRequest(id, callbackUrl.get()); manager.initiateTransfer(dataFlowRequest); @@ -99,7 +99,7 @@ void shouldCallTransferProcessApiWithComplete(TransferProcessStore store, DataPl void shouldCallTransferProcessApiWithFailed(TransferProcessStore store, DataPlaneManager manager, ControlPlaneApiUrl callbackUrl) { when(service.transfer(any())).thenReturn(completedFuture(StreamResult.error("error"))); var id = "tp-id"; - store.updateOrCreate(createTransferProcess(id)); + store.save(createTransferProcess(id)); var dataFlowRequest = createDataFlowRequest(id, callbackUrl.get()); manager.initiateTransfer(dataFlowRequest); @@ -117,7 +117,7 @@ void shouldCallTransferProcessApiWithFailed(TransferProcessStore store, DataPlan void shouldCallTransferProcessApiWithException(TransferProcessStore store, DataPlaneManager manager, ControlPlaneApiUrl callbackUrl) { when(service.transfer(any())).thenReturn(failedFuture(new EdcException("error"))); var id = "tp-id"; - store.updateOrCreate(createTransferProcess(id)); + store.save(createTransferProcess(id)); var dataFlowRequest = createDataFlowRequest(id, callbackUrl.get()); manager.initiateTransfer(dataFlowRequest); diff --git a/extensions/control-plane/store/sql/contract-negotiation-store-sql/docs/schema.sql b/extensions/control-plane/store/sql/contract-negotiation-store-sql/docs/schema.sql index 24570850070..0fabd673e85 100644 --- a/extensions/control-plane/store/sql/contract-negotiation-store-sql/docs/schema.sql +++ b/extensions/control-plane/store/sql/contract-negotiation-store-sql/docs/schema.sql @@ -37,17 +37,17 @@ CREATE TABLE IF NOT EXISTS edc_contract_agreement CREATE TABLE IF NOT EXISTS edc_contract_negotiation ( - id VARCHAR NOT NULL + id VARCHAR NOT NULL CONSTRAINT contract_negotiation_pk PRIMARY KEY, - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, correlation_id VARCHAR, - counterparty_id VARCHAR NOT NULL, - counterparty_address VARCHAR NOT NULL, - protocol VARCHAR NOT NULL, - type VARCHAR NOT NULL, - state INTEGER DEFAULT 0 NOT NULL, + counterparty_id VARCHAR NOT NULL, + counterparty_address VARCHAR NOT NULL, + protocol VARCHAR NOT NULL, + type VARCHAR NOT NULL, + state INTEGER DEFAULT 0 NOT NULL, state_count INTEGER DEFAULT 0, state_timestamp BIGINT, error_detail VARCHAR, @@ -57,6 +57,7 @@ CREATE TABLE IF NOT EXISTS edc_contract_negotiation contract_offers JSON, callback_addresses JSON, trace_context JSON, + pending BOOLEAN DEFAULT FALSE, lease_id VARCHAR CONSTRAINT contract_negotiation_lease_lease_id_fk REFERENCES edc_lease diff --git a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/SqlContractNegotiationStore.java b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/SqlContractNegotiationStore.java index 96cd7298ba5..d2f14117e8f 100644 --- a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/SqlContractNegotiationStore.java +++ b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/SqlContractNegotiationStore.java @@ -172,7 +172,7 @@ public void delete(String negotiationId) { @Override public @NotNull List nextNotLeased(int max, Criterion... criteria) { return transactionContext.execute(() -> { - var filter = Arrays.stream(criteria).collect(toList()); + var filter = Arrays.stream(criteria).toList(); var querySpec = QuerySpec.Builder.newInstance().filter(filter).limit(max).build(); var statement = statements.createNegotiationsQuery(querySpec); statement.addWhereClause(statements.getNotLeasedFilter()); @@ -269,6 +269,7 @@ private void update(Connection connection, String negotiationId, ContractNegotia toJson(updatedValues.getTraceContext()), ofNullable(updatedValues.getContractAgreement()).map(ContractAgreement::getId).orElse(null), updatedValues.getUpdatedAt(), + updatedValues.isPending(), negotiationId); } @@ -281,7 +282,8 @@ private void insert(Connection connection, ContractNegotiation negotiation) { } var stmt = statements.getInsertNegotiationTemplate(); - queryExecutor.execute(connection, stmt, negotiation.getId(), + queryExecutor.execute(connection, stmt, + negotiation.getId(), negotiation.getCorrelationId(), negotiation.getCounterPartyId(), negotiation.getCounterPartyAddress(), @@ -296,9 +298,8 @@ private void insert(Connection connection, ContractNegotiation negotiation) { toJson(negotiation.getCallbackAddresses()), toJson(negotiation.getTraceContext()), negotiation.getCreatedAt(), - negotiation.getUpdatedAt()); - - + negotiation.getUpdatedAt(), + negotiation.isPending()); } private void upsertAgreement(ContractAgreement contractAgreement) { @@ -396,6 +397,7 @@ private ContractNegotiation mapContractNegotiation(ResultSet resultSet, ResultSe .type(ContractNegotiation.Type.valueOf(resultSet.getString(statements.getTypeColumn()))) .createdAt(resultSet.getLong(statements.getCreatedAtColumn())) .updatedAt(resultSet.getLong(statements.getUpdatedAtColumn())) + .pending(resultSet.getBoolean(statements.getPendingColumn())) .build(); } diff --git a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/BaseSqlDialectStatements.java b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/BaseSqlDialectStatements.java index d5dbddc9eab..165392de3a9 100644 --- a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/BaseSqlDialectStatements.java +++ b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/BaseSqlDialectStatements.java @@ -16,6 +16,8 @@ import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.sql.dialect.BaseSqlDialect; +import org.eclipse.edc.sql.statement.ColumnEntry; +import org.eclipse.edc.sql.statement.SqlExecuteStatement; import org.eclipse.edc.sql.translation.SqlQueryStatement; import static java.lang.String.format; @@ -38,20 +40,41 @@ public String getFindContractAgreementTemplate() { @Override public String getUpdateNegotiationTemplate() { - return format("UPDATE %s SET %s=?, %s=?, %s=?, %s=?, %s=?%s, %s=?%s, %s=?%s, %s=?, %s=? WHERE id = ?;", - getContractNegotiationTable(), getStateColumn(), getStateCountColumn(), getStateTimestampColumn(), - getErrorDetailColumn(), getContractOffersColumn(), getFormatJsonOperator(), getCallbackAddressesColumn(), getFormatJsonOperator(), getTraceContextColumn(), - getFormatJsonOperator(), getContractAgreementIdFkColumn(), getUpdatedAtColumn()); + return SqlExecuteStatement.newInstance(getFormatJsonOperator()) + .column(getStateColumn()) + .column(getStateCountColumn()) + .column(getStateTimestampColumn()) + .column(getErrorDetailColumn()) + .jsonColumn(getContractOffersColumn()) + .jsonColumn(getCallbackAddressesColumn()) + .jsonColumn(getTraceContextColumn()) + .column(getContractAgreementIdFkColumn()) + .column(getUpdatedAtColumn()) + .column(getPendingColumn()) + .update(getContractNegotiationTable(), ColumnEntry.standardColumn(getIdColumn())); } @Override public String getInsertNegotiationTemplate() { - return format("INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n" + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?%s, ?%s, ?%s, ?, ?); ", - getContractNegotiationTable(), getIdColumn(), getCorrelationIdColumn(), getCounterPartyIdColumn(), getCounterPartyAddressColumn(), getTypeColumn(), getProtocolColumn(), getStateColumn(), getStateCountColumn(), - getStateTimestampColumn(), getErrorDetailColumn(), getContractAgreementIdFkColumn(), getContractOffersColumn(), getCallbackAddressesColumn(), getTraceContextColumn(), getCreatedAtColumn(), getUpdatedAtColumn(), - getFormatJsonOperator(), getFormatJsonOperator(), getFormatJsonOperator() - ); + return SqlExecuteStatement.newInstance(getFormatJsonOperator()) + .column(getIdColumn()) + .column(getCorrelationIdColumn()) + .column(getCounterPartyIdColumn()) + .column(getCounterPartyAddressColumn()) + .column(getTypeColumn()) + .column(getProtocolColumn()) + .column(getStateColumn()) + .column(getStateCountColumn()) + .column(getStateTimestampColumn()) + .column(getErrorDetailColumn()) + .column(getContractAgreementIdFkColumn()) + .jsonColumn(getContractOffersColumn()) + .jsonColumn(getCallbackAddressesColumn()) + .jsonColumn(getTraceContextColumn()) + .column(getCreatedAtColumn()) + .column(getUpdatedAtColumn()) + .column(getPendingColumn()) + .insertInto(getContractNegotiationTable()); } @Override diff --git a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/ContractNegotiationStatements.java b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/ContractNegotiationStatements.java index 7fe27a52c49..cffb0239c7b 100644 --- a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/ContractNegotiationStatements.java +++ b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/ContractNegotiationStatements.java @@ -137,6 +137,10 @@ default String getUpdatedAtColumn() { return "updated_at"; } + default String getPendingColumn() { + return "pending"; + } + SqlQueryStatement createNegotiationsQuery(QuerySpec querySpec); SqlQueryStatement createAgreementsQuery(QuerySpec querySpec); diff --git a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/postgres/ContractNegotiationMapping.java b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/postgres/ContractNegotiationMapping.java index 2682e0b355e..dee4db397fe 100644 --- a/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/postgres/ContractNegotiationMapping.java +++ b/extensions/control-plane/store/sql/contract-negotiation-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/contractnegotiation/store/schema/postgres/ContractNegotiationMapping.java @@ -35,6 +35,7 @@ class ContractNegotiationMapping extends TranslationMapping { private static final String FIELD_ERRORDETAIL = "errorDetail"; private static final String FIELD_CONTRACT_AGREEMENT = "contractAgreement"; private static final String FIELD_TRACECONTEXT = "traceContext"; + private static final String FIELD_PENDING = "pending"; ContractNegotiationMapping(ContractNegotiationStatements statements) { @@ -47,9 +48,9 @@ class ContractNegotiationMapping extends TranslationMapping { add(FIELD_TYPE, statements.getTypeColumn()); add(FIELD_STATE, statements.getStateColumn()); add(FIELD_STATECOUNT, statements.getStateCountColumn()); - add(FIELD_STATETIMESTAMP, statements.getStateTimestampColumn()); add(FIELD_ERRORDETAIL, statements.getErrorDetailColumn()); + add(FIELD_PENDING, statements.getPendingColumn()); fieldMap.put(FIELD_CONTRACT_AGREEMENT, new ContractAgreementMapping(statements)); add(FIELD_TRACECONTEXT, statements.getTraceContextColumn()); diff --git a/extensions/control-plane/store/sql/transfer-process-store-sql/docs/schema.sql b/extensions/control-plane/store/sql/transfer-process-store-sql/docs/schema.sql index 23beb113827..84784a62685 100644 --- a/extensions/control-plane/store/sql/transfer-process-store-sql/docs/schema.sql +++ b/extensions/control-plane/store/sql/transfer-process-store-sql/docs/schema.sql @@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS edc_transfer_process deprovisioned_resources JSON, private_properties JSON, callback_addresses JSON, + pending BOOLEAN DEFAULT FALSE, lease_id VARCHAR CONSTRAINT transfer_process_lease_lease_id_fk REFERENCES edc_lease diff --git a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/SqlTransferProcessStore.java b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/SqlTransferProcessStore.java index 0097c576e17..ad9a6c01621 100644 --- a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/SqlTransferProcessStore.java +++ b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/SqlTransferProcessStore.java @@ -242,9 +242,11 @@ private Stream executeQuery(Connection connection, QuerySpec qu private void update(Connection conn, TransferProcess process, String existingDataRequestId) { var updateStmt = statements.getUpdateTransferProcessTemplate(); - queryExecutor.execute(conn, updateStmt, process.getState(), + queryExecutor.execute(conn, updateStmt, + process.getState(), process.getStateCount(), process.getStateTimestamp(), + process.getUpdatedAt(), toJson(process.getTraceContext()), process.getErrorDetail(), toJson(process.getResourceManifest()), @@ -252,7 +254,7 @@ private void update(Connection conn, TransferProcess process, String existingDat toJson(process.getContentDataAddress()), toJson(process.getDeprovisionedResources()), toJson(process.getCallbackAddresses()), - process.getUpdatedAt(), + process.isPending(), process.getId()); var newDr = process.getDataRequest(); @@ -307,7 +309,8 @@ private void insert(Connection conn, TransferProcess process) { process.getType().toString(), toJson(process.getDeprovisionedResources()), toJson(process.getPrivateProperties()), - toJson(process.getCallbackAddresses())); + toJson(process.getCallbackAddresses()), + process.isPending()); //insert DataRequest var dr = process.getDataRequest(); @@ -350,6 +353,7 @@ private TransferProcess mapTransferProcess(ResultSet resultSet) throws SQLExcept .callbackAddresses(fromJson(resultSet.getString(statements.getCallbackAddressesColumn()), new TypeReference<>() { })) .privateProperties(fromJson(resultSet.getString(statements.getPrivatePropertiesColumn()), getTypeRef())) + .pending(resultSet.getBoolean(statements.getPendingColumn())) .build(); } diff --git a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/BaseSqlDialectStatements.java b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/BaseSqlDialectStatements.java index 71a0adf4e5d..e7324fbe916 100644 --- a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/BaseSqlDialectStatements.java +++ b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/BaseSqlDialectStatements.java @@ -16,9 +16,11 @@ import org.eclipse.edc.connector.store.sql.transferprocess.store.schema.postgres.TransferProcessMapping; import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.statement.SqlExecuteStatement; import org.eclipse.edc.sql.translation.SqlQueryStatement; import static java.lang.String.format; +import static org.eclipse.edc.sql.statement.ColumnEntry.standardColumn; /** * Postgres-specific variants and implementations of the statements required for the TransferProcessStore @@ -51,16 +53,24 @@ public String getFindLeaseByEntityTemplate() { @Override public String getInsertStatement() { - return format("INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES (?, ?, ?, ?, ?, ?, ?%s, ?, ?%s, ?%s, ?%s, ?, ?%s, ?%s, ?%s);", - // keys - getTransferProcessTableName(), getIdColumn(), getStateColumn(), getStateCountColumn(), getStateTimestampColumn(), - getCreatedAtColumn(), getUpdatedAtColumn(), - getTraceContextColumn(), getErrorDetailColumn(), getResourceManifestColumn(), - getProvisionedResourceSetColumn(), getContentDataAddressColumn(), getTypeColumn(), getDeprovisionedResourcesColumn(), - getPrivatePropertiesColumn(), getCallbackAddressesColumn(), - // values - getFormatAsJsonOperator(), getFormatAsJsonOperator(), getFormatAsJsonOperator(), getFormatAsJsonOperator(), - getFormatAsJsonOperator(), getFormatAsJsonOperator(), getFormatAsJsonOperator()); + return SqlExecuteStatement.newInstance(getFormatAsJsonOperator()) + .column(getIdColumn()) + .column(getStateColumn()) + .column(getStateCountColumn()) + .column(getStateTimestampColumn()) + .column(getCreatedAtColumn()) + .column(getUpdatedAtColumn()) + .jsonColumn(getTraceContextColumn()) + .column(getErrorDetailColumn()) + .jsonColumn(getResourceManifestColumn()) + .jsonColumn(getProvisionedResourceSetColumn()) + .jsonColumn(getContentDataAddressColumn()) + .column(getTypeColumn()) + .jsonColumn(getDeprovisionedResourcesColumn()) + .jsonColumn(getPrivatePropertiesColumn()) + .jsonColumn(getCallbackAddressesColumn()) + .column(getPendingColumn()) + .insertInto(getTransferProcessTableName()); } @Override @@ -70,12 +80,20 @@ public String getDeleteTransferProcessTemplate() { @Override public String getUpdateTransferProcessTemplate() { - return format("UPDATE %s SET %s=?, %s=?, %s=?, %s=?%s, %s=?, %s=?%s, %s=?%s, %s=?%s, %s=?%s, %s=?%s, %s=? WHERE %s=?", - getTransferProcessTableName(), getStateColumn(), getStateCountColumn(), getStateTimestampColumn(), - getTraceContextColumn(), getFormatAsJsonOperator(), getErrorDetailColumn(), - getResourceManifestColumn(), getFormatAsJsonOperator(), getProvisionedResourceSetColumn(), getFormatAsJsonOperator(), - getContentDataAddressColumn(), getFormatAsJsonOperator(), getDeprovisionedResourcesColumn(), getFormatAsJsonOperator(), - getCallbackAddressesColumn(), getFormatAsJsonOperator(), getUpdatedAtColumn(), getIdColumn()); + return SqlExecuteStatement.newInstance(getFormatAsJsonOperator()) + .column(getStateColumn()) + .column(getStateCountColumn()) + .column(getStateTimestampColumn()) + .column(getUpdatedAtColumn()) + .jsonColumn(getTraceContextColumn()) + .column(getErrorDetailColumn()) + .jsonColumn(getResourceManifestColumn()) + .jsonColumn(getProvisionedResourceSetColumn()) + .jsonColumn(getContentDataAddressColumn()) + .jsonColumn(getDeprovisionedResourcesColumn()) + .jsonColumn(getCallbackAddressesColumn()) + .column(getPendingColumn()) + .update(getTransferProcessTableName(), standardColumn(getIdColumn())); } @Override @@ -104,4 +122,5 @@ public String getUpdateDataRequestTemplate() { public SqlQueryStatement createQuery(QuerySpec querySpec) { return new SqlQueryStatement(getSelectTemplate(), querySpec, new TransferProcessMapping(this)); } + } diff --git a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/TransferProcessStoreStatements.java b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/TransferProcessStoreStatements.java index 0a1c762860b..4167a03bc4d 100644 --- a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/TransferProcessStoreStatements.java +++ b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/TransferProcessStoreStatements.java @@ -142,6 +142,10 @@ default String getCallbackAddressesColumn() { return "callback_addresses"; } + default String getPendingColumn() { + return "pending"; + } + default String getFormatAsJsonOperator() { return BaseSqlDialect.getJsonCastOperator(); } diff --git a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/postgres/TransferProcessMapping.java b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/postgres/TransferProcessMapping.java index 7836b46a6c6..480cf7c820f 100644 --- a/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/postgres/TransferProcessMapping.java +++ b/extensions/control-plane/store/sql/transfer-process-store-sql/src/main/java/org/eclipse/edc/connector/store/sql/transferprocess/store/schema/postgres/TransferProcessMapping.java @@ -40,6 +40,7 @@ public class TransferProcessMapping extends TranslationMapping { private static final String FIELD_DEPROVISIONED_RESOURCES = "deprovisionedResources"; private static final String FIELD_PRIVATE_PROPERTIES = "privateProperties"; + private static final String FIELD_PENDING = "pending"; public TransferProcessMapping(TransferProcessStoreStatements statements) { @@ -57,5 +58,6 @@ public TransferProcessMapping(TransferProcessStoreStatements statements) { add(FIELD_PROVISIONED_RESOURCE_SET, new ProvisionedResourceSetMapping()); // using the alias instead of the actual column name to avoid name clashes. add(FIELD_DEPROVISIONED_RESOURCES, new JsonFieldMapping(PostgresDialectStatements.DEPROVISIONED_RESOURCES_ALIAS)); + add(FIELD_PENDING, statements.getPendingColumn()); } } diff --git a/extensions/control-plane/store/sql/transfer-process-store-sql/src/test/java/org/eclipse/edc/connector/store/sql/transferprocess/PostgresTransferProcessStoreTest.java b/extensions/control-plane/store/sql/transfer-process-store-sql/src/test/java/org/eclipse/edc/connector/store/sql/transferprocess/PostgresTransferProcessStoreTest.java index beb0717a6e1..b1890e8310e 100644 --- a/extensions/control-plane/store/sql/transfer-process-store-sql/src/test/java/org/eclipse/edc/connector/store/sql/transferprocess/PostgresTransferProcessStoreTest.java +++ b/extensions/control-plane/store/sql/transfer-process-store-sql/src/test/java/org/eclipse/edc/connector/store/sql/transferprocess/PostgresTransferProcessStoreTest.java @@ -87,8 +87,8 @@ void find_queryByDataRequest_propNotExist() { var tp = createTransferProcessBuilder("testprocess1") .dataRequest(da) .build(); - store.updateOrCreate(tp); - store.updateOrCreate(createTransferProcess("testprocess2")); + store.save(tp); + store.save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("dataRequest.notexist", "=", "somevalue"))) @@ -106,8 +106,8 @@ void find_queryByResourceManifest_propNotExist() { var tp = createTransferProcessBuilder("testprocess1") .resourceManifest(rm) .build(); - store.updateOrCreate(tp); - store.updateOrCreate(createTransferProcess("testprocess2")); + store.save(tp); + store.save(createTransferProcess("testprocess2")); // throws exception when an explicit mapping exists var query = QuerySpec.Builder.newInstance() @@ -139,8 +139,8 @@ void find_queryByProvisionedResourceSet_propNotExist() { var tp = createTransferProcessBuilder("testprocess1") .provisionedResourceSet(prs) .build(); - store.updateOrCreate(tp); - store.updateOrCreate(createTransferProcess("testprocess2")); + store.save(tp); + store.save(createTransferProcess("testprocess2")); // throws exception when an explicit mapping exists var query = QuerySpec.Builder.newInstance() @@ -160,7 +160,7 @@ void find_queryByProvisionedResourceSet_propNotExist() { @Test void find_queryByLease() { - store.updateOrCreate(createTransferProcess("testprocess1")); + store.save(createTransferProcess("testprocess1")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("lease.leasedBy", "=", "foobar"))) @@ -177,13 +177,13 @@ void create_withoutDataRequest_throwsException() { var t1 = TestFunctions.createTransferProcessBuilder("id1") .dataRequest(null) .build(); - assertThatIllegalArgumentException().isThrownBy(() -> getTransferProcessStore().updateOrCreate(t1)); + assertThatIllegalArgumentException().isThrownBy(() -> getTransferProcessStore().save(t1)); } @Override @Test protected void findAll_verifySorting_invalidProperty() { - range(0, 10).forEach(i -> getTransferProcessStore().updateOrCreate(createTransferProcess("test-neg-" + i))); + range(0, 10).forEach(i -> getTransferProcessStore().save(createTransferProcess("test-neg-" + i))); var query = QuerySpec.Builder.newInstance().sortField("notexist").sortOrder(SortOrder.DESC).build(); diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/PendingGuard.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/PendingGuard.java new file mode 100644 index 00000000000..60e937f7701 --- /dev/null +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/PendingGuard.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.spi.entity; + +import java.util.function.Predicate; + +/** + * Provides a way to control the flow of an entity in the state machine. + * If the function returns true, the entity will be marked as "pending" and it won't be picked up + * again by the state machine. + * + * @param the entity type + */ +@FunctionalInterface +public interface PendingGuard> extends Predicate { +} diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/StatefulEntity.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/StatefulEntity.java index 043cc737923..bd85b1e690c 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/StatefulEntity.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/entity/StatefulEntity.java @@ -35,6 +35,7 @@ public abstract class StatefulEntity> extends Mutabl protected long stateTimestamp; protected Map traceContext = new HashMap<>(); protected String errorDetail; + protected boolean pending = false; protected StatefulEntity() { } @@ -60,10 +61,18 @@ public String getErrorDetail() { return errorDetail; } + public boolean isPending() { + return pending; + } + public void setErrorDetail(String errorDetail) { this.errorDetail = errorDetail; } + public void setPending(boolean pending) { + this.pending = pending; + } + /** * Sets the state timestamp to the clock time. * @@ -100,6 +109,7 @@ protected > T copy(Builder builder) { .traceContext(traceContext) .errorDetail(errorDetail) .clock(clock) + .pending(pending) .build(); } @@ -141,6 +151,11 @@ public B traceContext(Map traceContext) { return self(); } + public B pending(boolean pending) { + entity.pending = pending; + return self(); + } + protected T build() { super.build(); if (entity.id == null) { diff --git a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/persistence/StateEntityStore.java b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/persistence/StateEntityStore.java index 356f13fd3b7..4f6db745907 100644 --- a/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/persistence/StateEntityStore.java +++ b/spi/common/core-spi/src/main/java/org/eclipse/edc/spi/persistence/StateEntityStore.java @@ -35,6 +35,10 @@ static Criterion hasState(int stateCode) { return new Criterion("state", "=", stateCode); } + static Criterion isNotPending() { + return new Criterion("pending", "=", false); + } + /** * Returns a list of not leased entities that satisfy the filter criteria. *

diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationConfirmed.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationConfirmed.java deleted file mode 100644 index 90f3c15d722..00000000000 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationConfirmed.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * 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 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation - * Fraunhofer Institute for Software and Systems Engineering - expending Event classes - * - */ - -package org.eclipse.edc.connector.contract.spi.event.contractnegotiation; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -/** - * This event is raised when the ContractNegotiation has been confirmed. - * - * @deprecated please use {@link ContractNegotiationAgreed} - */ -@Deprecated(since = "milestone9") -@JsonDeserialize(builder = ContractNegotiationConfirmed.Builder.class) -public class ContractNegotiationConfirmed extends ContractNegotiationEvent { - - private ContractNegotiationConfirmed() { - } - - @Override - public String name() { - return "contract.negotiation.confirmed"; - } - - @JsonPOJOBuilder(withPrefix = "") - public static class Builder extends ContractNegotiationEvent.Builder { - - @JsonCreator - private Builder() { - super(new ContractNegotiationConfirmed()); - } - - public static Builder newInstance() { - return new Builder(); - } - - @Override - public Builder self() { - return this; - } - } -} diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationDeclined.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationDeclined.java deleted file mode 100644 index 2aad562466f..00000000000 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationDeclined.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * 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 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation - * Fraunhofer Institute for Software and Systems Engineering - expending Event classes - * - */ - -package org.eclipse.edc.connector.contract.spi.event.contractnegotiation; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -/** - * This event is raised when the ContractNegotiation has been declined. - * - * @deprecated please use {@link ContractNegotiationTerminated} - */ -@Deprecated(since = "milestone9") -@JsonDeserialize(builder = ContractNegotiationDeclined.Builder.class) -public class ContractNegotiationDeclined extends ContractNegotiationEvent { - - private ContractNegotiationDeclined() { - } - - @Override - public String name() { - return "contract.negotiation.declined"; - } - - @JsonPOJOBuilder(withPrefix = "") - public static class Builder extends ContractNegotiationEvent.Builder { - - private Builder() { - super(new ContractNegotiationDeclined()); - } - - @JsonCreator - public static Builder newInstance() { - return new Builder(); - } - - @Override - public Builder self() { - return this; - } - } - -} diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationFailed.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationFailed.java deleted file mode 100644 index cb2d3c1dada..00000000000 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/event/contractnegotiation/ContractNegotiationFailed.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * 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 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation - * Fraunhofer Institute for Software and Systems Engineering - expending Event classes - * - */ - -package org.eclipse.edc.connector.contract.spi.event.contractnegotiation; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -/** - * This event is raised when the ContractNegotiation has failed. - * - * @deprecated please use {@link ContractNegotiationTerminated} - */ -@Deprecated(since = "milestone9") -@JsonDeserialize(builder = ContractNegotiationFailed.Builder.class) -public class ContractNegotiationFailed extends ContractNegotiationEvent { - - private ContractNegotiationFailed() { - } - - @Override - public String name() { - return "contract.negotiation.failed"; - } - - @JsonPOJOBuilder(withPrefix = "") - public static class Builder extends ContractNegotiationEvent.Builder { - - private Builder() { - super(new ContractNegotiationFailed()); - } - - @JsonCreator - public static Builder newInstance() { - return new Builder(); - } - - @Override - public Builder self() { - return this; - } - } - -} diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/ContractNegotiationPendingGuard.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/ContractNegotiationPendingGuard.java new file mode 100644 index 00000000000..520d3568c0c --- /dev/null +++ b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/ContractNegotiationPendingGuard.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.contract.spi.negotiation; + +import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.entity.PendingGuard; + +/** + * Marker interface to define a service that permits to choose whether a ContractNegotiation will be waiting for an external + * interaction. + */ +@FunctionalInterface +@ExtensionPoint +public interface ContractNegotiationPendingGuard extends PendingGuard { +} diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/observe/ContractNegotiationListener.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/observe/ContractNegotiationListener.java index 97f07b34474..1bc76a10375 100644 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/observe/ContractNegotiationListener.java +++ b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/contract/spi/negotiation/observe/ContractNegotiationListener.java @@ -72,28 +72,6 @@ default void terminated(ContractNegotiation negotiation) { } - /** - * Called after a {@link ContractNegotiation} was declined. - * - * @param negotiation the contract negotiation that has been declined. - * @deprecated please use {@link #terminated(ContractNegotiation)} - */ - @Deprecated(since = "milestone9") - default void declined(ContractNegotiation negotiation) { - terminated(negotiation); - } - - /** - * Called after a {@link ContractNegotiation} failed. - * - * @param negotiation the contract negotiation that failed. - * @deprecated please use {@link #terminated(ContractNegotiation)} - */ - @Deprecated(since = "milestone9") - default void failed(ContractNegotiation negotiation) { - terminated(negotiation); - } - /** * Called after a {@link ContractNegotiation} was agreed by the provider. * diff --git a/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/contract/spi/event/ContractEventTest.java b/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/contract/spi/event/ContractEventTest.java index 6ae6fb1a26d..0bf9d671f65 100644 --- a/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/contract/spi/event/ContractEventTest.java +++ b/spi/control-plane/contract-spi/src/test/java/org/eclipse/edc/connector/contract/spi/event/ContractEventTest.java @@ -18,10 +18,7 @@ import org.eclipse.edc.connector.contract.spi.event.contractdefinition.ContractDefinitionCreated; import org.eclipse.edc.connector.contract.spi.event.contractdefinition.ContractDefinitionDeleted; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationAccepted; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationConfirmed; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationDeclined; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationEvent; -import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFailed; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationFinalized; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationInitiated; import org.eclipse.edc.connector.contract.spi.event.contractnegotiation.ContractNegotiationOffered; @@ -76,9 +73,6 @@ public Stream provideArguments(ExtensionContext context) { ContractDefinitionCreated.Builder.newInstance().contractDefinitionId("id").build(), ContractDefinitionDeleted.Builder.newInstance().contractDefinitionId("id").build(), baseBuilder(ContractNegotiationAccepted.Builder.newInstance()).build(), - baseBuilder(ContractNegotiationConfirmed.Builder.newInstance()).build(), - baseBuilder(ContractNegotiationDeclined.Builder.newInstance()).build(), - baseBuilder(ContractNegotiationFailed.Builder.newInstance()).build(), baseBuilder(ContractNegotiationInitiated.Builder.newInstance()).build(), baseBuilder(ContractNegotiationOffered.Builder.newInstance()).build(), baseBuilder(ContractNegotiationRequested.Builder.newInstance()).build(), diff --git a/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/ContractNegotiationStoreTestBase.java b/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/ContractNegotiationStoreTestBase.java index 0ca2f52c052..81046343e3e 100644 --- a/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/ContractNegotiationStoreTestBase.java +++ b/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/ContractNegotiationStoreTestBase.java @@ -35,8 +35,10 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.connector.contract.spi.testfixtures.negotiation.store.TestFunctions.createCallbackAddress; import static org.eclipse.edc.connector.contract.spi.testfixtures.negotiation.store.TestFunctions.createContract; import static org.eclipse.edc.connector.contract.spi.testfixtures.negotiation.store.TestFunctions.createContractBuilder; import static org.eclipse.edc.connector.contract.spi.testfixtures.negotiation.store.TestFunctions.createNegotiation; @@ -54,14 +56,17 @@ public abstract class ContractNegotiationStoreTestBase { @Test @DisplayName("Verify that an entity is found by ID") void find() { - var id = "test-cn1"; - var negotiation = createNegotiation(id); + var id = UUID.randomUUID().toString(); + var negotiation = createNegotiationBuilder(id) + .pending(true) + .build(); getContractNegotiationStore().save(negotiation); - assertThat(getContractNegotiationStore().findById(id)) + var result = getContractNegotiationStore().findById(id); + + assertThat(result) .usingRecursiveComparison() .isEqualTo(negotiation); - } @Test @@ -272,21 +277,28 @@ void update_addsAgreement_shouldPersist() { } @Test - @DisplayName("Should persist update the callbacks if changed") - void update_changeCallbacks() { - var negotiationId = "test-cn1"; - var negotiation = createNegotiation(negotiationId); - getContractNegotiationStore().save(negotiation); + void update_changeFields() { + var negotiationId = UUID.randomUUID().toString(); + var builder = createNegotiationBuilder(negotiationId) + .callbackAddresses(List.of(createCallbackAddress())) + .pending(false); + getContractNegotiationStore().save(builder.build()); - // one callback - assertThat(Objects.requireNonNull(getContractNegotiationStore().findById(negotiationId)).getCallbackAddresses()).hasSize(1); + var inserted = getContractNegotiationStore().findById(negotiationId); + assertThat(inserted).isNotNull().satisfies(i -> { + assertThat(i.getCallbackAddresses()).hasSize(1); + assertThat(i.isPending()).isFalse(); + }); - // remove callbacks - var updatedNegotiation = createNegotiationBuilder(negotiationId).callbackAddresses(List.of()).build(); + builder.callbackAddresses(emptyList()).pending(true); - getContractNegotiationStore().save(updatedNegotiation); //should perform an update + insert + getContractNegotiationStore().save(builder.build()); - assertThat(Objects.requireNonNull(getContractNegotiationStore().findById(negotiationId)).getCallbackAddresses()).isEmpty(); + var updated = getContractNegotiationStore().findById(negotiationId); + assertThat(updated).isNotNull().satisfies(u -> { + assertThat(u.getCallbackAddresses()).isEmpty(); + assertThat(u.isPending()).isTrue(); + }); } @Test diff --git a/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/TestFunctions.java b/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/TestFunctions.java index 1bf11e76caf..1e2aef2bc7f 100644 --- a/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/TestFunctions.java +++ b/spi/control-plane/contract-spi/src/testFixtures/java/org/eclipse/edc/connector/contract/spi/testfixtures/negotiation/store/TestFunctions.java @@ -72,13 +72,17 @@ public static ContractNegotiation.Builder createNegotiationBuilder(String id) { .state(ContractNegotiationStates.REQUESTED.code()) .counterPartyAddress("consumer") .counterPartyId("consumerId") - .callbackAddresses(List.of(CallbackAddress.Builder.newInstance() - .uri("local://test") - .events(Set.of("contract.negotiation.initiated")) - .build())) + .callbackAddresses(List.of(createCallbackAddress())) .protocol("protocol"); } + public static CallbackAddress createCallbackAddress() { + return CallbackAddress.Builder.newInstance() + .uri("local://test") + .events(Set.of("contract.negotiation.initiated")) + .build(); + } + public static Policy createPolicy() { return Policy.Builder.newInstance() .permission(Permission.Builder.newInstance() diff --git a/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/TransferProcessPendingGuard.java b/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/TransferProcessPendingGuard.java new file mode 100644 index 00000000000..671a3fc09c7 --- /dev/null +++ b/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/TransferProcessPendingGuard.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.connector.transfer.spi; + +import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.entity.PendingGuard; + +/** + * Marker interface to define a service that permits to choose whether a TransferProcess will be waiting for an external + * interaction. + */ +@FunctionalInterface +@ExtensionPoint +public interface TransferProcessPendingGuard extends PendingGuard { +} diff --git a/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/event/TransferProcessCancelled.java b/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/event/TransferProcessCancelled.java deleted file mode 100644 index c3d387f5895..00000000000 --- a/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/event/TransferProcessCancelled.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * 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 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation - * - */ - -package org.eclipse.edc.connector.transfer.spi.event; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -/** - * This event is raised when the TransferProcess has been cancelled. - * - * @deprecated this event is not thrown by anyone, please use {@link TransferProcessTerminated} instead - */ -@Deprecated(since = "milestone9") -@JsonDeserialize(builder = TransferProcessCancelled.Builder.class) -public class TransferProcessCancelled extends TransferProcessEvent { - - private TransferProcessCancelled() { - } - - @Override - public String name() { - return "transfer.process.cancelled"; - } - - - @JsonPOJOBuilder(withPrefix = "") - public static class Builder extends TransferProcessEvent.Builder { - - private Builder() { - super(new TransferProcessCancelled()); - } - - @JsonCreator - public static Builder newInstance() { - return new Builder(); - } - - @Override - public Builder self() { - return this; - } - } -} diff --git a/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/event/TransferProcessEnded.java b/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/event/TransferProcessEnded.java deleted file mode 100644 index bb2c11d37dc..00000000000 --- a/spi/control-plane/transfer-spi/src/main/java/org/eclipse/edc/connector/transfer/spi/event/TransferProcessEnded.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * 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 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation - * - */ - -package org.eclipse.edc.connector.transfer.spi.event; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; - -/** - * This event is raised when the TransferProcess has been ended. - * - * @deprecated this event is not thrown by anyone, please use {@link TransferProcessTerminated} instead - */ -@Deprecated(since = "milestone9") -@JsonDeserialize(builder = TransferProcessEvent.Builder.class) -public class TransferProcessEnded extends TransferProcessEvent { - - private TransferProcessEnded() { - } - - @Override - public String name() { - return "transfer.process.ended"; - } - - @JsonPOJOBuilder(withPrefix = "") - public static class Builder extends TransferProcessEvent.Builder { - - private Builder() { - super(new TransferProcessEnded()); - } - - @JsonCreator - public static Builder newInstance() { - return new Builder(); - } - - @Override - public Builder self() { - return this; - } - } - -} diff --git a/spi/control-plane/transfer-spi/src/testFixtures/java/org/eclipse/edc/connector/transfer/spi/testfixtures/store/TransferProcessStoreTestBase.java b/spi/control-plane/transfer-spi/src/testFixtures/java/org/eclipse/edc/connector/transfer/spi/testfixtures/store/TransferProcessStoreTestBase.java index e4c3fe216cd..04ad199eda4 100644 --- a/spi/control-plane/transfer-spi/src/testFixtures/java/org/eclipse/edc/connector/transfer/spi/testfixtures/store/TransferProcessStoreTestBase.java +++ b/spi/control-plane/transfer-spi/src/testFixtures/java/org/eclipse/edc/connector/transfer/spi/testfixtures/store/TransferProcessStoreTestBase.java @@ -86,8 +86,9 @@ void find_queryByState() { void create() { var transferProcess = createTransferProcessBuilder("test-id") .dataRequest(createDataRequestBuilder().id("data-request-id").build()) + .pending(true) .privateProperties(Map.of("key", "value")).build(); - getTransferProcessStore().updateOrCreate(transferProcess); + getTransferProcessStore().save(transferProcess); var retrieved = getTransferProcessStore().findById("test-id"); @@ -101,7 +102,7 @@ void create_verifyCallbacks() { var callbacks = List.of(CallbackAddress.Builder.newInstance().uri("test").events(Set.of("event")).build()); var t = createTransferProcessBuilder("test-id").privateProperties(Map.of("key", "value")).callbackAddresses(callbacks).build(); - getTransferProcessStore().updateOrCreate(t); + getTransferProcessStore().save(t); var all = getTransferProcessStore().findAll(QuerySpec.none()).collect(Collectors.toList()); assertThat(all).containsExactly(t); @@ -112,10 +113,10 @@ void create_verifyCallbacks() { @Test void create_withSameIdExists_shouldReplace() { var t = createTransferProcess("id1", INITIAL); - getTransferProcessStore().updateOrCreate(t); + getTransferProcessStore().save(t); var t2 = createTransferProcess("id1", PROVISIONING); - getTransferProcessStore().updateOrCreate(t2); + getTransferProcessStore().save(t2); assertThat(getTransferProcessStore().findAll(QuerySpec.none())).hasSize(1).containsExactly(t2); } @@ -125,7 +126,7 @@ void nextNotLeased() { var state = STARTED; var all = range(0, 10) .mapToObj(i -> createTransferProcess("id" + i, state)) - .peek(getTransferProcessStore()::updateOrCreate) + .peek(getTransferProcessStore()::save) .toList(); assertThat(getTransferProcessStore().nextNotLeased(5, hasState(state.code()))) @@ -140,7 +141,7 @@ void nextNotLeased_shouldOnlyReturnFreeItems() { var state = STARTED; var all = range(0, 10) .mapToObj(i -> createTransferProcess("id" + i, state)) - .peek(getTransferProcessStore()::updateOrCreate) + .peek(getTransferProcessStore()::save) .collect(Collectors.toList()); // lease a few @@ -158,7 +159,7 @@ void nextNotLeased_noFreeItem_shouldReturnEmpty() { var state = STARTED; range(0, 3) .mapToObj(i -> createTransferProcess("id" + i, state)) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); // first time works assertThat(getTransferProcessStore().nextNotLeased(10, hasState(state.code()))).hasSize(3); @@ -170,7 +171,7 @@ void nextNotLeased_noFreeItem_shouldReturnEmpty() { void nextNotLeased_noneInDesiredState() { range(0, 3) .mapToObj(i -> createTransferProcess("id" + i, STARTED)) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); var nextNotLeased = getTransferProcessStore().nextNotLeased(10, hasState(TERMINATED.code())); @@ -182,7 +183,7 @@ void nextNotLeased_batchSizeLimits() { var state = STARTED; range(0, 10) .mapToObj(i -> createTransferProcess("id" + i, state)) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); // first time works var result = getTransferProcessStore().nextNotLeased(3, hasState(state.code())); @@ -195,7 +196,7 @@ void nextNotLeased_verifyTemporalOrdering() { range(0, 10) .mapToObj(i -> createTransferProcess(String.valueOf(i), state)) .peek(this::delayByTenMillis) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); assertThat(getTransferProcessStore().nextNotLeased(20, hasState(state.code()))) .extracting(TransferProcess::getId) @@ -207,14 +208,14 @@ void nextNotLeased_verifyTemporalOrdering() { void nextNotLeased_verifyMostRecentlyUpdatedIsLast() throws InterruptedException { var all = range(0, 10) .mapToObj(i -> createTransferProcess("id" + i, STARTED)) - .peek(getTransferProcessStore()::updateOrCreate) + .peek(getTransferProcessStore()::save) .toList(); Thread.sleep(100); var fourth = all.get(3); fourth.updateStateTimestamp(); - getTransferProcessStore().updateOrCreate(fourth); + getTransferProcessStore().save(fourth); var next = getTransferProcessStore().nextNotLeased(20, hasState(STARTED.code())); assertThat(next.indexOf(fourth)).isEqualTo(9); @@ -224,7 +225,7 @@ void nextNotLeased_verifyMostRecentlyUpdatedIsLast() throws InterruptedException @DisplayName("Verifies that calling nextNotLeased locks the TP for any subsequent calls") void nextNotLeased_locksEntity() { var t = createTransferProcess("id1", INITIAL); - getTransferProcessStore().updateOrCreate(t); + getTransferProcessStore().save(t); getTransferProcessStore().nextNotLeased(100, hasState(INITIAL.code())); @@ -234,7 +235,7 @@ void nextNotLeased_locksEntity() { @Test void nextNotLeased_expiredLease() { var t = createTransferProcess("id1", INITIAL); - getTransferProcessStore().updateOrCreate(t); + getTransferProcessStore().save(t); leaseEntity(t.getId(), CONNECTOR_NAME, Duration.ofMillis(100)); @@ -246,7 +247,7 @@ void nextNotLeased_expiredLease() { @Test void nextNotLeased_shouldLeaseEntityUntilUpdate() { var initialTransferProcess = initialTransferProcess(); - getTransferProcessStore().updateOrCreate(initialTransferProcess); + getTransferProcessStore().save(initialTransferProcess); var firstQueryResult = getTransferProcessStore().nextNotLeased(1, hasState(INITIAL.code())); assertThat(firstQueryResult).hasSize(1); @@ -255,7 +256,7 @@ void nextNotLeased_shouldLeaseEntityUntilUpdate() { assertThat(secondQueryResult).hasSize(0); var retrieved = firstQueryResult.get(0); - getTransferProcessStore().updateOrCreate(retrieved); + getTransferProcessStore().save(retrieved); var thirdQueryResult = getTransferProcessStore().nextNotLeased(1, hasState(INITIAL.code())); assertThat(thirdQueryResult).hasSize(1); @@ -265,14 +266,14 @@ void nextNotLeased_shouldLeaseEntityUntilUpdate() { void nextNotLeased_avoidsStarvation() throws InterruptedException { for (int i = 0; i < 10; i++) { var process = createTransferProcess("test-process-" + i); - getTransferProcessStore().updateOrCreate(process); + getTransferProcessStore().save(process); } var list1 = getTransferProcessStore().nextNotLeased(5, hasState(INITIAL.code())); Thread.sleep(50); //simulate a short delay to generate different timestamps list1.forEach(tp -> { tp.updateStateTimestamp(); - getTransferProcessStore().updateOrCreate(tp); + getTransferProcessStore().save(tp); }); var list2 = getTransferProcessStore().nextNotLeased(5, hasState(INITIAL.code())); assertThat(list1).isNotEqualTo(list2).doesNotContainAnyElementsOf(list2); @@ -281,7 +282,7 @@ void nextNotLeased_avoidsStarvation() throws InterruptedException { @Test void findById() { var t = createTransferProcess("id1"); - getTransferProcessStore().updateOrCreate(t); + getTransferProcessStore().save(t); var result = getTransferProcessStore().findById("id1"); @@ -299,7 +300,7 @@ void findById_notExist() { void findForCorrelationId() { var dataRequest = createDataRequestBuilder().id("correlationId").build(); var transferProcess = createTransferProcessBuilder("id1").dataRequest(dataRequest).build(); - getTransferProcessStore().updateOrCreate(transferProcess); + getTransferProcessStore().save(transferProcess); var res = getTransferProcessStore().findForCorrelationId("correlationId"); @@ -314,10 +315,11 @@ void findForCorrelationId_notExist() { @Test void update_exists_shouldUpdate() { var t1 = createTransferProcess("id1", STARTED); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); t1.transitionCompleted(); //modify - getTransferProcessStore().updateOrCreate(t1); + t1.setPending(true); + getTransferProcessStore().save(t1); assertThat(getTransferProcessStore().findAll(QuerySpec.none())) .hasSize(1) @@ -330,7 +332,7 @@ void update_notExist_shouldCreate() { var t1 = createTransferProcess("id1", STARTED); t1.transitionCompleted(); //modify - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); var result = getTransferProcessStore().findAll(QuerySpec.none()).collect(Collectors.toList()); assertThat(result) @@ -343,12 +345,12 @@ void update_notExist_shouldCreate() { @DisplayName("Verify that the lease on a TP is cleared by an update") void update_shouldBreakLease() { var t1 = createTransferProcess("id1"); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); // acquire lease leaseEntity(t1.getId(), CONNECTOR_NAME); t1.transitionProvisioning(ResourceManifest.Builder.newInstance().build()); //modify - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); // lease should be broken var notLeased = getTransferProcessStore().nextNotLeased(10, hasState(PROVISIONING.code())); @@ -360,19 +362,19 @@ void update_shouldBreakLease() { void update_leasedByOther_shouldThrowException() { var tpId = "id1"; var t1 = createTransferProcess(tpId); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); leaseEntity(tpId, "someone"); t1.transitionProvisioning(ResourceManifest.Builder.newInstance().build()); //modify // leased by someone else -> throw exception - assertThatThrownBy(() -> getTransferProcessStore().updateOrCreate(t1)).isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> getTransferProcessStore().save(t1)).isInstanceOf(IllegalStateException.class); } @Test void delete() { var t1 = createTransferProcess("id1"); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); getTransferProcessStore().delete("id1"); assertThat(getTransferProcessStore().findAll(QuerySpec.none())).isEmpty(); @@ -381,7 +383,7 @@ void delete() { @Test void delete_isLeasedBySelf_shouldThrowException() { var t1 = createTransferProcess("id1"); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); leaseEntity(t1.getId(), CONNECTOR_NAME); @@ -391,7 +393,7 @@ void delete_isLeasedBySelf_shouldThrowException() { @Test void delete_isLeasedByOther_shouldThrowException() { var t1 = createTransferProcess("id1"); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); leaseEntity(t1.getId(), "someone-else"); @@ -408,7 +410,7 @@ void delete_notExist() { void findAll_noQuerySpec() { var all = range(0, 10) .mapToObj(i -> createTransferProcess("id" + i)) - .peek(getTransferProcessStore()::updateOrCreate) + .peek(getTransferProcessStore()::save) .collect(Collectors.toList()); assertThat(getTransferProcessStore().findAll(QuerySpec.none())).containsExactlyInAnyOrderElementsOf(all); @@ -418,7 +420,7 @@ void findAll_noQuerySpec() { void findAll_verifyPaging() { range(0, 10) .mapToObj(i -> createTransferProcess(String.valueOf(i))) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); var qs = QuerySpec.Builder.newInstance().limit(5).offset(3).build(); assertThat(getTransferProcessStore().findAll(qs)).hasSize(5) @@ -432,7 +434,7 @@ void findAll_verifyPaging_pageSizeLargerThanCollection() { range(0, 10) .mapToObj(i -> createTransferProcess(String.valueOf(i))) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); var qs = QuerySpec.Builder.newInstance().limit(20).offset(3).build(); assertThat(getTransferProcessStore().findAll(qs)) @@ -447,7 +449,7 @@ void findAll_verifyPaging_pageSizeOutsideCollection() { range(0, 10) .mapToObj(i -> createTransferProcess(String.valueOf(i))) - .forEach(getTransferProcessStore()::updateOrCreate); + .forEach(getTransferProcessStore()::save); var qs = QuerySpec.Builder.newInstance().limit(10).offset(12).build(); assertThat(getTransferProcessStore().findAll(qs)).isEmpty(); @@ -458,7 +460,7 @@ void findAll_verifyPaging_pageSizeOutsideCollection() { void update_dataRequestWithNewId_replacesOld() { var bldr = createTransferProcessBuilder("id1").state(STARTED.code()); var t1 = bldr.build(); - getTransferProcessStore().updateOrCreate(t1); + getTransferProcessStore().save(t1); var t2 = bldr .dataRequest(TestFunctions.createDataRequestBuilder() @@ -469,7 +471,7 @@ void update_dataRequestWithNewId_replacesOld() { .connectorId("new-connector") .build()) .build(); - getTransferProcessStore().updateOrCreate(t2); + getTransferProcessStore().save(t2); var all = getTransferProcessStore().findAll(QuerySpec.none()).collect(Collectors.toList()); assertThat(all) @@ -492,8 +494,8 @@ void find_queryByDataAddressProperty() { var tp = createTransferProcessBuilder("testprocess1") .contentDataAddress(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("contentDataAddress.properties.key", "=", "value"))) @@ -513,8 +515,8 @@ void find_queryByDataAddress_propNotExist() { var tp = createTransferProcessBuilder("testprocess1") .contentDataAddress(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("contentDataAddress.properties.notexist", "=", "value"))) @@ -532,8 +534,8 @@ void find_queryByDataAddress_invalidKey_valueNotExist() { var tp = createTransferProcessBuilder("testprocess1") .contentDataAddress(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("contentDataAddress.properties.key", "=", "notexist"))) @@ -548,8 +550,8 @@ void find_queryByDataRequestProperty_processId() { var tp = createTransferProcessBuilder("testprocess1") .dataRequest(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("dataRequest.processId", "=", "testprocess1"))) @@ -566,8 +568,8 @@ void find_queryByDataRequestProperty_id() { var tp = createTransferProcessBuilder("testprocess1") .dataRequest(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("dataRequest.id", "=", da.getId()))) @@ -585,8 +587,8 @@ void find_queryByDataRequestProperty_protocol() { var tp = createTransferProcessBuilder("testprocess1") .dataRequest(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("dataRequest.protocol", "like", "%/protocol"))) @@ -603,8 +605,8 @@ void find_queryByDataRequest_valueNotExist() { var tp = createTransferProcessBuilder("testprocess1") .dataRequest(da) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("dataRequest.id", "=", "notexist"))) @@ -621,8 +623,8 @@ void find_queryByResourceManifestProperty() { var tp = createTransferProcessBuilder("testprocess1") .resourceManifest(rm) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("resourceManifest.definitions.id", "=", "rd-id"))) @@ -640,8 +642,8 @@ void find_queryByResourceManifest_valueNotExist() { var tp = createTransferProcessBuilder("testprocess1") .resourceManifest(rm) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); // throws exception when an explicit mapping exists var query = QuerySpec.Builder.newInstance() @@ -664,8 +666,8 @@ void find_queryByProvisionedResourceSetProperty() { var tp = createTransferProcessBuilder("testprocess1") .provisionedResourceSet(prs) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); var query = QuerySpec.Builder.newInstance() .filter(List.of(new Criterion("provisionedResourceSet.resources.transferProcessId", "=", "testprocess1"))) @@ -689,8 +691,8 @@ void find_queryByProvisionedResourceSet_valueNotExist() { var tp = createTransferProcessBuilder("testprocess1") .provisionedResourceSet(prs) .build(); - getTransferProcessStore().updateOrCreate(tp); - getTransferProcessStore().updateOrCreate(createTransferProcess("testprocess2")); + getTransferProcessStore().save(tp); + getTransferProcessStore().save(createTransferProcess("testprocess2")); // returns empty when the invalid value is embedded in JSON @@ -724,8 +726,8 @@ void find_queryByDeprovisionedResourcesProperty() { .deprovisionedResources(List.of(dp3)) .build(); - getTransferProcessStore().updateOrCreate(process1); - getTransferProcessStore().updateOrCreate(process2); + getTransferProcessStore().save(process1); + getTransferProcessStore().save(process2); var query = QuerySpec.Builder.newInstance() .filter(Criterion.criterion("deprovisionedResources.inProcess", "=", "true")) @@ -761,8 +763,8 @@ void find_queryByDeprovisionedResourcesProperty_multipleCriteria() { .deprovisionedResources(List.of(dp3)) .build(); - getTransferProcessStore().updateOrCreate(process1); - getTransferProcessStore().updateOrCreate(process2); + getTransferProcessStore().save(process1); + getTransferProcessStore().save(process2); var query = QuerySpec.Builder.newInstance() .filter(List.of( @@ -801,8 +803,8 @@ void find_queryByDeprovisionedResourcesProperty_multipleResults() { .deprovisionedResources(List.of(dp3)) .build(); - getTransferProcessStore().updateOrCreate(process1); - getTransferProcessStore().updateOrCreate(process2); + getTransferProcessStore().save(process1); + getTransferProcessStore().save(process2); var query = QuerySpec.Builder.newInstance() .filter(Criterion.criterion("deprovisionedResources.inProcess", "=", "false")) @@ -830,7 +832,7 @@ void find_queryByDeprovisionedResources_propNotExist() { var process1 = createTransferProcessBuilder("test-pid1") .deprovisionedResources(List.of(dp1, dp2)) .build(); - getTransferProcessStore().updateOrCreate(process1); + getTransferProcessStore().save(process1); var query = QuerySpec.Builder.newInstance() .filter(Criterion.criterion("deprovisionedResources.foobar", "=", "barbaz")) @@ -856,7 +858,7 @@ void find_queryByDeprovisionedResources_valueNotExist() { var process1 = createTransferProcessBuilder("test-pid1") .deprovisionedResources(List.of(dp1, dp2)) .build(); - getTransferProcessStore().updateOrCreate(process1); + getTransferProcessStore().save(process1); var query = QuerySpec.Builder.newInstance() .filter(Criterion.criterion("deprovisionedResources.errorMessage", "=", "notexist")) @@ -871,7 +873,7 @@ void find_queryByDeprovisionedResources_valueNotExist() { void verifyCreateUpdateDelete() { var id = UUID.randomUUID().toString(); var transferProcess = createTransferProcess(id, createDataRequestBuilder().id("correlationId").build()); - getTransferProcessStore().updateOrCreate(transferProcess); + getTransferProcessStore().save(transferProcess); var found = getTransferProcessStore().findById(id); @@ -884,7 +886,7 @@ void verifyCreateUpdateDelete() { transferProcess.transitionProvisioning(ResourceManifest.Builder.newInstance().build()); - getTransferProcessStore().updateOrCreate(transferProcess); + getTransferProcessStore().save(transferProcess); found = getTransferProcessStore().findById(id); assertNotNull(found); @@ -897,7 +899,7 @@ void verifyCreateUpdateDelete() { @Test void findAll_verifyFiltering() { - range(0, 10).forEach(i -> getTransferProcessStore().updateOrCreate(createTransferProcess("test-neg-" + i))); + range(0, 10).forEach(i -> getTransferProcessStore().save(createTransferProcess("test-neg-" + i))); var querySpec = QuerySpec.Builder.newInstance().filter(Criterion.criterion("id", "=", "test-neg-3")).build(); var result = getTransferProcessStore().findAll(querySpec); @@ -907,7 +909,7 @@ void findAll_verifyFiltering() { @Test void findAll_verifyFiltering_invalidFilterExpression() { - range(0, 10).forEach(i -> getTransferProcessStore().updateOrCreate(createTransferProcess("test-neg-" + i))); + range(0, 10).forEach(i -> getTransferProcessStore().save(createTransferProcess("test-neg-" + i))); var querySpec = QuerySpec.Builder.newInstance().filter(Criterion.criterion("something", "foobar", "other")).build(); assertThatThrownBy(() -> getTransferProcessStore().findAll(querySpec)).isInstanceOfAny(IllegalArgumentException.class); @@ -915,16 +917,25 @@ void findAll_verifyFiltering_invalidFilterExpression() { @Test void findAll_verifySorting() { - range(0, 10).forEach(i -> getTransferProcessStore().updateOrCreate(createTransferProcess("test-neg-" + i))); + range(0, 10).forEach(i -> getTransferProcessStore().save(createTransferProcess("test-neg-" + i))); assertThat(getTransferProcessStore().findAll(QuerySpec.Builder.newInstance().sortField("id").sortOrder(SortOrder.ASC).build())).hasSize(10).isSortedAccordingTo(Comparator.comparing(TransferProcess::getId)); assertThat(getTransferProcessStore().findAll(QuerySpec.Builder.newInstance().sortField("id").sortOrder(SortOrder.DESC).build())).hasSize(10).isSortedAccordingTo((c1, c2) -> c2.getId().compareTo(c1.getId())); } + @Test + protected void findAll_verifySorting_invalidProperty() { + range(0, 10).forEach(i -> getTransferProcessStore().save(createTransferProcess("test-neg-" + i))); + + var query = QuerySpec.Builder.newInstance().sortField("notexist").sortOrder(SortOrder.DESC).build(); + + assertThat(getTransferProcessStore().findAll(query)).isEmpty(); + } + @Test void findByIdAndLease_shouldReturnTheEntityAndLeaseIt() { var id = UUID.randomUUID().toString(); - getTransferProcessStore().updateOrCreate(createTransferProcess(id)); + getTransferProcessStore().save(createTransferProcess(id)); var result = getTransferProcessStore().findByIdAndLease(id); @@ -942,7 +953,7 @@ void findByIdAndLease_shouldReturnNotFound_whenEntityDoesNotExist() { @Test void findByIdAndLease_shouldReturnAlreadyLeased_whenEntityIsAlreadyLeased() { var id = UUID.randomUUID().toString(); - getTransferProcessStore().updateOrCreate(createTransferProcess(id)); + getTransferProcessStore().save(createTransferProcess(id)); leaseEntity(id, "other owner"); var result = getTransferProcessStore().findByIdAndLease(id); @@ -983,15 +994,6 @@ void findByCorrelationIdAndLease_shouldReturnAlreadyLeased_whenEntityIsAlreadyLe assertThat(result).isFailed().extracting(StoreFailure::getReason).isEqualTo(ALREADY_LEASED); } - @Test - protected void findAll_verifySorting_invalidProperty() { - range(0, 10).forEach(i -> getTransferProcessStore().updateOrCreate(createTransferProcess("test-neg-" + i))); - - var query = QuerySpec.Builder.newInstance().sortField("notexist").sortOrder(SortOrder.DESC).build(); - - assertThat(getTransferProcessStore().findAll(query).collect(Collectors.toList())).isEmpty(); - } - protected abstract boolean supportsCollectionQuery(); protected abstract boolean supportsLikeOperator(); diff --git a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/TransferProcessApiEndToEndTest.java b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/TransferProcessApiEndToEndTest.java index 27839807c03..7e7685d5aa6 100644 --- a/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/TransferProcessApiEndToEndTest.java +++ b/system-tests/management-api/management-api-test-runner/src/test/java/org/eclipse/edc/test/e2e/managementapi/TransferProcessApiEndToEndTest.java @@ -56,8 +56,8 @@ public class TransferProcessApiEndToEndTest extends BaseManagementApiEndToEndTes @Test void getAll() { - getStore().updateOrCreate(createTransferProcess("tp1")); - getStore().updateOrCreate(createTransferProcess("tp2")); + getStore().save(createTransferProcess("tp1")); + getStore().save(createTransferProcess("tp2")); baseRequest() .contentType(JSON) @@ -71,8 +71,8 @@ void getAll() { @Test void getById() { - getStore().updateOrCreate(createTransferProcess("tp1")); - getStore().updateOrCreate(createTransferProcess("tp2")); + getStore().save(createTransferProcess("tp1")); + getStore().save(createTransferProcess("tp2")); baseRequest() .get("/v2/transferprocesses/tp2") @@ -84,7 +84,7 @@ void getById() { @Test void getState() { - getStore().updateOrCreate(createTransferProcessBuilder("tp2").state(COMPLETED.code()).build()); + getStore().save(createTransferProcessBuilder("tp2").state(COMPLETED.code()).build()); baseRequest() .get("/v2/transferprocesses/tp2/state") @@ -131,7 +131,7 @@ void create() { @Test void deprovision() { var id = UUID.randomUUID().toString(); - getStore().updateOrCreate(createTransferProcessBuilder(id).state(COMPLETED.code()).build()); + getStore().save(createTransferProcessBuilder(id).state(COMPLETED.code()).build()); baseRequest() .contentType(JSON) @@ -143,7 +143,7 @@ void deprovision() { @Test void terminate() { var id = UUID.randomUUID().toString(); - getStore().updateOrCreate(createTransferProcessBuilder(id).state(REQUESTED.code()).build()); + getStore().save(createTransferProcessBuilder(id).state(REQUESTED.code()).build()); var requestBody = createObjectBuilder() .add(CONTEXT, createObjectBuilder().add(VOCAB, EDC_NAMESPACE)) .add("reason", "any")