Add support for native query for multiple JPA vendors (#2129)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaConstants;
|
||||
import org.eclipse.hawkbit.repository.jpa.Jpa;
|
||||
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.BaseEntity;
|
||||
@@ -205,7 +205,7 @@ public abstract class AbstractManagementApiIntegrationTest extends AbstractRestI
|
||||
|
||||
// version is 1, 2 ... based
|
||||
protected int version(final int version) {
|
||||
return switch (JpaConstants.JPA_VENDOR) {
|
||||
return switch (Jpa.JPA_VENDOR) {
|
||||
case ECLIPSELINK -> version;
|
||||
case HIBERNATE -> version - 1;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@ import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
|
||||
import org.eclipse.hawkbit.repository.builder.SoftwareModuleTypeCreate;
|
||||
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaConstants;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.NamedEntity;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
public class Jpa {
|
||||
|
||||
public enum JpaVendor {
|
||||
ECLIPSELINK,
|
||||
HIBERNATE // NOT SUPPORTED!
|
||||
}
|
||||
|
||||
public static final JpaVendor JPA_VENDOR = JpaVendor.ECLIPSELINK;
|
||||
|
||||
public static char NATIVE_QUERY_PARAMETER_PREFIX = switch (JPA_VENDOR) {
|
||||
case ECLIPSELINK -> '?';
|
||||
case HIBERNATE -> ':';
|
||||
};
|
||||
|
||||
public static <T> String formatNativeQueryInClause(final String name, final List<T> list) {
|
||||
return switch (Jpa.JPA_VENDOR) {
|
||||
case ECLIPSELINK -> formatEclipseLinkNativeQueryInClause(IntStream.range(0, list.size()).mapToObj(i -> name + "_" + i).toList());
|
||||
case HIBERNATE -> ":" + name;
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> void setNativeQueryInParameter(final Query deleteQuery, final String name, final List<T> list) {
|
||||
if (Jpa.JPA_VENDOR == Jpa.JpaVendor.ECLIPSELINK) {
|
||||
for (int i = 0, len = list.size(); i < len; i++) {
|
||||
deleteQuery.setParameter(name + "_" + i, list.get(i));
|
||||
}
|
||||
} else if (Jpa.JPA_VENDOR == Jpa.JpaVendor.HIBERNATE) {
|
||||
deleteQuery.setParameter(name, list);
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatEclipseLinkNativeQueryInClause(final Collection<String> elements) {
|
||||
return "?" + String.join(",?", elements);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public class JpaConstants {
|
||||
|
||||
public enum JpaVendor {
|
||||
ECLIPSELINK,
|
||||
HIBERNATE // NOT SUPPORTED!
|
||||
}
|
||||
|
||||
public static final JpaVendor JPA_VENDOR = JpaVendor.ECLIPSELINK;
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
@@ -53,6 +52,7 @@ import org.eclipse.hawkbit.repository.exception.IncompatibleTargetTypeException;
|
||||
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
|
||||
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
|
||||
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
|
||||
import org.eclipse.hawkbit.repository.jpa.Jpa;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
|
||||
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
|
||||
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
||||
@@ -123,9 +123,29 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
||||
*/
|
||||
private static final int ACTION_PAGE_LIMIT = 1000;
|
||||
private static final String QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED_DEFAULT =
|
||||
"DELETE FROM sp_action WHERE tenant=#tenant AND status IN (%s) AND last_modified_at<#last_modified_at LIMIT " + ACTION_PAGE_LIMIT;
|
||||
"DELETE FROM sp_action " +
|
||||
"WHERE tenant=" + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "tenant" +
|
||||
" AND status IN (%s)" +
|
||||
" AND last_modified_at<" + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "last_modified_at LIMIT " + ACTION_PAGE_LIMIT;
|
||||
private static final EnumMap<Database, String> QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED;
|
||||
|
||||
static {
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED = new EnumMap<>(Database.class);
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED.put(
|
||||
Database.SQL_SERVER,
|
||||
"DELETE TOP (" + ACTION_PAGE_LIMIT + ") FROM sp_action " +
|
||||
"WHERE tenant=" + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "tenant" +
|
||||
" AND status IN (%s)" +
|
||||
" AND last_modified_at<" + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "last_modified_at ");
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED.put(
|
||||
Database.POSTGRESQL,
|
||||
"DELETE FROM sp_action " +
|
||||
"WHERE id IN (SELECT id FROM sp_action " +
|
||||
"WHERE tenant=" + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "tenant" +
|
||||
" AND status IN (%s)" +
|
||||
" AND last_modified_at<" + Jpa.NATIVE_QUERY_PARAMETER_PREFIX + "last_modified_at LIMIT " + ACTION_PAGE_LIMIT + ")");
|
||||
}
|
||||
|
||||
private final EntityManager entityManager;
|
||||
private final DistributionSetManagement distributionSetManagement;
|
||||
private final TargetRepository targetRepository;
|
||||
@@ -141,18 +161,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
||||
private final Database database;
|
||||
private final RetryTemplate retryTemplate;
|
||||
|
||||
static {
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED = new EnumMap<>(Database.class);
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED.put(
|
||||
Database.SQL_SERVER,
|
||||
"DELETE TOP (" + ACTION_PAGE_LIMIT + ") FROM sp_action " +
|
||||
"WHERE tenant=#tenant AND status IN (%s) AND last_modified_at<#last_modified_at ");
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED.put(
|
||||
Database.POSTGRESQL,
|
||||
"DELETE FROM sp_action " +
|
||||
"WHERE id IN (SELECT id FROM sp_action WHERE tenant=#tenant AND status IN (%s) AND last_modified_at<#last_modified_at LIMIT " + ACTION_PAGE_LIMIT + ")");
|
||||
}
|
||||
|
||||
public JpaDeploymentManagement(
|
||||
final EntityManager entityManager, final ActionRepository actionRepository,
|
||||
final DistributionSetManagement distributionSetManagement, final TargetRepository targetRepository,
|
||||
@@ -500,23 +508,18 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
||||
if (status.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* We use a native query here because Spring JPA does not support to specify a
|
||||
* LIMIT clause on a DELETE statement. However, for this specific use case
|
||||
* (action cleanup), we must specify a row limit to reduce the overall load on
|
||||
* the database.
|
||||
*/
|
||||
|
||||
final int statusCount = status.size();
|
||||
final Status[] statusArr = status.toArray(new Status[statusCount]);
|
||||
// We use a native query here because Spring JPA does not support to specify a LIMIT clause on a DELETE statement.
|
||||
// However, for this specific use case (action cleanup), we must specify a row limit to reduce the overall load of
|
||||
// the database.
|
||||
final List<Integer> statusList = status.stream().map(Status::ordinal).toList();
|
||||
|
||||
final String queryStr = String.format(getQueryForDeleteActionsByStatusAndLastModifiedBeforeString(database),
|
||||
formatInClauseWithNumberKeys(statusCount));
|
||||
final Query deleteQuery = entityManager.createNativeQuery(queryStr);
|
||||
final Query deleteQuery = entityManager.createNativeQuery(String.format(
|
||||
getQueryForDeleteActionsByStatusAndLastModifiedBeforeString(database),
|
||||
Jpa.formatNativeQueryInClause("status", statusList)));
|
||||
|
||||
IntStream.range(0, statusCount)
|
||||
.forEach(i -> deleteQuery.setParameter(String.valueOf(i), statusArr[i].ordinal()));
|
||||
deleteQuery.setParameter("tenant", tenantAware.getCurrentTenant().toUpperCase());
|
||||
Jpa.setNativeQueryInParameter(deleteQuery, "status", statusList);
|
||||
deleteQuery.setParameter("last_modified_at", lastModified);
|
||||
|
||||
log.debug("Action cleanup: Executing the following (native) query: {}", deleteQuery);
|
||||
@@ -600,14 +603,6 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
||||
QUERY_DELETE_ACTIONS_BY_STATE_AND_LAST_MODIFIED_DEFAULT);
|
||||
}
|
||||
|
||||
private static String formatInClauseWithNumberKeys(final int count) {
|
||||
return formatInClause(IntStream.range(0, count).mapToObj(String::valueOf).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private static String formatInClause(final Collection<String> elements) {
|
||||
return "#" + String.join(",#", elements);
|
||||
}
|
||||
|
||||
private static RetryTemplate createRetryTemplate() {
|
||||
final RetryTemplate template = new RetryTemplate();
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ public class HawkbitBaseRepository<T, ID extends Serializable> extends SimpleJpa
|
||||
|
||||
private TypedQuery<T> withEntityGraph(final TypedQuery<T> query, final String entityGraph) {
|
||||
final EntityGraph<?> graph = ObjectUtils.isEmpty(entityGraph) ? null : entityManager.createEntityGraph(entityGraph);
|
||||
return graph == null ? query : query.setHint("javax.persistence.loadgraph", graph);
|
||||
return graph == null ? query : query.setHint("jakarta.persistence.loadgraph", graph);
|
||||
}
|
||||
|
||||
private <S extends T> Page<S> readPageWithoutCount(final TypedQuery<S> query, final Pageable pageable) {
|
||||
|
||||
@@ -246,7 +246,7 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
|
||||
|
||||
// version is 1, 2 ... based
|
||||
protected int version(final int version) {
|
||||
return switch (JpaConstants.JPA_VENDOR) {
|
||||
return switch (Jpa.JPA_VENDOR) {
|
||||
case ECLIPSELINK -> version;
|
||||
case HIBERNATE -> version - 1;
|
||||
};
|
||||
|
||||
@@ -33,15 +33,14 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
*/
|
||||
@Feature("Component Tests - Repository")
|
||||
@Story("Action cleanup handler")
|
||||
public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private AutoActionCleanup autoActionCleanup;
|
||||
|
||||
@Test
|
||||
@Description("Verifies that running actions are not cleaned up.")
|
||||
public void runningActionsAreNotCleanedUp() {
|
||||
|
||||
void runningActionsAreNotCleanedUp() {
|
||||
// cleanup config for this test case
|
||||
setupCleanupConfiguration(true, 0, Action.Status.CANCELED, Action.Status.ERROR);
|
||||
|
||||
@@ -60,13 +59,11 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
autoActionCleanup.run();
|
||||
|
||||
assertThat(actionRepository.count()).isEqualTo(2);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that nothing is cleaned up if the cleanup is disabled.")
|
||||
public void cleanupDisabled() {
|
||||
|
||||
void cleanupDisabled() {
|
||||
// cleanup config for this test case
|
||||
setupCleanupConfiguration(false, 0, Action.Status.CANCELED);
|
||||
|
||||
@@ -87,13 +84,11 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
autoActionCleanup.run();
|
||||
|
||||
assertThat(actionRepository.count()).isEqualTo(2);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that canceled and failed actions are cleaned up.")
|
||||
public void canceledAndFailedActionsAreCleanedUp() {
|
||||
|
||||
void canceledAndFailedActionsAreCleanedUp() {
|
||||
// cleanup config for this test case
|
||||
setupCleanupConfiguration(true, 0, Action.Status.CANCELED, Action.Status.ERROR);
|
||||
|
||||
@@ -120,13 +115,11 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
assertThat(actionRepository.count()).isEqualTo(1);
|
||||
assertThat(actionRepository.findWithDetailsById(action3)).isPresent();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that canceled actions are cleaned up.")
|
||||
public void canceledActionsAreCleanedUp() {
|
||||
|
||||
void canceledActionsAreCleanedUp() {
|
||||
// cleanup config for this test case
|
||||
setupCleanupConfiguration(true, 0, Action.Status.CANCELED);
|
||||
|
||||
@@ -154,14 +147,12 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(actionRepository.count()).isEqualTo(2);
|
||||
assertThat(actionRepository.findWithDetailsById(action2)).isPresent();
|
||||
assertThat(actionRepository.findWithDetailsById(action3)).isPresent();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that canceled and failed actions are cleaned up once they expired.")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
public void canceledAndFailedActionsAreCleanedUpWhenExpired() throws InterruptedException {
|
||||
|
||||
void canceledAndFailedActionsAreCleanedUpWhenExpired() throws InterruptedException {
|
||||
// cleanup config for this test case
|
||||
setupCleanupConfiguration(true, 500, Action.Status.CANCELED, Action.Status.ERROR);
|
||||
|
||||
@@ -194,7 +185,6 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
assertThat(actionRepository.count()).isEqualTo(1);
|
||||
assertThat(actionRepository.findWithDetailsById(action3)).isPresent();
|
||||
|
||||
}
|
||||
|
||||
private void setActionToCanceled(final Long id) {
|
||||
@@ -209,7 +199,8 @@ public class AutoActionCleanupTest extends AbstractJpaIntegrationTest {
|
||||
private void setupCleanupConfiguration(final boolean cleanupEnabled, final long expiry, final Status... status) {
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(ACTION_CLEANUP_ENABLED, cleanupEnabled);
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(ACTION_CLEANUP_ACTION_EXPIRY, expiry);
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(ACTION_CLEANUP_ACTION_STATUS,
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(
|
||||
ACTION_CLEANUP_ACTION_STATUS,
|
||||
Arrays.stream(status).map(Status::toString).collect(Collectors.joining(",")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,8 +160,7 @@ class RolloutGroupManagementTest extends AbstractJpaIntegrationTest {
|
||||
@Description("Verifies that Rollouts in different states are handled correctly.")
|
||||
void findAllTargetsOfRolloutGroupWithActionStatus() {
|
||||
final Rollout rollout = testdataFactory.createRollout();
|
||||
final List<RolloutGroup> rolloutGroups = rolloutGroupManagement.findByRollout(PAGE, rollout.getId())
|
||||
.getContent();
|
||||
final List<RolloutGroup> rolloutGroups = rolloutGroupManagement.findByRollout(PAGE, rollout.getId()).getContent();
|
||||
rolloutHandler.handleAll();
|
||||
|
||||
// check query when no actions exist
|
||||
@@ -170,8 +169,7 @@ class RolloutGroupManagementTest extends AbstractJpaIntegrationTest {
|
||||
PageRequest.of(0, 500, Sort.by(Direction.DESC, "lastActionStatusCode")),
|
||||
rolloutGroups.get(0).getId())
|
||||
.getContent();
|
||||
assertThat(targetsWithActionStatus)
|
||||
.hasSize((int) rolloutGroupManagement.countTargetsOfRolloutsGroup(rolloutGroups.get(0).getId()));
|
||||
assertThat(targetsWithActionStatus).hasSize((int) rolloutGroupManagement.countTargetsOfRolloutsGroup(rolloutGroups.get(0).getId()));
|
||||
assertTargetNotNullAndActionStatusNullAndActionStatusCode(targetsWithActionStatus, null);
|
||||
|
||||
rolloutManagement.start(rollout.getId());
|
||||
|
||||
@@ -1805,19 +1805,19 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
@Test
|
||||
@ExpectEvents({
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = RolloutGroupUpdatedEvent.class, count = 10),
|
||||
@Expect(type = RolloutUpdatedEvent.class, count = 6),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = TargetCreatedEvent.class, count = 25),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = RolloutGroupCreatedEvent.class, count = 5),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 10),
|
||||
@Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = RolloutCreatedEvent.class, count = 1),
|
||||
@Expect(type = RolloutUpdatedEvent.class, count = 6),
|
||||
@Expect(type = RolloutDeletedEvent.class, count = 1),
|
||||
@Expect(type = RolloutCreatedEvent.class, count = 1) })
|
||||
@Expect(type = RolloutGroupUpdatedEvent.class, count = 10),
|
||||
@Expect(type = RolloutGroupCreatedEvent.class, count = 5) })
|
||||
void deleteRolloutWhichHasBeenStartedBeforeIsSoftDeleted() {
|
||||
final int amountTargetsForRollout = 10;
|
||||
final int amountOtherTargets = 15;
|
||||
@@ -1825,17 +1825,15 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
final String successCondition = "50";
|
||||
final String errorCondition = "80";
|
||||
final Rollout createdRollout = testdataFactory.createSimpleTestRolloutWithTargetsAndDistributionSet(
|
||||
amountTargetsForRollout,
|
||||
amountOtherTargets, amountGroups, successCondition, errorCondition);
|
||||
amountTargetsForRollout, amountOtherTargets, amountGroups, successCondition, errorCondition);
|
||||
|
||||
// start the rollout, so it has active running actions and a group which
|
||||
// has been started
|
||||
// start the rollout, so it has active running actions and a group which has been started
|
||||
rolloutManagement.start(createdRollout.getId());
|
||||
rolloutHandler.handleAll();
|
||||
|
||||
// verify we have running actions
|
||||
assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, createdRollout.getId(), Status.RUNNING)
|
||||
.getNumberOfElements()).isEqualTo(2);
|
||||
assertThat(actionRepository.findByRolloutIdAndStatus(PAGE, createdRollout.getId(), Status.RUNNING).getNumberOfElements())
|
||||
.isEqualTo(2);
|
||||
|
||||
// test
|
||||
rolloutManagement.delete(createdRollout.getId());
|
||||
|
||||
Reference in New Issue
Block a user