Fix dynamic rollout override behavior (#2447)

Since static assigments of dynamic rollouts always override the oldest rollouts
it seems more consistent if this is the case also with dynamic assigments. I.e.
all older are overriden and if there are actions of newer rollouts - won't be assigned.

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-06-11 15:24:48 +03:00
committed by GitHub
parent 58b969db38
commit 4cfd90b745
6 changed files with 92 additions and 118 deletions

View File

@@ -245,39 +245,17 @@ public interface TargetManagement {
* with the passed {@link DistributionSetType}.
*
* @param groups the list of {@link RolloutGroup}s
* @param targetFilterQuery filter definition in RSQL syntax
* @param rsql filter definition in RSQL syntax
* @param distributionSetType type of the {@link DistributionSet} the targets must be compatible
* withs
* @param pageable the pageable to enhance the query for paging and sorting
* @return a page of the found {@link Target}s
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
Slice<Target> findByTargetFilterQueryAndNotInRolloutAndCompatibleAndUpdatable(
@NotEmpty Collection<Long> groups, @NotNull String targetFilterQuery, @NotNull DistributionSetType distributionSetType,
Slice<Target> findByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatable(
@NotEmpty Collection<Long> groups, @NotNull String rsql, @NotNull DistributionSetType distributionSetType,
@NotNull Pageable pageable);
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
Slice<Target> findByTargetFilterQueryAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
final long rolloutId, final int weight, final long firstGroupId, @NotNull String targetFilterQuery,
@NotNull DistributionSetType distributionSetType, @NotNull Pageable pageable);
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
long countByActionsInRolloutGroup(final long rolloutGroupId);
/**
* Finds all targets with failed actions for specific Rollout and that are not
* assigned to one of the retried {@link RolloutGroup}s and are compatible with
* the passed {@link DistributionSetType}.
*
* @param rolloutId rolloutId of the rollout to be retried.
* @param groups the list of {@link RolloutGroup}s
* @param pageable the pageable to enhance the query for paging and sorting
* @return a page of the found {@link Target}s
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
Slice<Target> findByFailedRolloutAndNotInRolloutGroups(
@NotNull String rolloutId, @NotEmpty Collection<Long> groups, @NotNull Pageable pageable);
/**
* Counts all targets for all the given parameter {@link TargetFilterQuery} and
* that are not assigned to one of the {@link RolloutGroup}s and are compatible
@@ -292,6 +270,20 @@ public interface TargetManagement {
long countByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatable(
@NotNull String rsql, @NotEmpty Collection<Long> groups, @NotNull DistributionSetType distributionSetType);
/**
* Finds all targets with failed actions for specific Rollout and that are not
* assigned to one of the retried {@link RolloutGroup}s and are compatible with
* the passed {@link DistributionSetType}.
*
* @param rolloutId rolloutId of the rollout to be retried.
* @param groups the list of {@link RolloutGroup}s
* @param pageable the pageable to enhance the query for paging and sorting
* @return a page of the found {@link Target}s
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
Slice<Target> findByFailedRolloutAndNotInRolloutGroups(
@NotNull String rolloutId, @NotEmpty Collection<Long> groups, @NotNull Pageable pageable);
/**
* Counts all targets with failed actions for specific Rollout and that are not
* assigned to one of the {@link RolloutGroup}s and are compatible with the
@@ -304,6 +296,13 @@ public interface TargetManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
long countByFailedRolloutAndNotInRolloutGroups(@NotNull String rolloutId, @NotEmpty Collection<Long> groups);
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
Slice<Target> findByRsqlAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
final long rolloutId, @NotNull String rsql, @NotNull DistributionSetType distributionSetType, @NotNull Pageable pageable);
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ)
long countByActionsInRolloutGroup(final long rolloutGroupId);
/**
* Finds all targets of the provided {@link RolloutGroup} that have no Action
* for the RolloutGroup.

View File

@@ -91,13 +91,12 @@ public class JpaRolloutExecutor implements RolloutExecutor {
/**
* Action statuses that result in a terminated action
*/
private static final List<Status> DEFAULT_ACTION_TERMINATION_STATUSES = List.of(
Status.ERROR, Status.FINISHED, Status.CANCELED);
private static final List<Status> DEFAULT_ACTION_TERMINATION_STATUSES = List.of(Status.ERROR, Status.FINISHED, Status.CANCELED);
/**
* In case of DOWNLOAD_ONLY, actions can be finished with DOWNLOADED status.
*/
private static final List<Status> DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES = List.of(
Status.ERROR, Status.FINISHED, Status.CANCELED, Status.DOWNLOADED);
private static final List<Status> DOWNLOAD_ONLY_ACTION_TERMINATION_STATUSES =
List.of(Status.ERROR, Status.FINISHED, Status.CANCELED, Status.DOWNLOADED);
private static final Comparator<RolloutGroup> DESC_COMP = Comparator.comparingLong(RolloutGroup::getId).reversed();
private static final String TRANSACTION_ASSIGNING_TARGETS_TO_ROLLOUT_GROUP_FAILED = "Transaction assigning Targets to RolloutGroup failed";
@@ -647,7 +646,7 @@ public class JpaRolloutExecutor implements RolloutExecutor {
rollout.getRolloutGroups(), RolloutGroupStatus.READY, group);
final Slice<Target> targets;
if (!RolloutHelper.isRolloutRetried(rollout.getTargetFilterQuery())) {
targets = targetManagement.findByTargetFilterQueryAndNotInRolloutAndCompatibleAndUpdatable(
targets = targetManagement.findByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatable(
readyGroups, targetFilter, rollout.getDistributionSet().getType(), pageRequest);
} else {
targets = targetManagement.findByFailedRolloutAndNotInRolloutGroups(
@@ -688,8 +687,7 @@ public class JpaRolloutExecutor implements RolloutExecutor {
long targetsLeftToAdd = expectedInGroup - currentlyInGroup;
final String groupTargetFilter = RolloutHelper.getGroupTargetFilter(
// don't use RolloutHelper.getTargetFilterQuery(rollout)
// since it contains condition for device to be created
// before the rollout
// since it contains condition for device to be created before the rollout
rollout.getTargetFilterQuery(), group);
long newActions = 0;
do {
@@ -723,8 +721,8 @@ public class JpaRolloutExecutor implements RolloutExecutor {
return false;
}
private void createDynamicGroup(final JpaRollout rollout, final JpaRolloutGroup lastGroup, final int groupCount,
final RolloutGroupStatus status) {
private void createDynamicGroup(
final JpaRollout rollout, final JpaRolloutGroup lastGroup, final int groupCount, final RolloutGroupStatus status) {
try {
RolloutHelper.verifyRolloutGroupAmount(groupCount + 1, quotaManagement);
} catch (final AssignmentQuotaExceededException e) {
@@ -738,8 +736,9 @@ public class JpaRolloutExecutor implements RolloutExecutor {
final JpaRolloutGroup group = new JpaRolloutGroup();
final String lastGroupWithoutSuffix = "group-" + groupCount;
final String suffix = lastGroup.getName().startsWith(lastGroupWithoutSuffix) ? lastGroup.getName()
.substring(lastGroupWithoutSuffix.length()) : "";
final String suffix = lastGroup.getName().startsWith(lastGroupWithoutSuffix)
? lastGroup.getName().substring(lastGroupWithoutSuffix.length())
: "";
final String nameAndDesc = "group-" + (groupCount + 1) + suffix;
group.setName(nameAndDesc);
group.setDescription(nameAndDesc);
@@ -767,15 +766,12 @@ public class JpaRolloutExecutor implements RolloutExecutor {
((JpaRolloutManagement) rolloutManagement).publishRolloutGroupCreatedEventAfterCommit(savedGroup, rollout);
}
private int createActionsForDynamicGroupInNewTransaction(final JpaRollout rollout, final RolloutGroup group,
final String targetFilter, final long limit) {
private int createActionsForDynamicGroupInNewTransaction(
final JpaRollout rollout, final RolloutGroup group, final String targetFilter, final long limit) {
return DeploymentHelper.runInNewTransaction(txManager, "createActionsForRolloutDynamicGroup", status -> {
final PageRequest pageRequest = PageRequest.of(0, Math.toIntExact(limit));
final Slice<Target> targets = targetManagement.findByTargetFilterQueryAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
rollout.getId(), rollout.getWeight().orElse(1000), rolloutGroupRepository.findByRolloutOrderByIdAsc(rollout).get(0).getId(),
targetFilter, rollout.getDistributionSet().getType(), pageRequest
// Dynamic rollouts shall always have weight!
);
// Dynamic rollouts shall always have weight!
final Slice<Target> targets = targetManagement.findByRsqlAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
rollout.getId(), targetFilter, rollout.getDistributionSet().getType(), PageRequest.of(0, Math.toIntExact(limit)));
if (targets.getNumberOfElements() == 0) {
return 0;

View File

@@ -277,46 +277,18 @@ public class JpaTargetManagement implements TargetManagement {
}
@Override
public Slice<Target> findByTargetFilterQueryAndNotInRolloutAndCompatibleAndUpdatable(
final Collection<Long> groups, final String targetFilterQuery, final DistributionSetType dsType, final Pageable pageable) {
public Slice<Target> findByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatable(
final Collection<Long> groups, final String rsql, final DistributionSetType dsType, final Pageable pageable) {
return targetRepository
.findAllWithoutCount(AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database),
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.isNotInRolloutGroups(groups),
TargetSpecifications.isCompatibleWithDistributionSetType(dsType.getId()))),
pageable)
.map(Target.class::cast);
}
@Override
public Slice<Target> findByTargetFilterQueryAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
final long rolloutId, final int weight, final long firstGroupId, final String targetFilterQuery,
final DistributionSetType distributionSetType, final Pageable pageable) {
return targetRepository
.findAllWithoutCount(AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNoOverridingActionsAndNotInRollout(weight, rolloutId),
TargetSpecifications.isCompatibleWithDistributionSetType(distributionSetType.getId()))),
pageable)
.map(Target.class::cast);
}
@Override
public long countByActionsInRolloutGroup(final long rolloutGroupId) {
return targetRepository.count(TargetSpecifications.isInActionRolloutGroup(rolloutGroupId));
}
@Override
public Slice<Target> findByFailedRolloutAndNotInRolloutGroups(String rolloutId, Collection<Long> groups, Pageable pageable) {
final List<Specification<JpaTarget>> specList = List.of(
TargetSpecifications.failedActionsForRollout(rolloutId),
TargetSpecifications.isNotInRolloutGroups(groups));
return JpaManagementHelper.findAllWithCountBySpec(targetRepository, specList, pageable);
}
@Override
public long countByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatable(
final String rsql, final Collection<Long> groups, final DistributionSetType dsType) {
@@ -327,6 +299,14 @@ public class JpaTargetManagement implements TargetManagement {
TargetSpecifications.isCompatibleWithDistributionSetType(dsType.getId()))));
}
@Override
public Slice<Target> findByFailedRolloutAndNotInRolloutGroups(String rolloutId, Collection<Long> groups, Pageable pageable) {
final List<Specification<JpaTarget>> specList = List.of(
TargetSpecifications.failedActionsForRollout(rolloutId),
TargetSpecifications.isNotInRolloutGroups(groups));
return JpaManagementHelper.findAllWithCountBySpec(targetRepository, specList, pageable);
}
@Override
public long countByFailedRolloutAndNotInRolloutGroups(String rolloutId, Collection<Long> groups) {
final List<Specification<JpaTarget>> specList = List.of(
@@ -335,6 +315,24 @@ public class JpaTargetManagement implements TargetManagement {
return JpaManagementHelper.countBySpec(targetRepository, specList);
}
@Override
public Slice<Target> findByRsqlAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
final long rolloutId, final String rsql, final DistributionSetType distributionSetType, final Pageable pageable) {
return targetRepository
.findAllWithoutCount(AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNoOverridingActionsAndNotInRollout(rolloutId),
TargetSpecifications.isCompatibleWithDistributionSetType(distributionSetType.getId()))),
pageable)
.map(Target.class::cast);
}
@Override
public long countByActionsInRolloutGroup(final long rolloutGroupId) {
return targetRepository.count(TargetSpecifications.isInActionRolloutGroup(rolloutGroupId));
}
@Override
public Slice<Target> findByInRolloutGroupWithoutAction(final long group, final Pageable pageable) {
if (!rolloutGroupRepository.existsById(group)) {

View File

@@ -173,8 +173,8 @@ public final class TargetSpecifications {
* @return the {@link Target} {@link Specification}
*/
public static Specification<JpaTarget> isOverdue(final long overdueTimestamp) {
return (targetRoot, query, cb) -> cb.lessThanOrEqualTo(targetRoot.get(JpaTarget_.lastTargetQuery),
overdueTimestamp);
return (targetRoot, query, cb) ->
cb.lessThanOrEqualTo(targetRoot.get(JpaTarget_.lastTargetQuery), overdueTimestamp);
}
/**
@@ -186,7 +186,8 @@ public final class TargetSpecifications {
public static Specification<JpaTarget> likeControllerIdOrName(final String searchText) {
return (targetRoot, query, cb) -> {
final String searchTextToLower = searchText.toLowerCase();
return cb.or(cb.like(cb.lower(targetRoot.get(JpaTarget_.controllerId)), searchTextToLower),
return cb.or(
cb.like(cb.lower(targetRoot.get(JpaTarget_.controllerId)), searchTextToLower),
cb.like(cb.lower(targetRoot.get(AbstractJpaNamedEntity_.name)), searchTextToLower));
};
}
@@ -251,8 +252,8 @@ public final class TargetSpecifications {
* @return the {@link Target} {@link Specification}
*/
public static Specification<JpaTarget> hasAssignedDistributionSet(final Long distributionSetId) {
return (targetRoot, query, cb) -> cb.equal(
targetRoot.get(JpaTarget_.assignedDistributionSet).get(AbstractJpaBaseEntity_.id), distributionSetId);
return (targetRoot, query, cb) ->
cb.equal(targetRoot.get(JpaTarget_.assignedDistributionSet).get(AbstractJpaBaseEntity_.id), distributionSetId);
}
/**
@@ -358,10 +359,8 @@ public final class TargetSpecifications {
*/
public static Specification<JpaTarget> hasNoActionInRolloutGroup(final Long group) {
return (targetRoot, query, cb) -> {
final ListJoin<JpaTarget, RolloutTargetGroup> rolloutTargetJoin = targetRoot
.join(JpaTarget_.rolloutTargetGroup, JoinType.INNER);
rolloutTargetJoin.on(
cb.equal(rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup).get(AbstractJpaBaseEntity_.id), group));
final ListJoin<JpaTarget, RolloutTargetGroup> rolloutTargetJoin = targetRoot.join(JpaTarget_.rolloutTargetGroup, JoinType.INNER);
rolloutTargetJoin.on(cb.equal(rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup).get(AbstractJpaBaseEntity_.id), group));
final ListJoin<JpaTarget, JpaAction> actionsJoin = targetRoot.join(JpaTarget_.actions, JoinType.LEFT);
actionsJoin.on(cb.equal(actionsJoin.get(JpaAction_.rolloutGroup).get(AbstractJpaBaseEntity_.id), group));
@@ -388,7 +387,6 @@ public final class TargetSpecifications {
* @return the {@link Target} {@link Specification}
*/
public static Specification<JpaTarget> hasTag(final Long tagId) {
return (targetRoot, query, cb) -> {
final SetJoin<JpaTarget, JpaTargetTag> tags = targetRoot.join(JpaTarget_.tags, JoinType.LEFT);
return cb.equal(tags.get(AbstractJpaBaseEntity_.id), tagId);
@@ -435,31 +433,14 @@ public final class TargetSpecifications {
}
/**
* {@link Specification} for retrieving {@link Target}s that have:
* <ul>
* <li>no active (non-finished) actions with greater weight in the older rollouts</li>
* <li>or have actions in the current rollout</li>
* <li>or have actions with great or equal weight in the newer rollouts</li>
* </ul>
* {@link Specification} for retrieving {@link Target}s that have no overriding actions - i.e. no actions from newer rollouts
*
* @param weight the referent weight
* @return the {@link Target} {@link Specification}
*/
public static Specification<JpaTarget> hasNoOverridingActionsAndNotInRollout(final int weight, final long rolloutId) {
public static Specification<JpaTarget> hasNoOverridingActionsAndNotInRollout(final long rolloutId) {
return (targetRoot, query, cb) -> {
final ListJoin<JpaTarget, JpaAction> actionsJoin = targetRoot.join(JpaTarget_.actions, JoinType.LEFT);
actionsJoin.on(
cb.or(
cb.and(
cb.and(
cb.lt(actionsJoin.get(JpaAction_.rollout).get(AbstractJpaBaseEntity_.id), rolloutId),
cb.gt(actionsJoin.get(JpaAction_.weight), weight)),
cb.or(
cb.equal(actionsJoin.get(JpaAction_.active), true),
cb.equal(actionsJoin.get(JpaAction_.status), Action.Status.SCHEDULED))),
cb.and(
cb.ge(actionsJoin.get(JpaAction_.rollout).get(AbstractJpaBaseEntity_.id), rolloutId),
cb.ge(actionsJoin.get(JpaAction_.weight), weight))));
actionsJoin.on(cb.ge(actionsJoin.get(JpaAction_.rollout).get(AbstractJpaBaseEntity_.id), rolloutId));
return cb.isNull(actionsJoin.get(AbstractJpaBaseEntity_.id));
};
}
@@ -497,4 +478,4 @@ public final class TargetSpecifications {
final Join<JpaTarget, JpaTargetType> targetTypeJoin = root.join(JpaTarget_.targetType, JoinType.LEFT);
return targetTypeJoin.join(JpaTargetType_.distributionSetTypes, JoinType.LEFT).get(AbstractJpaBaseEntity_.id);
}
}
}

View File

@@ -147,9 +147,9 @@ class TargetManagementSecurityTest extends AbstractJpaIntegrationTest {
@Test
@Description("Tests ManagementAPI PreAuthorized method with correct and insufficient permissions.")
void findByTargetFilterQueryAndNotInRolloutAndCompatibleAndUpdatablePermissionsCheck() {
void findByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatablePermissionsCheck() {
assertPermissions(
() -> targetManagement.findByTargetFilterQueryAndNotInRolloutAndCompatibleAndUpdatable(List.of(1L), "controllerId==id",
() -> targetManagement.findByRsqlAndNotInRolloutGroupsAndCompatibleAndUpdatable(List.of(1L), "controllerId==id",
entityFactory.distributionSetType().create().build(), PAGE
), List.of(SpPermission.READ_TARGET, SpPermission.READ_ROLLOUT));
}

View File

@@ -825,7 +825,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
assertThat(changedLockRevisionTarget2.getLastModifiedAt()).isPositive();
// verify updated meta data contains the updated value
assertThat(targetManagement.getMetadata(target.getControllerId()).get(knownKey)).isEqualTo(knownUpdateValue);
assertThat(targetManagement.getMetadata(target.getControllerId())).containsEntry(knownKey, knownUpdateValue);
}
@Test
@@ -1137,20 +1137,20 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
// old ro with equal weight - match
expected.add(target);
createAction(targets.get(target++), rolloutOlder, 10, Status.RUNNING, distributionSet);
// old ro with bigger weight, scheduled - doesn't match
// old ro with bigger weight, scheduled - match
expected.add(target);
createAction(targets.get(target++), rolloutOlder, 11, Status.SCHEDULED, distributionSet);
// old ro with bigger weight, running - doesn't match
// old ro with bigger weight, running - match
expected.add(target);
createAction(targets.get(target++), rolloutOlder, 11, Status.RUNNING, distributionSet);
// old ro with bigger weight, running match
// old ro with bigger weight, running - match
expected.add(target);
createAction(targets.get(target++), rolloutOlder, 11, Status.FINISHED, distributionSet);
// same ro - doesn't match
createAction(targets.get(target++), rollout, 10, Status.RUNNING, distributionSet);
// new ro with less weight - match
expected.add(target);
// new ro with less weight - doesn't match
createAction(targets.get(target++), rolloutNewer, 0, Status.RUNNING, distributionSet);
// new ro with less weight - match
expected.add(target);
// new ro with less weight - doesn't match
createAction(targets.get(target++), rolloutNewer, 5, Status.WARNING, distributionSet);
// NEW ro with EQUAL weight - doesn't match
createAction(targets.get(target++), rolloutNewer, 10, Status.RUNNING, distributionSet);
@@ -1158,8 +1158,8 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
createAction(targets.get(target), rolloutNewer, 20, Status.DOWNLOADED, distributionSet);
final Slice<Target> matching =
targetManagement.findByTargetFilterQueryAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
rollout.getId(), 10, Long.MAX_VALUE, "controllerid==dyn_action_filter_*", distributionSet.getType(), PAGE);
targetManagement.findByRsqlAndNoOverridingActionsAndNotInRolloutAndCompatibleAndUpdatable(
rollout.getId(), "controllerid==dyn_action_filter_*", distributionSet.getType(), PAGE);
assertThat(matching.getNumberOfElements()).isEqualTo(expected.size());
assertThat(matching.stream()