From a2c1e5f1321e047900862a7de9357aa505e5a5dd Mon Sep 17 00:00:00 2001 From: Bondar Bogdan <36962546+bogdan-bondar@users.noreply.github.com> Date: Fri, 14 Dec 2018 17:41:38 +0100 Subject: [PATCH] Transaction handling refactoring (#771) * unified new transaction handling in jpa repositories, extended Deployment Management and Action Repository Signed-off-by: Bogdan Bondar * moved Deployment Helper to utilities package Signed-off-by: Bogdan Bondar * removed superfluous utility method from Deployment Helper Signed-off-by: Bogdan Bondar * refactored distribution set to target assignment, fixed PR review findings Signed-off-by: Bogdan Bondar * fixed PR review findings Signed-off-by: Bogdan Bondar * added additional validation of active flag and current status fields for action status update repository method Signed-off-by: Bogdan Bondar * fixed timing issue in amqp message handler integration test, when validating target attributes update Signed-off-by: Bogdan Bondar --- ...pMessageHandlerServiceIntegrationTest.java | 35 +-- .../AmqpServiceIntegrationTest.java | 20 +- .../repository/DeploymentManagement.java | 12 + .../repository/AbstractRolloutManagement.java | 16 +- .../repository/jpa/ActionRepository.java | 44 +++- .../jpa/JpaControllerManagement.java | 27 +-- .../jpa/JpaDeploymentManagement.java | 209 +++++++++++------- .../repository/jpa/JpaRolloutManagement.java | 40 ++-- .../repository/jpa/JpaSystemManagement.java | 12 +- .../jpa/{ => utils}/DeploymentHelper.java | 61 ++--- .../repository/jpa/utils/QuotaHelper.java | 2 - 11 files changed, 277 insertions(+), 201 deletions(-) rename hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/{ => utils}/DeploymentHelper.java (58%) diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java index dffa16dba..e099030ad 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java @@ -655,7 +655,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra // update mode REPLACE updateAttributesWithUpdateModeReplace(controllerId); - // update mode REPLACE + // update mode MERGE updateAttributesWithUpdateModeMerge(controllerId); // update mode REMOVE @@ -675,6 +675,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra final Map removeAttributes = new HashMap<>(); removeAttributes.put("k1", "foo"); removeAttributes.put("k3", "bar"); + final DmfAttributeUpdate remove = new DmfAttributeUpdate(); remove.setMode(DmfUpdateMode.REMOVE); remove.getAttributes().putAll(removeAttributes); @@ -694,6 +695,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra final Map mergeAttributes = new HashMap<>(); mergeAttributes.put("k1", "v1_modified_again"); mergeAttributes.put("k4", "v4"); + final DmfAttributeUpdate merge = new DmfAttributeUpdate(); merge.setMode(DmfUpdateMode.MERGE); merge.getAttributes().putAll(mergeAttributes); @@ -710,33 +712,33 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra private void updateAttributesWithUpdateModeReplace(final String controllerId) { // send a update message with update mode REPLACE - final Map replacementAttributes = new HashMap<>(); - replacementAttributes.put("k1", "v1_modified"); - replacementAttributes.put("k2", "v2"); - replacementAttributes.put("k3", "v3"); + final Map expectedAttributes = new HashMap<>(); + expectedAttributes.put("k1", "v1_modified"); + expectedAttributes.put("k2", "v2"); + expectedAttributes.put("k3", "v3"); + final DmfAttributeUpdate replace = new DmfAttributeUpdate(); replace.setMode(DmfUpdateMode.REPLACE); - replace.getAttributes().putAll(replacementAttributes); + replace.getAttributes().putAll(expectedAttributes); sendUpdateAttributeMessage(controllerId, TENANT_EXIST, replace); // validate - final Map expectedAttributes = replacementAttributes; assertUpdateAttributes(controllerId, expectedAttributes); } @Step private void updateAttributesWithoutUpdateMode(final String controllerId) { - // send a update message which does not specify a update mode - final Map initialAttributes = new HashMap<>(); - initialAttributes.put("k0", "v0"); - initialAttributes.put("k1", "v1"); + // send a update message which does not specify an update mode + final Map expectedAttributes = new HashMap<>(); + expectedAttributes.put("k0", "v0"); + expectedAttributes.put("k1", "v1"); + final DmfAttributeUpdate defaultUpdate = new DmfAttributeUpdate(); - defaultUpdate.getAttributes().putAll(initialAttributes); + defaultUpdate.getAttributes().putAll(expectedAttributes); sendUpdateAttributeMessage(controllerId, TENANT_EXIST, defaultUpdate); // validate - final Map expectedAttributes = initialAttributes; assertUpdateAttributes(controllerId, expectedAttributes); } @@ -807,7 +809,8 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra verifyNumberOfDeadLetterMessages(3); } - private void sendUpdateAttributesMessageWithGivenAttributes(final String target, final String key, final String value) { + private void sendUpdateAttributesMessageWithGivenAttributes(final String target, final String key, + final String value) { final DmfAttributeUpdate controllerAttribute = new DmfAttributeUpdate(); controllerAttribute.getAttributes().put(key, value); final Message message = createUpdateAttributesMessage(target, TENANT_EXIST, controllerAttribute); @@ -893,7 +896,7 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra private void verifyNumberOfDeadLetterMessages(final int numberOfInvocations) { assertEmptyReceiverQueueCount(); - createConditionFactory() - .until(() -> Mockito.verify(getDeadletterListener(), Mockito.times(numberOfInvocations)).handleMessage(Mockito.any())); + createConditionFactory().until(() -> Mockito.verify(getDeadletterListener(), Mockito.times(numberOfInvocations)) + .handleMessage(Mockito.any())); } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java index 72022ee9a..c3baeb155 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java @@ -152,7 +152,7 @@ public abstract class AmqpServiceIntegrationTest extends AbstractAmqpIntegration protected void assertRequestAttributesUpdateMessage(final String target) { assertReplyMessageHeader(EventTopic.REQUEST_ATTRIBUTES_UPDATE, target); } - + protected void assertRequestAttributesUpdateMessageAbsent() { assertThat(replyToListener.getEventTopicMessages()).doesNotContainKey(EventTopic.REQUEST_ATTRIBUTES_UPDATE); } @@ -352,12 +352,18 @@ public abstract class AmqpServiceIntegrationTest extends AbstractAmqpIntegration @Step protected void assertUpdateAttributes(final String controllerId, final Map attributes) { - final Target findByControllerId = waitUntilIsPresent( - () -> controllerManagement.getByControllerId(controllerId)); - final Map controllerAttributes = targetManagement - .getControllerAttributes(findByControllerId.getControllerId()); - assertThat(controllerAttributes.size()).isEqualTo(attributes.size()); - attributes.forEach((k, v) -> assertKeyValueInMap(k, v, controllerAttributes)); + waitUntilIsPresent(() -> controllerManagement.getByControllerId(controllerId)); + + createConditionFactory().until(() -> { + try { + final Map controllerAttributes = securityRule + .runAsPrivileged(() -> targetManagement.getControllerAttributes(controllerId)); + assertThat(controllerAttributes.size()).isEqualTo(attributes.size()); + attributes.forEach((k, v) -> assertKeyValueInMap(k, v, controllerAttributes)); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); } private void assertKeyValueInMap(final String key, final String value, diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index f4e6f23ff..7fbb98972 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository; import java.util.Collection; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -411,6 +412,17 @@ public interface DeploymentManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) Action forceTargetAction(long actionId); + /** + * Sets the status of inactive scheduled {@link Action}s for the specified + * {@link Target}s to {@link Status#CANCELED} + * + * @param targetIds + * ids of the {@link Target}s the actions belong to + * + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) + void cancelInactiveScheduledActionsForTargets(List targetIds); + /** * Starts all scheduled actions of an RolloutGroup parent. * diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java index 201f9b4b9..c33cff05d 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/AbstractRolloutManagement.java @@ -27,10 +27,6 @@ import org.springframework.integration.support.locks.LockRegistry; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.support.DefaultTransactionDefinition; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.concurrent.ListenableFuture; /** @@ -65,8 +61,8 @@ public abstract class AbstractRolloutManagement implements RolloutManagement { final DeploymentManagement deploymentManagement, final RolloutGroupManagement rolloutGroupManagement, final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer, - final PlatformTransactionManager txManager, final TenantAware tenantAware, - final LockRegistry lockRegistry, final RolloutApprovalStrategy rolloutApprovalStrategy) { + final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, + final RolloutApprovalStrategy rolloutApprovalStrategy) { this.targetManagement = targetManagement; this.deploymentManagement = deploymentManagement; this.rolloutGroupManagement = rolloutGroupManagement; @@ -80,14 +76,6 @@ public abstract class AbstractRolloutManagement implements RolloutManagement { this.rolloutApprovalStrategy = rolloutApprovalStrategy; } - protected Long runInNewTransaction(final String transactionName, final TransactionCallback action) { - final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setName(transactionName); - def.setReadOnly(false); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - return new TransactionTemplate(txManager, def).execute(action); - } - protected RolloutGroupsValidation validateTargetsInGroups(final List groups, final String baseFilter, final long totalTargets) { final List groupTargetCounts = new ArrayList<>(groups.size()); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index 1da3b9dd4..d2d99a0aa 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -207,10 +207,50 @@ public interface ActionRepository extends BaseEntityRepository, void switchStatus(@Param("statusToSet") Action.Status statusToSet, @Param("targetsIds") List targetIds, @Param("active") boolean active, @Param("currentStatus") Action.Status currentStatus); + /** + * + * Retrieves all IDs for {@link Action}s referring to the given target IDs, + * active flag, current status and distribution set not requiring migration + * step. + * + * @param targetIds + * the IDs of targets for the actions + * @param active + * flag to indicate active/inactive actions + * @param currentStatus + * the current status of the actions + * @return the found list of {@link Action} IDs + */ + @Query("SELECT a.id FROM JpaAction a WHERE a.target IN :targetsIds AND a.active = :active AND a.status = :currentStatus AND a.distributionSet.requiredMigrationStep = false") + List findByTargetIdInAndIsActiveAndActionStatusAndDistributionSetNotRequiredMigrationStep( + @Param("targetsIds") List targetIds, @Param("active") boolean active, + @Param("currentStatus") Action.Status currentStatus); + + /** + * Switches the status of actions from one specific status into another for + * given actions IDs, active flag and current status + * + * @param statusToSet + * the new status the actions should get + * @param actionIds + * the IDs of the actions which are affected + * @param active + * flag to indicate active/inactive actions + * @param currentStatus + * the current status of the actions + * @return the amount of updated actions + */ + @Modifying + @Transactional + @Query("UPDATE JpaAction a SET a.status = :statusToSet WHERE a.id IN :actionIds AND a.active = :active AND a.status = :currentStatus") + int switchStatusForActionIdInAndIsActiveAndActionStatus(@Param("statusToSet") Action.Status statusToSet, + @Param("actionIds") List actionIds, @Param("active") boolean active, + @Param("currentStatus") Action.Status currentStatus); + /** * * Retrieves all {@link Action}s which are active and referring to the given - * target Ids and distribution set required migration step. + * target Ids and distribution set not requiring migration step. * * @param targetIds * the IDs of targets for the actions @@ -226,7 +266,7 @@ public interface ActionRepository extends BaseEntityRepository, /** * * Retrieves all {@link Action}s which are active and referring to the given - * target Ids and distribution set required migration step. + * target Ids and distribution set not requiring migration step. * * @param targetIds * the IDs of targets for the actions diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index 693f68a77..81858c11c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -60,6 +60,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications; +import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -88,12 +89,9 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.validation.annotation.Validated; import com.google.common.base.Joiner; @@ -178,14 +176,6 @@ public class JpaControllerManagement implements ControllerManagement { this.repositoryProperties = repositoryProperties; } - private T runInNewTransaction(final String transactionName, final TransactionCallback action) { - final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setName(transactionName); - def.setReadOnly(false); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - return new TransactionTemplate(txManager, def).execute(action); - } - @Override public String getPollingTime() { return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement @@ -423,7 +413,8 @@ public class JpaControllerManagement implements ControllerManagement { try { events.stream().collect(Collectors.groupingBy(TargetPoll::getTenant)).forEach((tenant, polls) -> { final TransactionCallback createTransaction = status -> updateLastTargetQueries(tenant, polls); - tenantAware.runAsTenant(tenant, () -> runInNewTransaction("flushUpdateQueue", createTransaction)); + tenantAware.runAsTenant(tenant, + () -> DeploymentHelper.runInNewTransaction(txManager, "flushUpdateQueue", createTransaction)); }); } catch (final RuntimeException ex) { LOG.error("Failed to persist UpdateQueue content.", ex); @@ -605,8 +596,8 @@ public class JpaControllerManagement implements ControllerManagement { switch (actionStatus.getStatus()) { case ERROR: - final JpaTarget target = DeploymentHelper.updateTargetInfo((JpaTarget) action.getTarget(), - TargetUpdateStatus.ERROR, false); + final JpaTarget target = (JpaTarget) action.getTarget(); + target.setUpdateStatus(TargetUpdateStatus.ERROR); handleErrorOnAction(action, target); break; case FINISHED: @@ -626,7 +617,7 @@ public class JpaControllerManagement implements ControllerManagement { if (controllerId != null) { targetManagement.requestControllerAttributes(controllerId); } - + return savedAction; } @@ -673,10 +664,10 @@ public class JpaControllerManagement implements ControllerManagement { final UpdateMode mode) { /* - Constraints on attribute keys & values are not validated by EclipseLink. Hence, they are validated here. + * Constraints on attribute keys & values are not validated by + * EclipseLink. Hence, they are validated here. */ - if (data.entrySet().stream() - .anyMatch(e -> !isAttributeEntryValid(e))) { + if (data.entrySet().stream().anyMatch(e -> !isAttributeEntryValid(e))) { throw new InvalidTargetAttributeException(); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 4600293e4..83659dec3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -50,6 +50,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -82,11 +83,8 @@ import org.springframework.orm.jpa.vendor.Database; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.DefaultTransactionDefinition; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; @@ -142,7 +140,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { private final TenantAware tenantAware; private final Database database; - JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, + protected JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository, final DistributionSetRepository distributionSetRepository, final TargetRepository targetRepository, final ActionStatusRepository actionStatusRepository, final TargetManagement targetManagement, final AuditorAware auditorProvider, final ApplicationEventPublisher eventPublisher, @@ -253,102 +251,82 @@ public class JpaDeploymentManagement implements DeploymentManagement { final Collection targetsWithActionType, final String actionMessage, final AbstractDsAssignmentStrategy assignmentStrategy) { - final JpaDistributionSet set = distributionSetRepository.findOne(dsID); - if (set == null) { - throw new EntityNotFoundException(DistributionSet.class, dsID); - } + final JpaDistributionSet distributionSetEntity = getAndValidateDsById(dsID); + final List controllerIDs = getControllerIdsForAssignmentAndCheckQuota(targetsWithActionType, + distributionSetEntity); + final List targetEntities = assignmentStrategy.findTargetsForAssignment(controllerIDs, + distributionSetEntity.getId()); - if (!set.isComplete()) { - throw new IncompleteDistributionSetException( - "Distribution set of type " + set.getType().getKey() + " is incomplete: " + set.getId()); - } - - final List controllerIDs = targetsWithActionType.stream().map(TargetWithActionType::getControllerId) - .collect(Collectors.toList()); - - // enforce the 'max targets per manual assignment' quota - if (!controllerIDs.isEmpty()) { - assertMaxTargetsPerManualAssignmentQuota(set.getId(), controllerIDs.size()); - } - - LOG.debug("assignDistribution({}) to {} targets", set, controllerIDs.size()); - - final Map targetsWithActionMap = targetsWithActionType.stream() - .collect(Collectors.toMap(TargetWithActionType::getControllerId, Function.identity())); - - // split tIDs length into max entries in-statement because many database - // have constraint of max entries in in-statements e.g. Oracle with - // maximum 1000 elements, so we need to split the entries here and - // execute multiple statements we take the target only into account if - // the requested operation is no duplicate of a previous one - final List targets = assignmentStrategy.findTargetsForAssignment(controllerIDs, set.getId()); - - if (targets.isEmpty()) { + if (targetEntities.isEmpty()) { // detaching as it is not necessary to persist the set itself - entityManager.detach(set); + entityManager.detach(distributionSetEntity); // return with nothing as all targets had the DS already assigned return new DistributionSetAssignmentResult(Collections.emptyList(), 0, targetsWithActionType.size(), Collections.emptyList(), targetManagement); } - final List> targetIds = Lists.partition( - targets.stream().map(Target::getId).collect(Collectors.toList()), Constants.MAX_ENTRIES_IN_STATEMENT); + // split tIDs length into max entries in-statement because many database + // have constraint of max entries in in-statements e.g. Oracle with + // maximum 1000 elements, so we need to split the entries here and + // execute multiple statements + final List> targetEntitiesIdsChunks = Lists.partition( + targetEntities.stream().map(Target::getId).collect(Collectors.toList()), + Constants.MAX_ENTRIES_IN_STATEMENT); // override all active actions and set them into canceling state, we // need to remember which one we have been switched to canceling state // because for targets which we have changed to canceling we don't want // to publish the new action update event. - final Set targetIdsCancellList; - - if (systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class) - .getValue())) { - targetIdsCancellList = Collections.emptySet(); - assignmentStrategy.closeActiveActions(targetIds); - } else { - targetIdsCancellList = assignmentStrategy.cancelActiveActions(targetIds); - } - + final Set cancelingTargetEntitiesIds = closeOrCancelActiveActions(assignmentStrategy, + targetEntitiesIdsChunks); // cancel all scheduled actions which are in-active, these actions were // not active before and the manual assignment which has been done - // cancels the - targetIds.forEach(tIds -> actionRepository.switchStatus(Status.CANCELED, tIds, false, Status.SCHEDULED)); + // cancels them + targetEntitiesIdsChunks.forEach(this::cancelInactiveScheduledActionsForTargets); - // set assigned distribution set and TargetUpdateStatus - final String currentUser; - if (auditorProvider != null) { - currentUser = auditorProvider.getCurrentAuditor(); - } else { - currentUser = null; - } - - assignmentStrategy.updateTargetStatus(set, targetIds, currentUser); - - final Map targetIdsToActions = targets.stream() - .map(trg -> createTargetAction(assignmentStrategy, targetsWithActionType, controllerIDs, targets, - targetsWithActionMap, trg, set)) - .map(actionRepository::save) - .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), Function.identity())); + setAssignedDistributionSetAndTargetUpdateStatus(assignmentStrategy, distributionSetEntity, + targetEntitiesIdsChunks); + final Map controllerIdsToActions = createActions(targetsWithActionType, targetEntities, + assignmentStrategy, controllerIDs, distributionSetEntity); // create initial action status when action is created so we remember // the initial running status because we will change the status // of the action itself and with this action status we have a nicer // action history. - actionStatusRepository.save(targetIdsToActions.values().stream() - .map(action -> assignmentStrategy.createActionStatus(action, actionMessage)) - .collect(Collectors.toList())); + createActionsStatus(controllerIdsToActions.values(), assignmentStrategy, actionMessage); - // detaching as it is not necessary to persist the set itself - entityManager.detach(set); - // detaching as the entity has been updated by the JPQL query above - targets.forEach(entityManager::detach); - - assignmentStrategy.sendAssignmentEvents(set, targets, targetIdsCancellList, targetIdsToActions); + detachEntitiesAndSendAssignmentEvents(distributionSetEntity, targetEntities, assignmentStrategy, + cancelingTargetEntitiesIds, controllerIdsToActions); return new DistributionSetAssignmentResult( - targets.stream().map(Target::getControllerId).collect(Collectors.toList()), targets.size(), - controllerIDs.size() - targets.size(), Lists.newArrayList(targetIdsToActions.values()), - targetManagement); + targetEntities.stream().map(Target::getControllerId).collect(Collectors.toList()), + targetEntities.size(), controllerIDs.size() - targetEntities.size(), + Lists.newArrayList(controllerIdsToActions.values()), targetManagement); + } + + private JpaDistributionSet getAndValidateDsById(final Long dsID) { + final JpaDistributionSet distributionSet = distributionSetRepository.findById(dsID) + .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, dsID)); + + if (!distributionSet.isComplete()) { + throw new IncompleteDistributionSetException("Distribution set of type " + + distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId()); + } + + return distributionSet; + } + + private List getControllerIdsForAssignmentAndCheckQuota( + final Collection targetsWithActionType, final JpaDistributionSet distributionSet) { + final List controllerIDs = targetsWithActionType.stream().map(TargetWithActionType::getControllerId) + .collect(Collectors.toList()); + + // enforce the 'max targets per manual assignment' quota + if (!controllerIDs.isEmpty()) { + assertMaxTargetsPerManualAssignmentQuota(distributionSet.getId(), controllerIDs.size()); + } + + return controllerIDs; } /** @@ -360,9 +338,71 @@ public class JpaDeploymentManagement implements DeploymentManagement { * @param requested * number of targets to check */ - private void assertMaxTargetsPerManualAssignmentQuota(final Long id, final int requested) { - QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxTargetsPerManualAssignment(), - Target.class, DistributionSet.class, null); + private void assertMaxTargetsPerManualAssignmentQuota(final Long distributionSetId, + final int requestedTargetsCount) { + QuotaHelper.assertAssignmentQuota(distributionSetId, requestedTargetsCount, + quotaManagement.getMaxTargetsPerManualAssignment(), Target.class, DistributionSet.class, null); + } + + private Set closeOrCancelActiveActions(final AbstractDsAssignmentStrategy assignmentStrategy, + final List> targetIdsChunks) { + if (isActionsAutocloseEnabled()) { + assignmentStrategy.closeActiveActions(targetIdsChunks); + return Collections.emptySet(); + } else { + return assignmentStrategy.cancelActiveActions(targetIdsChunks); + } + } + + protected boolean isActionsAutocloseEnabled() { + return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement + .getConfigurationValue(TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED, Boolean.class) + .getValue()); + } + + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + @Retryable(include = { + ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) + public void cancelInactiveScheduledActionsForTargets(final List targetIds) { + actionRepository.switchStatus(Status.CANCELED, targetIds, false, Status.SCHEDULED); + } + + private void setAssignedDistributionSetAndTargetUpdateStatus(final AbstractDsAssignmentStrategy assignmentStrategy, + final JpaDistributionSet set, final List> targetIdsChunks) { + final String currentUser = auditorProvider != null ? auditorProvider.getCurrentAuditor() : null; + assignmentStrategy.updateTargetStatus(set, targetIdsChunks, currentUser); + } + + private Map createActions(final Collection targetsWithActionType, + final List targets, final AbstractDsAssignmentStrategy assignmentStrategy, + final List controllerIDs, final JpaDistributionSet set) { + final Map targetsWithActionMap = targetsWithActionType.stream() + .collect(Collectors.toMap(TargetWithActionType::getControllerId, Function.identity())); + + return targets.stream() + .map(trg -> createTargetAction(assignmentStrategy, targetsWithActionType, controllerIDs, targets, + targetsWithActionMap, trg, set)) + .map(actionRepository::save) + .collect(Collectors.toMap(action -> action.getTarget().getControllerId(), Function.identity())); + } + + private void createActionsStatus(final Collection actions, + final AbstractDsAssignmentStrategy assignmentStrategy, final String actionMessage) { + actionStatusRepository + .save(actions.stream().map(action -> assignmentStrategy.createActionStatus(action, actionMessage)) + .collect(Collectors.toList())); + } + + private void detachEntitiesAndSendAssignmentEvents(final JpaDistributionSet set, final List targets, + final AbstractDsAssignmentStrategy assignmentStrategy, final Set targetIdsCancellList, + final Map controllerIdsToActions) { + // detaching as it is not necessary to persist the set itself + entityManager.detach(set); + // detaching as the entity has been updated by the JPQL query above + targets.forEach(entityManager::detach); + + assignmentStrategy.sendAssignmentEvents(set, targets, targetIdsCancellList, controllerIdsToActions); } @Override @@ -439,11 +479,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { private long startScheduledActionsByRolloutGroupParentInNewTransaction(final Long rolloutId, final Long distributionSetId, final Long rolloutGroupParentId, final int limit) { - final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setName("startScheduledActions-" + rolloutId); - def.setReadOnly(false); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - return new TransactionTemplate(txManager, def).execute(status -> { + return DeploymentHelper.runInNewTransaction(txManager, "startScheduledActions-" + rolloutId, status -> { final Page rolloutGroupActions = findActionsByRolloutAndRolloutGroupParent(rolloutId, rolloutGroupParentId, limit); @@ -797,4 +833,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { } } + protected ActionRepository getActionRepository() { + return actionRepository; + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 05b2e7092..c116c0b83 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -53,6 +53,7 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupConditio import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; +import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; @@ -158,7 +159,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { final DistributionSetManagement distributionSetManagement, final ApplicationContext context, final ApplicationEventPublisher eventPublisher, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, final TenantAware tenantAware, final LockRegistry lockRegistry, - final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy) { + final Database database, final RolloutApprovalStrategy rolloutApprovalStrategy) { super(targetManagement, deploymentManagement, rolloutGroupManagement, distributionSetManagement, context, eventPublisher, virtualPropertyReplacer, txManager, tenantAware, lockRegistry, rolloutApprovalStrategy); this.database = database; @@ -361,7 +362,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { if (!rolloutApprovalStrategy.isApprovalNeeded(rollout)) { rollout.setStatus(RolloutStatus.READY); LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId()); - } else { + } else { LOGGER.debug("rollout {} creation done. Switch to WAITING_FOR_APPROVAL.", rollout.getId()); rollout.setStatus(RolloutStatus.WAITING_FOR_APPROVAL); rolloutApprovalStrategy.onApprovalRequired(rollout); @@ -388,10 +389,12 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), RolloutGroupStatus.READY, group); - final long targetsInGroupFilter = runInNewTransaction("countAllTargetsByTargetFilterQueryAndNotInRolloutGroups", + final long targetsInGroupFilter = DeploymentHelper.runInNewTransaction(txManager, + "countAllTargetsByTargetFilterQueryAndNotInRolloutGroups", count -> targetManagement.countByRsqlAndNotInRolloutGroups(readyGroups, groupTargetFilter)); final long expectedInGroup = Math.round(group.getTargetPercentage() / 100 * (double) targetsInGroupFilter); - final long currentlyInGroup = runInNewTransaction("countRolloutTargetGroupByRolloutGroup", + final long currentlyInGroup = DeploymentHelper.runInNewTransaction(txManager, + "countRolloutTargetGroupByRolloutGroup", count -> rolloutTargetGroupRepository.countByRolloutGroup(group)); // Switch the Group status to READY, when there are enough Targets in @@ -413,8 +416,9 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } while (targetsLeftToAdd > 0); group.setStatus(RolloutGroupStatus.READY); - group.setTotalTargets(runInNewTransaction("countRolloutTargetGroupByRolloutGroup", - count -> rolloutTargetGroupRepository.countByRolloutGroup(group)).intValue()); + group.setTotalTargets( + DeploymentHelper.runInNewTransaction(txManager, "countRolloutTargetGroupByRolloutGroup", + count -> rolloutTargetGroupRepository.countByRolloutGroup(group)).intValue()); return rolloutGroupRepository.save(group); } catch (final TransactionException e) { @@ -426,7 +430,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { private Long assignTargetsToGroupInNewTransaction(final JpaRollout rollout, final RolloutGroup group, final String targetFilter, final long limit) { - return runInNewTransaction("assignTargetsToRolloutGroup", status -> { + return DeploymentHelper.runInNewTransaction(txManager, "assignTargetsToRolloutGroup", status -> { final PageRequest pageRequest = new PageRequest(0, Math.toIntExact(limit)); final List readyGroups = RolloutHelper.getGroupsByStatusIncludingGroup(rollout.getRolloutGroups(), RolloutGroupStatus.READY, group); @@ -475,14 +479,14 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { final JpaRollout rollout = getRolloutAndThrowExceptionIfNotFound(rolloutId); RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.WAITING_FOR_APPROVAL); switch (decision) { - case APPROVED: - rollout.setStatus(RolloutStatus.READY); - break; - case DENIED: - rollout.setStatus(RolloutStatus.APPROVAL_DENIED); - break; - default: - throw new IllegalArgumentException("Unknown approval decision: " + decision); + case APPROVED: + rollout.setStatus(RolloutStatus.READY); + break; + case DENIED: + rollout.setStatus(RolloutStatus.APPROVAL_DENIED); + break; + default: + throw new IllegalArgumentException("Unknown approval decision: " + decision); } rollout.setApprovalDecidedBy(rolloutApprovalStrategy.getApprovalUser(rollout)); if (remark != null) { @@ -577,7 +581,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } private Long createActionsForTargetsInNewTransaction(final long rolloutId, final long groupId, final int limit) { - return runInNewTransaction("createActionsForTargets", status -> { + return DeploymentHelper.runInNewTransaction(txManager, "createActionsForTargets", status -> { final PageRequest pageRequest = new PageRequest(0, limit); final Rollout rollout = rolloutRepository.findOne(rolloutId); final RolloutGroup group = rolloutGroupRepository.findOne(groupId); @@ -608,7 +612,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { // current scheduled action to cancel. E.g. a new scheduled action is // created. final List targetIds = targets.stream().map(Target::getId).collect(Collectors.toList()); - actionRepository.switchStatus(Action.Status.CANCELED, targetIds, false, Action.Status.SCHEDULED); + deploymentManagement.cancelInactiveScheduledActionsForTargets(targetIds); targets.forEach(target -> { assertActionsPerTargetQuota(target, 1); @@ -821,7 +825,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } try { - rollouts.forEach(rolloutId -> runInNewTransaction(handlerId + "-" + rolloutId, + rollouts.forEach(rolloutId -> DeploymentHelper.runInNewTransaction(txManager, handlerId + "-" + rolloutId, status -> executeFittingHandler(rolloutId))); } finally { lock.unlock(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java index 0b92b0297..861c8bc24 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java @@ -25,6 +25,7 @@ import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactio import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; import org.eclipse.hawkbit.repository.jpa.model.JpaTenantMetaData; +import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.TenantMetaData; @@ -46,11 +47,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.DefaultTransactionDefinition; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.validation.annotation.Validated; /** @@ -207,12 +205,8 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst * @return the initial created {@link TenantMetaData} */ private TenantMetaData createInitialTenantMetaData(final String tenant) { - final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); - def.setName("initial-tenant-creation"); - def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - def.setReadOnly(false); - return systemSecurityContext - .runAsSystemAsTenant(() -> new TransactionTemplate(txManager, def).execute(status -> { + return systemSecurityContext.runAsSystemAsTenant( + () -> DeploymentHelper.runInNewTransaction(txManager, "initial-tenant-creation", status -> { final DistributionSetType defaultDsType = createStandardSoftwareDataSetup(); return tenantMetaDataRepository.save(new JpaTenantMetaData(defaultDsType, tenant)); }), tenant); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DeploymentHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java similarity index 58% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DeploymentHelper.java rename to hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java index bb3e7b0d5..cd248f9ca 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DeploymentHelper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/DeploymentHelper.java @@ -6,18 +6,25 @@ * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ -package org.eclipse.hawkbit.repository.jpa; +package org.eclipse.hawkbit.repository.jpa.utils; import java.util.List; import java.util.stream.Collectors; import javax.validation.constraints.NotNull; +import org.eclipse.hawkbit.repository.jpa.ActionRepository; +import org.eclipse.hawkbit.repository.jpa.TargetRepository; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; /** * Utility class for deployment related topics. @@ -29,31 +36,6 @@ public final class DeploymentHelper { // utility class } - /** - * Internal helper method used only inside service level. As a result is no - * additional security necessary. - * - * @param target - * to update - * @param status - * of the target - * @param setInstalledDate - * to set - * @param targetInfoRepository - * for the operation - * - * @return updated target - */ - static JpaTarget updateTargetInfo(@NotNull final JpaTarget target, @NotNull final TargetUpdateStatus status, - final boolean setInstalledDate) { - target.setUpdateStatus(status); - - if (setInstalledDate) { - target.setInstallationDate(System.currentTimeMillis()); - } - return target; - } - /** * This method is called, when cancellation has been successful. It sets the * action to canceled, resets the meta data of the target and in case there @@ -65,10 +47,8 @@ public final class DeploymentHelper { * for the operation * @param targetRepository * for the operation - * @param targetInfoRepository - * for the operation */ - static void successCancellation(final JpaAction action, final ActionRepository actionRepository, + public static void successCancellation(final JpaAction action, final ActionRepository actionRepository, final TargetRepository targetRepository) { // set action inactive @@ -81,11 +61,32 @@ public final class DeploymentHelper { if (nextActiveActions.isEmpty()) { target.setAssignedDistributionSet(target.getInstalledDistributionSet()); - updateTargetInfo(target, TargetUpdateStatus.IN_SYNC, false); + target.setUpdateStatus(TargetUpdateStatus.IN_SYNC); } else { target.setAssignedDistributionSet(nextActiveActions.get(0).getDistributionSet()); } + targetRepository.save(target); } + /** + * Executes the modifying action in new transaction + * + * @param txManager + * transaction manager interface + * @param transactionName + * the name of the new transaction + * @param action + * the callback to execute in new tranaction + * + * @return the result of the action + */ + public static T runInNewTransaction(@NotNull final PlatformTransactionManager txManager, + final String transactionName, @NotNull final TransactionCallback action) { + final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setName(transactionName); + def.setReadOnly(false); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + return new TransactionTemplate(txManager, def).execute(action); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java index f59fc983b..e7c1b1ba1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java @@ -15,12 +15,10 @@ import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.validation.annotation.Validated; /** * Helper class to check assignment quotas. */ -@Validated public final class QuotaHelper { /**