Skip to content

Commit

Permalink
[#1580] Software Module & Distribution Set lock: implicit (#1649)
Browse files Browse the repository at this point in the history
Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
  • Loading branch information
avgustinmm committed Feb 18, 2024
1 parent 94576bd commit 9e76223
Show file tree
Hide file tree
Showing 34 changed files with 629 additions and 292 deletions.
Expand Up @@ -206,9 +206,14 @@ public enum SpServerError {
SP_DS_INCOMPLETE("hawkbit.server.error.distributionset.incomplete",
"Distribution set is assigned/locked to a target that is incomplete (i.e. mandatory modules are missing)"),
/**
*
* error message, which describes that an entity is locked and can't be functionally modified
*/
SP_LOCKED("hawkbit.server.error.locked", "Entry is locked. Could not be functionally modified"),

/**
* error message, which describes that an entity is locked and can't be functionally modified
*/
SP_LOCKED("hawkbit.server.error.locked", "Entry is locked. Could not be modified"),
SP_DELETED("hawkbit.server.error.deleted", "Entry is soft deleted"),

/**
*
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.exception;

import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.repository.model.BaseEntity;

import java.io.Serial;

/**
* Thrown if assignment quota is exceeded
*/
public class DeletedException extends AbstractServerRtException {

@Serial
private static final long serialVersionUID = 1L;

private static final SpServerError THIS_ERROR = SpServerError.SP_DELETED;

public DeletedException(
final Class<? extends BaseEntity> type, final Object entityId) {
super(type.getSimpleName() + " with given identifier {" + entityId + "} is soft-deleted!",
THIS_ERROR);
}
}
Expand Up @@ -16,20 +16,18 @@
import java.io.Serial;

/**
* Thrown if assignment quota is exceeded
* Thrown if there is attempt to functionally modify a locked entity
*/
public class LockedException extends AbstractServerRtException {

@Serial
private static final long serialVersionUID = 1L;

private static final String ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE = "Quota exceeded: Cannot assign %s more %s entities to %s '%s'. The maximum is %s.";
private static final SpServerError THIS_ERROR = SpServerError.SP_LOCKED;

public LockedException(
final Class<? extends BaseEntity> type, final Object entityId, final String operation) {
super(
type.getSimpleName() + " with given identifier {" + entityId + "} is locked and " + operation +
super(type.getSimpleName() + " with given identifier {" + entityId + "} is locked and " + operation +
" is forbidden!",
THIS_ERROR);
}
Expand Down
Expand Up @@ -329,10 +329,11 @@ private static Map<Long, List<TargetWithActionType>> convertRequest(
}

private DistributionSetAssignmentResult assignDistributionSetToTargetsWithRetry(final String initiatedBy,
final Long dsID, final Collection<TargetWithActionType> targetsWithActionType, final String actionMessage,
final Long dsId, final Collection<TargetWithActionType> targetsWithActionType, final String actionMessage,
final AbstractDsAssignmentStrategy assignmentStrategy) {
final RetryCallback<DistributionSetAssignmentResult, ConcurrencyFailureException> retryCallback = retryContext -> assignDistributionSetToTargets(
initiatedBy, dsID, targetsWithActionType, actionMessage, assignmentStrategy);
final RetryCallback<DistributionSetAssignmentResult, ConcurrencyFailureException> retryCallback =
retryContext -> assignDistributionSetToTargets(
initiatedBy, dsId, targetsWithActionType, actionMessage, assignmentStrategy);
return retryTemplate.execute(retryCallback);
}

Expand All @@ -351,7 +352,7 @@ private DistributionSetAssignmentResult assignDistributionSetToTargetsWithRetry(
*
* @param initiatedBy
* the username of the user who initiated the assignment
* @param dsID
* @param dsId
* the ID of the distribution set to assign
* @param targetsWithActionType
* a list of all targets and their action type
Expand All @@ -365,12 +366,20 @@ private DistributionSetAssignmentResult assignDistributionSetToTargetsWithRetry(
* if mandatory {@link SoftwareModuleType} are not assigned as
* define by the {@link DistributionSetType}.
*/
private DistributionSetAssignmentResult assignDistributionSetToTargets(final String initiatedBy, final Long dsID,
private DistributionSetAssignmentResult assignDistributionSetToTargets(final String initiatedBy, final Long dsId,
final Collection<TargetWithActionType> targetsWithActionType, final String actionMessage,
final AbstractDsAssignmentStrategy assignmentStrategy) {
final JpaDistributionSet distributionSet =
(JpaDistributionSet) distributionSetManagement.getValidAndComplete(dsId);
// implicit lock
if (!distributionSet.isLocked()) {
// without new transaction DS changed event is not thrown
DeploymentHelper.runInNewTransaction(txManager, "Implicit lock", status -> {
distributionSetManagement.lock(distributionSet.getId());
return null;
});
}

final JpaDistributionSet distributionSetEntity = (JpaDistributionSet) distributionSetManagement
.getValidAndComplete(dsID);
final List<String> providedTargetIds = targetsWithActionType.stream().map(TargetWithActionType::getControllerId)
.distinct().toList();

Expand All @@ -381,19 +390,19 @@ private DistributionSetAssignmentResult assignDistributionSetToTargets(final Str
.flatMap(List::stream).map(JpaTarget::getControllerId).toList();

final List<JpaTarget> targetEntities = assignmentStrategy.findTargetsForAssignment(existingTargetIds,
distributionSetEntity.getId());
distributionSet.getId());

if (targetEntities.isEmpty()) {
return allTargetsAlreadyAssignedResult(distributionSetEntity, existingTargetIds.size());
return allTargetsAlreadyAssignedResult(distributionSet, existingTargetIds.size());
}

final List<TargetWithActionType> existingTargetsWithActionType = targetsWithActionType.stream()
.filter(target -> existingTargetIds.contains(target.getControllerId())).toList();

final List<JpaAction> assignedActions = doAssignDistributionSetToTargets(initiatedBy,
existingTargetsWithActionType, actionMessage, assignmentStrategy, distributionSetEntity,
existingTargetsWithActionType, actionMessage, assignmentStrategy, distributionSet,
targetEntities);
return buildAssignmentResult(distributionSetEntity, assignedActions, existingTargetsWithActionType.size());
return buildAssignmentResult(distributionSet, assignedActions, existingTargetsWithActionType.size());
}

private DistributionSetAssignmentResult allTargetsAlreadyAssignedResult(
Expand Down
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.RolloutManagement;
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.StopRolloutException;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
Expand Down Expand Up @@ -94,7 +95,6 @@ public void invalidateDistributionSet(final DistributionSetInvalidation distribu
// no lock is needed as no rollout will be stopped
invalidateDistributionSetsInTransaction(distributionSetInvalidation, tenant);
}

}

private void invalidateDistributionSetsInTransaction(final DistributionSetInvalidation distributionSetInvalidation,
Expand All @@ -108,21 +108,25 @@ private void invalidateDistributionSetsInTransaction(final DistributionSetInvali

private void invalidateDistributionSet(final long setId, final CancelationType cancelationType,
final boolean cancelRollouts) {
final DistributionSet set = distributionSetManagement.getValidAndComplete(setId);
distributionSetManagement.invalidate(set);
final DistributionSet distributionSet = distributionSetManagement.getOrElseThrowException(setId);
if (!distributionSet.isComplete()) {
throw new IncompleteDistributionSetException("Distribution set of type "
+ distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId());
}
distributionSetManagement.invalidate(distributionSet);
log.debug("Distribution set {} marked as invalid.", setId);

// rollout cancellation should only be permitted with UPDATE_ROLLOUT permission
if (shouldRolloutsBeCanceled(cancelationType, cancelRollouts)) {
log.debug("Cancel rollouts after ds invalidation. ID: {}", setId);
rolloutManagement.cancelRolloutsForDistributionSet(set);
rolloutManagement.cancelRolloutsForDistributionSet(distributionSet);
}

// Do run as system to ensure all actions (even invisible) are canceled due to invalidation.
systemSecurityContext.runAsSystem(() -> {
if (cancelationType != CancelationType.NONE) {
log.debug("Cancel actions after ds invalidation. ID: {}", setId);
deploymentManagement.cancelActionsForDistributionSet(cancelationType, set);
deploymentManagement.cancelActionsForDistributionSet(cancelationType, distributionSet);
}

log.debug("Cancel auto assignments after ds invalidation. ID: {}", setId);
Expand Down
Expand Up @@ -34,6 +34,7 @@
import org.eclipse.hawkbit.repository.builder.DistributionSetUpdate;
import org.eclipse.hawkbit.repository.builder.GenericDistributionSetUpdate;
import org.eclipse.hawkbit.repository.event.remote.DistributionSetDeletedEvent;
import org.eclipse.hawkbit.repository.exception.DeletedException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
Expand Down Expand Up @@ -697,6 +698,13 @@ public DistributionSet unAssignTag(final long id, final long dsTagId) {
public void lock(final long id) {
final JpaDistributionSet distributionSet = getById(id);
if (!distributionSet.isLocked()) {
distributionSet.getModules().forEach(module -> {
if (!module.isLocked()) {
final JpaSoftwareModule jpaSoftwareModule = (JpaSoftwareModule)module;
jpaSoftwareModule.lock();
softwareModuleRepository.save(jpaSoftwareModule);
}
});
distributionSet.lock();
distributionSetRepository.save(distributionSet);
}
Expand Down Expand Up @@ -780,6 +788,10 @@ public DistributionSet getValidAndComplete(final long id) {
+ distributionSet.getType().getKey() + " is incomplete: " + distributionSet.getId());
}

if (distributionSet.isDeleted()) {
throw new DeletedException(DistributionSet.class, id);
}

return distributionSet;
}

Expand Down
Expand Up @@ -49,6 +49,7 @@
import org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaRollout;
import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup;
import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_;
Expand Down Expand Up @@ -205,22 +206,29 @@ public Rollout create(final RolloutCreate rollout, final List<RolloutGroupCreate

private JpaRollout createRollout(final JpaRollout rollout) {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(rollout);
long totalTargets;
String errMsg;
final JpaDistributionSet distributionSet = (JpaDistributionSet) rollout.getDistributionSet();
final long totalTargets;
final String errMsg;
if (RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
totalTargets = targetManagement.countByFailedInRollout(
RolloutHelper.getIdFromRetriedTargetFilter(rollout.getTargetFilterQuery()),
rollout.getDistributionSet().getType().getId());
distributionSet.getType().getId());
errMsg = "No failed targets in Rollout";
} else {
totalTargets = targetManagement.countByRsqlAndCompatible(rollout.getTargetFilterQuery(),
rollout.getDistributionSet().getType().getId());
distributionSet.getType().getId());
errMsg = "Rollout does not match any existing targets";
}
if (totalTargets == 0) {
throw new ValidationException(errMsg);
}
rollout.setTotalTargets(totalTargets);

// implicit lock
if (!distributionSet.isLocked()) {
distributionSetManagement.lock(distributionSet.getId());
}

if (rollout.getWeight().isEmpty()) {
rollout.setWeight(repositoryProperties.getActionWeightIfAbsent());
}
Expand Down Expand Up @@ -526,7 +534,6 @@ public Rollout update(final RolloutUpdate u) {
rollout.setStartAt(update.getStartAt().orElse(null));
update.getSet().ifPresent(setId -> {
final DistributionSet set = distributionSetManagement.getValidAndComplete(setId);

rollout.setDistributionSet(set);
});
if (rolloutApprovalStrategy.isApprovalNeeded(rollout)) {
Expand Down
Expand Up @@ -276,11 +276,14 @@ public TargetFilterQuery updateAutoAssignDS(final AutoAssignDistributionSetUpdat
} else {
WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(update);
assertMaxTargetsQuota(targetFilterQuery.getQuery(), targetFilterQuery.getName(), update.getDsId());
final JpaDistributionSet ds = (JpaDistributionSet) distributionSetManagement
final JpaDistributionSet distributionSet = (JpaDistributionSet) distributionSetManagement
.getValidAndComplete(update.getDsId());
verifyDistributionSetAndThrowExceptionIfDeleted(ds);
// implicit lock
if (!distributionSet.isLocked()) {
distributionSetManagement.lock(distributionSet.getId());
}

targetFilterQuery.setAutoAssignDistributionSet(ds);
targetFilterQuery.setAutoAssignDistributionSet(distributionSet);
contextAware.getCurrentContext().ifPresent(targetFilterQuery::setAccessControlContext);
targetFilterQuery.setAutoAssignInitiatedBy(contextAware.getCurrentUsername());
targetFilterQuery.setAutoAssignActionType(sanitizeAutoAssignActionType(update.getActionType()));
Expand All @@ -299,12 +302,6 @@ private boolean isConfirmationFlowEnabled() {
.isConfirmationFlowEnabled();
}

private static void verifyDistributionSetAndThrowExceptionIfDeleted(final DistributionSet distributionSet) {
if (distributionSet.isDeleted()) {
throw new EntityNotFoundException(DistributionSet.class, distributionSet.getId());
}
}

private static ActionType sanitizeAutoAssignActionType(final ActionType actionType) {
if (actionType == null) {
return ActionType.FORCED;
Expand Down
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.SoftwareModule;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TestHelper {

public static void implicitLock(final DistributionSet set) {
((JpaDistributionSet) set).setOptLockRevision(set.getOptLockRevision() + 1);
}

public static void implicitLock(final SoftwareModule module) {
((JpaSoftwareModule) module).setOptLockRevision(module.getOptLockRevision() + 1);
}
}
Expand Up @@ -264,12 +264,17 @@ void verifyTagFilteringAndManagement() {
@Test
void verifyAutoAssignmentUsage() {
// permit all operations first to prepare test setup
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);

final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet readOnly = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
// has to lock them, otherwise implicit lock shall be made which require DistributionSet update permissions
distributionSetManagement.lock(permitted.getId());
distributionSetManagement.lock(readOnly.getId());
distributionSetManagement.lock(hidden.getId());

// entities created - reset rules
testAccessControlManger.deleteAllRules();
Expand Down

0 comments on commit 9e76223

Please sign in to comment.