From 0083d5538a32cd7e6ef25925b344e4316d28112f Mon Sep 17 00:00:00 2001 From: Vasil Ilchev Date: Tue, 13 Jan 2026 11:20:21 +0200 Subject: [PATCH] Introduce Pause Success Action (#2867) * Introduce Pause Success Action Signed-off-by: vasilchev * Instead of overriding SuccessAction, trigger next group from resume rollout Fix Rollout Mgmt Resource to accept new Pause Action Signed-off-by: vasilchev * Review findings Signed-off-by: vasilchev * Remove unused import --------- Signed-off-by: vasilchev --- .../rollout/MgmtRolloutSuccessAction.java | 3 +- .../resource/mapper/MgmtRolloutMapper.java | 16 +- .../repository/model/RolloutGroup.java | 3 +- .../jpa/JpaRepositoryConfiguration.java | 14 +- .../jpa/management/JpaRolloutManagement.java | 53 ++++++- ...a => AbstractPauseRolloutGroupAction.java} | 30 ++-- .../PauseRolloutGroupErrorAction.java | 45 ++++++ .../PauseRolloutGroupSuccessAction.java | 43 ++++++ .../jpa/scheduler/JpaRolloutExecutor.java | 22 +-- .../management/RolloutManagementFlowTest.java | 138 +++++++++++++++--- .../jpa/management/RolloutManagementTest.java | 4 +- .../repository/test/util/TestdataFactory.java | 15 +- 12 files changed, 299 insertions(+), 87 deletions(-) rename hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/{PauseRolloutGroupAction.java => AbstractPauseRolloutGroupAction.java} (61%) create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupErrorAction.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupSuccessAction.java diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutSuccessAction.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutSuccessAction.java index d3ab0f694..aeff05dbd 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutSuccessAction.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutSuccessAction.java @@ -38,6 +38,7 @@ public class MgmtRolloutSuccessAction { } public enum SuccessAction { - NEXTGROUP + NEXTGROUP, + PAUSE } } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRolloutMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRolloutMapper.java index b2d8b6fbf..bc3c5cf5a 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRolloutMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtRolloutMapper.java @@ -294,17 +294,17 @@ public final class MgmtRolloutMapper { } private static RolloutGroupSuccessAction map(final SuccessAction action) { - if (SuccessAction.NEXTGROUP == action) { - return RolloutGroupSuccessAction.NEXTGROUP; - } - throw new IllegalArgumentException("Success Action " + action + NOT_SUPPORTED); + return switch (action) { + case NEXTGROUP -> RolloutGroupSuccessAction.NEXTGROUP; + case PAUSE -> RolloutGroupSuccessAction.PAUSE; + }; } private static SuccessAction map(final RolloutGroupSuccessAction successAction) { - if (RolloutGroupSuccessAction.NEXTGROUP == successAction) { - return SuccessAction.NEXTGROUP; - } - throw new IllegalArgumentException("Rollout group success action " + successAction + NOT_SUPPORTED); + return switch (successAction) { + case NEXTGROUP -> SuccessAction.NEXTGROUP; + case PAUSE -> SuccessAction.PAUSE; + }; } private static ErrorAction map(final RolloutGroupErrorAction errorAction) { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java index 96c6a7c23..3043d2dc0 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroup.java @@ -169,6 +169,7 @@ public interface RolloutGroup extends NamedEntity { * is hit. */ enum RolloutGroupSuccessAction { - NEXTGROUP + NEXTGROUP, + PAUSE } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRepositoryConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRepositoryConfiguration.java index 06bbad913..4f48b5c79 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRepositoryConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRepositoryConfiguration.java @@ -70,7 +70,8 @@ import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository; import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository; import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository; import org.eclipse.hawkbit.repository.jpa.repository.TargetTypeRepository; -import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupAction; +import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupErrorAction; +import org.eclipse.hawkbit.repository.jpa.rollout.condition.PauseRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupActionEvaluator; import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupConditionEvaluator; import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluationManager; @@ -213,9 +214,9 @@ public class JpaRepositoryConfiguration { @Bean @ConditionalOnMissingBean - PauseRolloutGroupAction pauseRolloutGroupAction( + PauseRolloutGroupErrorAction pauseRolloutGroupErrorAction( final RolloutManagement rolloutManagement, final RolloutGroupRepository rolloutGroupRepository) { - return new PauseRolloutGroupAction(rolloutManagement, rolloutGroupRepository); + return new PauseRolloutGroupErrorAction(rolloutManagement, rolloutGroupRepository); } @Bean @@ -225,6 +226,13 @@ public class JpaRepositoryConfiguration { return new StartNextGroupRolloutGroupSuccessAction(rolloutGroupRepository, deploymentManagement); } + @Bean + @ConditionalOnMissingBean + PauseRolloutGroupSuccessAction pauseRolloutGroupSuccessAction(final RolloutManagement rolloutManagement, + final RolloutGroupRepository rolloutGroupRepository) { + return new PauseRolloutGroupSuccessAction(rolloutManagement, rolloutGroupRepository); + } + @Bean @ConditionalOnMissingBean ThresholdRolloutGroupErrorCondition thresholdRolloutGroupErrorCondition(final ActionRepository actionRepository) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java index 391838cf4..b9140c548 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaRolloutManagement.java @@ -66,6 +66,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository; import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository; +import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluationManager; import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications; import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification; @@ -86,8 +87,10 @@ import org.eclipse.hawkbit.repository.model.TotalTargetCountActionStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; import org.eclipse.hawkbit.repository.qfields.RolloutFields; import org.eclipse.hawkbit.utils.ObjectCopyUtil; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; +import org.springframework.context.annotation.Lazy; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -118,6 +121,9 @@ public class JpaRolloutManagement implements RolloutManagement { RolloutStatus.CREATING, RolloutStatus.READY, RolloutStatus.WAITING_FOR_APPROVAL, RolloutStatus.STARTING, RolloutStatus.RUNNING, RolloutStatus.PAUSED, RolloutStatus.APPROVAL_DENIED); + private static final Comparator ROLLOUT_GROUP_DESC_COMP = Comparator.comparingLong(RolloutGroup::getId).reversed(); + + private RolloutGroupEvaluationManager rolloutGroupEvaluationManager; @Value("${hawkbit.repository.jpa.management.rollout.max.actions.per.transaction:5000}") private int maxActions; @@ -164,6 +170,13 @@ public class JpaRolloutManagement implements RolloutManagement { quotaManagement, this::isMultiAssignmentsEnabled, this::isConfirmationFlowEnabled, repositoryProperties, null); } + @Autowired + @Lazy + private void setRolloutGroupEvaluationManager( + final RolloutGroupEvaluationManager rolloutGroupEvaluationManager) { + this.rolloutGroupEvaluationManager = rolloutGroupEvaluationManager; + } + public static String createRolloutLockKey(final String tenant) { return tenant + "-rollout"; } @@ -348,10 +361,34 @@ public class JpaRolloutManagement implements RolloutManagement { throw new RolloutIllegalStateException("Rollout can only be resumed in state paused but current state is " + rollout.getStatus().name().toLowerCase()); } + final List allStartedGroups = rollout.getRolloutGroups().stream() + .filter(g -> RolloutGroupStatus.SCHEDULED != g.getStatus()).toList(); + if (!allStartedGroups.isEmpty()) { + final RolloutGroup lastStartedGroup = allStartedGroups.get(allStartedGroups.size() - 1); + if (shouldStartNextGroupOnResume(rollout, lastStartedGroup)) { + startNextRolloutGroupAction.exec(rollout, lastStartedGroup); + } + } rollout.setStatus(RolloutStatus.RUNNING); rolloutRepository.save(rollout); } + /** + * Check if on resume of a paused rollout the next group shall be started directly. + * Cases where we need to manually start the next group: + * - last running group is in error state and there is still some old group in running state, only running groups would be evaluated which would leave Rollout in running state but no trigger new group + * - last running group has success action to PAUSE and the success condition is fulfilled + * @param rollout + * @param lastStartedGroup + * @return true if next group shall be started directly on resume, false otherwise + */ + private boolean shouldStartNextGroupOnResume(final JpaRollout rollout, final RolloutGroup lastStartedGroup) { + return lastStartedGroup.getStatus().equals(RolloutGroupStatus.ERROR) || + (lastStartedGroup.getSuccessAction() == RolloutGroup.RolloutGroupSuccessAction.PAUSE && + rolloutGroupEvaluationManager.getSuccessConditionEvaluator(lastStartedGroup.getSuccessCondition()) + .eval(rollout, lastStartedGroup, lastStartedGroup.getSuccessConditionExp())); + } + @Override @Transactional @Retryable(retryFor = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, @@ -482,13 +519,15 @@ public class JpaRolloutManagement implements RolloutManagement { throw new RolloutIllegalStateException("Rollout does not have any groups left to be triggered"); } - final RolloutGroup latestRunning = groups.stream() - .sorted(Comparator.comparingLong(RolloutGroup::getId).reversed()) - .filter(g -> RolloutGroupStatus.RUNNING.equals(g.getStatus())) - .findFirst() - .orElseThrow(() -> new RolloutIllegalStateException("No group is running")); - - startNextRolloutGroupAction.exec(rollout, latestRunning); + final List startedRolloutGroups = rollout.getRolloutGroups().stream() + .filter(group -> group.getStatus() != RolloutGroupStatus.SCHEDULED) + .sorted(ROLLOUT_GROUP_DESC_COMP) + .map(JpaRolloutGroup.class::cast) + .toList(); + if (startedRolloutGroups.isEmpty()) { + throw new RolloutIllegalStateException("Cannot find any started rollout group to trigger next from"); + } + startNextRolloutGroupAction.exec(rollout, startedRolloutGroups.get(0)); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/AbstractPauseRolloutGroupAction.java similarity index 61% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupAction.java rename to hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/AbstractPauseRolloutGroupAction.java index 400f7fd88..ed8a232ae 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/AbstractPauseRolloutGroupAction.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others + * Copyright (c) 2025 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -12,38 +12,27 @@ package org.eclipse.hawkbit.repository.jpa.rollout.condition; import static org.eclipse.hawkbit.context.AccessContext.asSystem; import org.eclipse.hawkbit.repository.RolloutManagement; -import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; -import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; /** - * Error action evaluator which pauses the whole {@link Rollout} and sets the - * current {@link RolloutGroup} to error. + * Abstract class for pausing a rollout group. + * + * @param the type of the action */ -public class PauseRolloutGroupAction implements RolloutGroupActionEvaluator { +public abstract class AbstractPauseRolloutGroupAction> implements RolloutGroupActionEvaluator { - private final RolloutManagement rolloutManagement; - private final RolloutGroupRepository rolloutGroupRepository; + protected final RolloutManagement rolloutManagement; + protected final RolloutGroupRepository rolloutGroupRepository; - public PauseRolloutGroupAction(final RolloutManagement rolloutManagement, + protected AbstractPauseRolloutGroupAction(final RolloutManagement rolloutManagement, final RolloutGroupRepository rolloutGroupRepository) { this.rolloutManagement = rolloutManagement; this.rolloutGroupRepository = rolloutGroupRepository; } - @Override - public RolloutGroup.RolloutGroupErrorAction getAction() { - return RolloutGroup.RolloutGroupErrorAction.PAUSE; - } - - @Override - public void exec(final Rollout rollout, final RolloutGroup rolloutG) { - final JpaRolloutGroup rolloutGroup = (JpaRolloutGroup) rolloutG; - - rolloutGroup.setStatus(RolloutGroupStatus.ERROR); - rolloutGroupRepository.save(rolloutGroup); + public void exec(final Rollout rollout, final RolloutGroup rolloutGroup) { // Refresh latest rollout state in order to avoid cases when // previous group have matched error condition and paused the rollout // and this one tries to pause the rollout too but throws an exception @@ -56,3 +45,4 @@ public class PauseRolloutGroupAction implements RolloutGroupActionEvaluator { + + public PauseRolloutGroupErrorAction(final RolloutManagement rolloutManagement, + final RolloutGroupRepository rolloutGroupRepository) { + super(rolloutManagement, rolloutGroupRepository); + } + + @Override + public RolloutGroup.RolloutGroupErrorAction getAction() { + return RolloutGroup.RolloutGroupErrorAction.PAUSE; + } + + @Override + public void exec(final Rollout rollout, final RolloutGroup rolloutG) { + // set rollout group status to error + final JpaRolloutGroup rolloutGroup = (JpaRolloutGroup) rolloutG; + rolloutGroup.setStatus(RolloutGroupStatus.ERROR); + rolloutGroupRepository.save(rolloutGroup); + // pause the rollout + super.exec(rollout, rolloutGroup); + } +} + diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupSuccessAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupSuccessAction.java new file mode 100644 index 000000000..60ff30db1 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/condition/PauseRolloutGroupSuccessAction.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 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.rollout.condition; + +import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.RolloutGroup; + +/** + * Success action evaluator which pauses the whole {@link Rollout}. + */ +public class PauseRolloutGroupSuccessAction + extends AbstractPauseRolloutGroupAction { + + public PauseRolloutGroupSuccessAction(final RolloutManagement rolloutManagement, + final RolloutGroupRepository rolloutGroupRepository) { + super(rolloutManagement, rolloutGroupRepository); + } + + @Override + public RolloutGroup.RolloutGroupSuccessAction getAction() { + return RolloutGroup.RolloutGroupSuccessAction.PAUSE; + } + + @Override + public void exec(final Rollout rollout, final RolloutGroup rolloutGroup) { + if (!rolloutGroupRepository + .findByParentIdAndStatus(rolloutGroup.getId(), RolloutGroup.RolloutGroupStatus.SCHEDULED).isEmpty()) { + // if there are still scheduled child groups, do pause the rollout, otherwise just let it in running state + super.exec(rollout, rolloutGroup); + } + } +} + + diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/scheduler/JpaRolloutExecutor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/scheduler/JpaRolloutExecutor.java index 0aa61b188..b14bf10a4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/scheduler/JpaRolloutExecutor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/scheduler/JpaRolloutExecutor.java @@ -111,7 +111,6 @@ public class JpaRolloutExecutor implements RolloutExecutor { */ private static final List DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES = List.of(Status.ERROR, Status.FINISHED, Status.CANCELED, Status.DOWNLOADED); - private static final Comparator DESC_COMP = Comparator.comparingLong(RolloutGroup::getId).reversed(); private static final String TRANSACTION_ASSIGNING_TARGETS_TO_ROLLOUT_GROUP_FAILED = "Transaction assigning Targets to RolloutGroup failed"; private final ActionRepository actionRepository; @@ -353,11 +352,10 @@ public class JpaRolloutExecutor implements RolloutExecutor { .filter(group -> group.getStatus() == RolloutGroupStatus.RUNNING) .map(JpaRolloutGroup.class::cast) .toList(); - if (runningGroups.isEmpty()) { // no running rollouts, probably there was an error somewhere at the latest group. And the latest group has // been switched from running into error state. So we need to find the latest group which - executeLatestRolloutGroup(rollout); + asSystem(() -> rolloutManagement.triggerNextGroup(rollout.getId())); } else { log.debug("Rollout {} has {} running groups", rollout.getId(), runningGroups.size()); executeRunningGroups(rollout, runningGroups, rollout.getRolloutGroups().get(rollout.getRolloutGroups().size() - 1)); @@ -410,18 +408,6 @@ public class JpaRolloutExecutor implements RolloutExecutor { return groupsActiveLeft == 0; } - private void executeLatestRolloutGroup(final JpaRollout rollout) { - final List latestRolloutGroup = rollout.getRolloutGroups().stream() - .filter(group -> group.getStatus() != RolloutGroupStatus.SCHEDULED) - .sorted(DESC_COMP) - .map(JpaRolloutGroup.class::cast) - .toList(); - if (latestRolloutGroup.isEmpty()) { - return; - } - executeRolloutGroupSuccessAction(rollout, latestRolloutGroup.get(0)); - } - // fakes getTotalTargets count to match expected for the last dynamic group // so the evaluation to use total targets to properly private RolloutGroup evalProxy(final RolloutGroup group) { @@ -530,7 +516,7 @@ public class JpaRolloutExecutor implements RolloutExecutor { .eval(rollout, evalProxy, rolloutGroup.getSuccessConditionExp()); if (isFinished) { log.debug("Rollout group {} is finished, starting next group", rolloutGroup); - executeRolloutGroupSuccessAction(rollout, rolloutGroup); + evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup); } else { log.debug("Rollout group {} is still running", rolloutGroup); } @@ -539,10 +525,6 @@ public class JpaRolloutExecutor implements RolloutExecutor { } } - private void executeRolloutGroupSuccessAction(final Rollout rollout, final RolloutGroup rolloutGroup) { - evaluationManager.getSuccessActionEvaluator(rolloutGroup.getSuccessAction()).exec(rollout, rolloutGroup); - } - private void startFirstRolloutGroup(final JpaRollout rollout) { log.debug("startFirstRolloutGroup called for rollout {}", rollout.getId()); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java index f08969708..8f4c7a928 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementFlowTest.java @@ -27,6 +27,7 @@ import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; +import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessAction; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; @@ -52,7 +53,21 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { */ @SneakyThrows @Test - void rolloutFlow() { + void rolloutDefaultFlow() { + rolloutFlow(RolloutGroupSuccessAction.NEXTGROUP); + } + + /** + * Verifies a simple rollout flow + */ + @SneakyThrows + @Test + void rolloutPauseFlow() { + rolloutFlow(RolloutGroupSuccessAction.PAUSE); + } + + + void rolloutFlow(final RolloutGroupSuccessAction successAction) throws Exception { final String rolloutName = "rollout-std"; final int amountGroups = 5; // static only final String targetPrefix = "controller-rollout-std-"; @@ -62,7 +77,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { final Rollout rollout = callAs( withUser("rolloutFlowUser", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"), () -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups, - "controllerid==" + targetPrefix + "*", distributionSet, "60", "30", false, false)); + "controllerid==" + targetPrefix + "*", distributionSet, "60", successAction,"30", false, false)); final List groups = rolloutGroupManagement.findByRollout( rollout.getId(), new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id"))).getContent(); @@ -78,7 +93,9 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { assertGroup(groups.get(i), false, i == 0 ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.SCHEDULED, 3); } - executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups); + executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups, successAction); + + assertRollout(rollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3); rolloutManagement.pauseRollout(rollout.getId()); rolloutHandler.handleAll(); @@ -92,11 +109,24 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { } /** - * Verifies a simple dynamic rollout flow + * Verifies a simple dynamic rollout flow with default {@link RolloutGroupSuccessAction#NEXTGROUP} success action */ @SneakyThrows @Test - void dynamicRolloutFlow() { + void dynamicRolloutDefaultFlow() { + dynamicRolloutFlow(RolloutGroupSuccessAction.NEXTGROUP); + } + + /** + * Verifies a simple dynamic rollout flow with {@link RolloutGroupSuccessAction#PAUSE} success action + */ + @SneakyThrows + @Test + void dynamicRolloutPauseFlow() { + dynamicRolloutFlow(RolloutGroupSuccessAction.PAUSE); + } + + void dynamicRolloutFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception { final String rolloutName = "dynamic-rollout-std"; final int amountGroups = 2; // static only final String targetPrefix = "controller-dynamic-rollout-std-"; @@ -106,7 +136,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { final Rollout rollout = callAs( withUser("dynamicRolloutFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"), () -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups, - "controllerid==" + targetPrefix + "*", distributionSet, "60", "30", false, true)); + "controllerid==" + targetPrefix + "*", distributionSet, "60", successAction,"30", false, true)); // rollout is READY assertRollout(rollout, true, RolloutStatus.READY, amountGroups + 1, amountGroups * 3); @@ -132,7 +162,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { } assertGroup(dynamic1, true, RolloutGroupStatus.SCHEDULED, 0); - executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups); + executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups, successAction); // partially fill the first dynamic (it is running and now create actions for 2 targets) rolloutHandler.handleAll(); @@ -166,7 +196,13 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { .forEach(this::finishAction); executeWithoutOneTargetFromAGroup(dynamic1, rollout, 3); assertAndGetRunning(rollout, 1); // remains on in the first dynamic - + if (successAction == RolloutGroupSuccessAction.PAUSE) { + // let success pause action run + rolloutHandler.handleAll(); + // external resume rollout + rolloutManagement.resumeRollout(rollout.getId()); + } + // start next group rolloutHandler.handleAll(); assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 4); assertGroup(groups.get(amountGroups - 1), false, RolloutGroupStatus.FINISHED, 3); @@ -208,12 +244,26 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { assertThat(refresh(dynamic2).getStatus()).isEqualTo(RolloutGroupStatus.FINISHED); } + /** - * Verifies a simple dynamic rollout flow with a dynamic group template + * Verifies a simple dynamic rollout flow with a dynamic group template with default {@link RolloutGroupSuccessAction#NEXTGROUP} success action */ @SneakyThrows @Test - void dynamicRolloutTemplateFlow() { + void dynamicDefaultRolloutTemplateFlow() { + dynamicRolloutTemplateFlow(RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP); + } + + /** + * Verifies a simple dynamic rollout flow with a dynamic group template with {@link RolloutGroupSuccessAction#PAUSE} success action + */ + @SneakyThrows + @Test + void dynamicPauseRolloutTemplateFlow() { + dynamicRolloutTemplateFlow(RolloutGroup.RolloutGroupSuccessAction.PAUSE); + } + + void dynamicRolloutTemplateFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception { final String rolloutName = "dynamic-template-rollout-std"; final int amountGroups = 3; // static only final String targetPrefix = "controller-template-dynamic-rollout-std-"; @@ -224,7 +274,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { final Rollout rollout = callAs( withUser("dynamicRolloutTemplateFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"), () -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups, - "controllerid==" + targetPrefix + "*", distributionSet, "60", "30", + "controllerid==" + targetPrefix + "*", distributionSet, "60", successAction, "30", Action.ActionType.FORCED, 1000, false, true, RolloutManagement.DynamicRolloutGroupTemplate.builder().nameSuffix("-dyn").targetCount(6).build())); @@ -252,7 +302,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { } assertGroup(dynamic1, true, RolloutGroupStatus.SCHEDULED, 0); - executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups); + executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll(groups, rollout, amountGroups, successAction); // partially fill the first dynamic (it is running and now create actions for 4 targets) rolloutHandler.handleAll(); @@ -287,6 +337,13 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { executeWithoutOneTargetFromAGroup(dynamic1, rollout, 6); assertAndGetRunning(rollout, 1); // remains on in the first dynamic + if (successAction == RolloutGroupSuccessAction.PAUSE) { + // let success pause action run + rolloutHandler.handleAll(); + // external resume rollout + rolloutManagement.resumeRollout(rollout.getId()); + } + // start next group rolloutHandler.handleAll(); assertRollout(rollout, true, RolloutStatus.RUNNING, amountGroups + 2, amountGroups * 3 + 8); assertGroup(groups.get(amountGroups - 1), false, RolloutGroupStatus.FINISHED, 3); @@ -310,11 +367,24 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { } /** - * Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template + * Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template with default {@link RolloutGroupSuccessAction#NEXTGROUP} success action */ @SneakyThrows @Test - void dynamicRolloutPureFlow() { + void dynamicDefaultRolloutPureFlow() { + dynamicRolloutPureFlow(RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP); + } + + /** + * Verifies a simple pure (no static groups) dynamic rollout flow with a dynamic group template with {@link RolloutGroupSuccessAction#PAUSE} success action + */ + @SneakyThrows + @Test + void dynamicPauseRolloutPureFlow() { + dynamicRolloutPureFlow(RolloutGroup.RolloutGroupSuccessAction.PAUSE); + } + + void dynamicRolloutPureFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception { final String rolloutName = "pure-dynamic-rollout-std"; final String targetPrefix = "controller-pure-dynamic-rollout-std-"; final DistributionSet distributionSet = testdataFactory.createDistributionSetLocked("dsFor" + rolloutName); @@ -322,7 +392,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { final Rollout rollout = callAs( withUser("dynamicRolloutPureFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"), () -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, 0, - "controllerid==" + targetPrefix + "*", distributionSet, "60", "30", + "controllerid==" + targetPrefix + "*", distributionSet, "60", successAction,"30", Action.ActionType.FORCED, 1000, false, true, RolloutManagement.DynamicRolloutGroupTemplate.builder().nameSuffix("-dyn").targetCount(6).build())); @@ -373,6 +443,13 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { executeWithoutOneTargetFromAGroup(dynamic1, rollout, 6); assertAndGetRunning(rollout, 1); // remains on in the first dynamic + if (successAction == RolloutGroupSuccessAction.PAUSE) { + // let success pause action run + rolloutHandler.handleAll(); + // external resume rollout + rolloutManagement.resumeRollout(rollout.getId()); + } + // start next group rolloutHandler.handleAll(); assertRollout(rollout, true, RolloutStatus.RUNNING, 2, 8); assertGroup(dynamic1, true, RolloutGroupStatus.RUNNING, 6); @@ -395,11 +472,24 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { } /** - * Verifies a simple rollout flow + * Verifies a simple rollout flow with {@link RolloutGroupSuccessAction#NEXTGROUP} success action */ @SneakyThrows @Test - void rollout0ThresholdFlow() { + void rolloutDefault0ThresholdFlow() { + rollout0ThresholdFlow(RolloutGroupSuccessAction.NEXTGROUP); + } + + /** + * Verifies a simple rollout flow with {@link RolloutGroupSuccessAction#PAUSE} success action + */ + @SneakyThrows + @Test + void rolloutPause0ThresholdFlow() { + rollout0ThresholdFlow(RolloutGroupSuccessAction.PAUSE); + } + + void rollout0ThresholdFlow(final RolloutGroup.RolloutGroupSuccessAction successAction) throws Exception { final String rolloutName = "rollout-std-0threshold"; final int amountGroups = 5; // static only final String targetPrefix = "controller-rollout-std-0threshold-"; @@ -409,7 +499,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { final Rollout rollout = callAs( withUser("rollout0ThresholdFlow", "READ_DISTRIBUTION_SET", "READ_TARGET", "READ_ROLLOUT", "CREATE_ROLLOUT"), () -> testdataFactory.createRolloutByVariables(rolloutName, rolloutName, amountGroups, - "controllerid==" + targetPrefix + "*", distributionSet, "0", "25", false, false)); + "controllerid==" + targetPrefix + "*", distributionSet, "0", successAction, "25", false, false)); final List groups = rolloutGroupManagement.findByRollout( rollout.getId(), new OffsetBasedPageRequest(0, amountGroups + 10, Sort.by(Direction.ASC, "id"))).getContent(); @@ -423,6 +513,12 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { for (int i = 0; i < amountGroups; i++) { assertGroup(groups.get(i), false, i < step ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.SCHEDULED, 3); } + + // expect all groups without last to trigger PAUSE action + if (step < amountGroups && successAction == RolloutGroupSuccessAction.PAUSE) { + rolloutHandler.handleAll(); + rolloutManagement.resumeRollout(rollout.getId()); + } // starting the next group rolloutHandler.handleAll(); } @@ -430,7 +526,7 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { private void executeStaticWithoutOneTargetFromTheLastGroupAndHandleAll( final List groups, - final Rollout rollout, final int amountGroups) { + final Rollout rollout, final int amountGroups, final RolloutGroupSuccessAction successAction) { // create dynamic group if needed rolloutHandler.handleAll(); // execute groups (without on of the last) @@ -455,6 +551,10 @@ class RolloutManagementFlowTest extends AbstractJpaIntegrationTest { .forEach(this::finishAction); assertAndGetRunning(rollout, i + 1 == amountGroups ? 1 : 0); rolloutHandler.handleAll(); + if (i + 1 < groups.size() && successAction == RolloutGroupSuccessAction.PAUSE) { + rolloutManagement.resumeRollout(rollout.getId()); + rolloutHandler.handleAll(); + } final RolloutGroupStatus expectedStatus = i + 1 == amountGroups ? RolloutGroupStatus.RUNNING : RolloutGroupStatus.FINISHED; assertThat(refresh(groups.get(i)).getStatus()) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java index 817220b30..4acf7b2d8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java @@ -117,7 +117,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { testdataFactory.createTargets(targetPrefix, 0, amountGroups * 2); final Rollout dynamicRollout = testdataFactory.createRolloutByVariables("dynamic", "static rollout", amountGroups, - "controllerid==" + targetPrefix + "*", distributionSet, "0", "30", ActionType.FORCED, 1000, false, true); + "controllerid==" + targetPrefix + "*", distributionSet, "0", RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP, "30", ActionType.FORCED, 1000, false, true); rolloutManagement.start(dynamicRollout.getId()); rolloutHandler.handleAll(); assertRollout(dynamicRollout, true, RolloutStatus.RUNNING, amountGroups + 1, amountGroups * 2); @@ -152,7 +152,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { testdataFactory.createTargets(targetPrefix, amountGroups * 2, amountGroups); final Rollout staticRollout = testdataFactory.createRolloutByVariables("static", "static rollout", amountGroups, - "controllerid==" + targetPrefix + "*", distributionSet, "0", "30", ActionType.FORCED, 0, false, false); + "controllerid==" + targetPrefix + "*", distributionSet, "0", RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP,"30", ActionType.FORCED, 0, false, false); rolloutManagement.start(staticRollout.getId()); rolloutHandler.handleAll(); assertRollout(staticRollout, false, RolloutStatus.RUNNING, amountGroups, amountGroups * 3); diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 89f3d89a7..a5347ae45 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -63,6 +63,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.NamedEntity; import org.eclipse.hawkbit.repository.model.NamedVersionedEntity; import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorAction; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorCondition; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; @@ -982,10 +983,10 @@ public class TestdataFactory { public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, final int groupSize, final String filterQuery, final DistributionSet distributionSet, - final String successCondition, final String errorCondition, final boolean confirmationRequired, + final String successCondition, final RolloutGroup.RolloutGroupSuccessAction successAction, final String errorCondition, final boolean confirmationRequired, final boolean dynamic) { return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, - successCondition, errorCondition, Action.ActionType.FORCED, null, confirmationRequired, dynamic); + successCondition, successAction, errorCondition, Action.ActionType.FORCED, null, confirmationRequired, dynamic); } public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, @@ -993,7 +994,7 @@ public class TestdataFactory { final String successCondition, final String errorCondition, final Action.ActionType actionType, final Integer weight, final boolean confirmationRequired) { return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, - successCondition, errorCondition, actionType, weight, confirmationRequired, false); + successCondition, RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP, errorCondition, actionType, weight, confirmationRequired, false); } /** @@ -1014,19 +1015,21 @@ public class TestdataFactory { */ public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, final int groupSize, final String filterQuery, final DistributionSet distributionSet, - final String successCondition, final String errorCondition, final Action.ActionType actionType, + final String successCondition, final RolloutGroup.RolloutGroupSuccessAction successAction, final String errorCondition, final Action.ActionType actionType, final Integer weight, final boolean confirmationRequired, final boolean dynamic) { return createRolloutByVariables(rolloutName, rolloutDescription, groupSize, filterQuery, distributionSet, - successCondition, errorCondition, actionType, weight, confirmationRequired, dynamic, null); + successCondition, successAction, errorCondition, actionType, weight, confirmationRequired, dynamic, null); } public Rollout createRolloutByVariables(final String rolloutName, final String rolloutDescription, final int groupSize, final String filterQuery, final DistributionSet distributionSet, - final String successCondition, final String errorCondition, final Action.ActionType actionType, + final String successCondition, final RolloutGroup.RolloutGroupSuccessAction successAction, final String errorCondition, + final Action.ActionType actionType, final Integer weight, final boolean confirmationRequired, final boolean dynamic, final RolloutManagement.DynamicRolloutGroupTemplate dynamicRolloutGroupTemplate) { final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) + .successAction(successAction, "") .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) .errorAction(RolloutGroupErrorAction.PAUSE, null).build();