Fix for endless loop when an exception of type EntityAlreadyExistsException is thrown in the context of the retryable findOrRegisterTargetIfItDoesNotExist method (#828)

* fixed typo in method name

Signed-off-by: Ahmed Sayed <ahmed.sayed@bosch-si.com>

* Added recover method to handle EntityAlreadyExistsException

Signed-off-by: Ahmed Sayed <ahmed.sayed@bosch-si.com>

* Added tests

Signed-off-by: Ahmed Sayed <ahmed.sayed@bosch-si.com>

* Apply suggestions from code review

Use thenThrow(Exception.class) instead of (new Exception())

Co-Authored-By: a-sayyed <ahmed.sayed@bosch-si.com>

* Apply suggestions from code review

use final modifier

Co-Authored-By: a-sayyed <ahmed.sayed@bosch-si.com>

* Adapted review findings

Signed-off-by: Ahmed Sayed <ahmed.sayed@bosch-si.com>

* Adapted review findings

Signed-off-by: Ahmed Sayed <ahmed.sayed@bosch-si.com>

* added logs for EntityAlreadyExistsException case

Signed-off-by: Ahmed Sayed <ahmed.sayed@bosch-si.com>
This commit is contained in:
Ahmed Sayed
2019-04-30 12:47:06 +02:00
committed by Dominic Schabel
parent ed95ae6398
commit 9884452ad4
10 changed files with 137 additions and 26 deletions

View File

@@ -11,9 +11,14 @@ package org.eclipse.hawkbit.repository.jpa;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS;
import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX;
import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY;
import static org.eclipse.hawkbit.repository.test.util.TestdataFactory.DEFAULT_CONTROLLER_ID;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.net.URISyntaxException;
@@ -43,6 +48,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedE
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
@@ -61,7 +67,9 @@ import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -497,19 +505,111 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Description("Register a controller which does not exist")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetPollEvent.class, count = 2) })
public void findOrRegisterTargetIfItDoesNotexist() {
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotexist("AA", LOCALHOST);
public void findOrRegisterTargetIfItDoesNotExist() {
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
assertThat(target).as("target should not be null").isNotNull();
final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotexist("AA", LOCALHOST);
final Target sameTarget = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
assertThat(target.getId()).as("Target should be the equals").isEqualTo(sameTarget.getId());
assertThat(targetRepository.count()).as("Only 1 target should be registred").isEqualTo(1L);
assertThatExceptionOfType(ConstraintViolationException.class)
.isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotexist("", LOCALHOST))
.isThrownBy(() -> controllerManagement.findOrRegisterTargetIfItDoesNotExist("", LOCALHOST))
.as("register target with empty controllerId should fail");
}
@Test
@Description("Register a controller which does not exist, when a ConcurrencyFailureException is raised, the " +
"exception is rethrown after max retries")
public void findOrRegisterTargetIfItDoesNotExistThrowsExceptionAfterMaxRetries() {
final TargetRepository mockTargetRepository = Mockito.mock(TargetRepository.class);
when(mockTargetRepository.findOne(any())).thenThrow(ConcurrencyFailureException.class);
((JpaControllerManagement) controllerManagement).setTargetRepository(mockTargetRepository);
try {
controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
fail("Expected an ConcurrencyFailureException to be thrown!");
} catch (final ConcurrencyFailureException e) {
verify(mockTargetRepository, times(TX_RT_MAX)).findOne(any());
} finally {
// revert
((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository);
}
}
@Test
@Description("Register a controller which does not exist, when a ConcurrencyFailureException is raised, the " +
"exception is not rethrown when the max retries are not yet reached")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetPollEvent.class, count = 1) })
public void findOrRegisterTargetIfItDoesNotExistDoesNotThrowExceptionBeforeMaxRetries() {
TargetRepository mockTargetRepository = Mockito.mock(TargetRepository.class);
((JpaControllerManagement) controllerManagement).setTargetRepository(mockTargetRepository);
final Target target = testdataFactory.createTarget();
when(mockTargetRepository.findOne(any()))
.thenThrow(ConcurrencyFailureException.class)
.thenThrow(ConcurrencyFailureException.class)
.thenReturn(Optional.of((JpaTarget) target));
when(mockTargetRepository.save(any())).thenReturn(target);
try {
final Target targetFromControllerManagement = controllerManagement
.findOrRegisterTargetIfItDoesNotExist(target.getControllerId(), LOCALHOST);
verify(mockTargetRepository, times(3)).findOne(any());
verify(mockTargetRepository, times(1)).save(any());
assertThat(target).isEqualTo(targetFromControllerManagement);
} finally {
// revert
((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository);
}
}
@Test
@Description("Register a controller which does not exist, if a EntityAlreadyExistsException is raised, the " +
"exception is rethrown and no further retries will be attempted")
public void findOrRegisterTargetIfItDoesNotExistDoesntRetryWhenEntityAlreadyExistsException() {
TargetRepository mockTargetRepository = Mockito.mock(TargetRepository.class);
((JpaControllerManagement) controllerManagement).setTargetRepository(mockTargetRepository);
when(mockTargetRepository.findOne(any())).thenReturn(Optional.empty());
when(mockTargetRepository.save(any())).thenThrow(EntityAlreadyExistsException.class);
try {
controllerManagement.findOrRegisterTargetIfItDoesNotExist("1234", LOCALHOST);
fail("Expected an EntityAlreadyExistsException to be thrown!");
} catch (EntityAlreadyExistsException e) {
verify(mockTargetRepository, times(1)).findOne(any());
verify(mockTargetRepository, times(1)).save(any());
} finally {
// revert
((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository);
}
}
@Test
@Description("Retry is aborted when an unchecked exception is thrown and the exception should also be " +
"rethrown")
public void recoverFindOrRegisterTargetIfItDoesNotExistIsNotInvokedForOtherExceptions() {
TargetRepository mockTargetRepository = Mockito.mock(TargetRepository.class);
((JpaControllerManagement) controllerManagement).setTargetRepository(mockTargetRepository);
when(mockTargetRepository.findOne(any())).thenThrow(RuntimeException.class);
try {
controllerManagement.findOrRegisterTargetIfItDoesNotExist("aControllerId", LOCALHOST);
fail("Expected a RuntimeException to be thrown!");
} catch (RuntimeException e){
verify(mockTargetRepository, times(1)).findOne(any());
} finally {
// revert
((JpaControllerManagement) controllerManagement).setTargetRepository(targetRepository);
}
}
@Test
@Description("Verify that targetVisible metadata is returned from repository")
@ExpectEvents({ @Expect(type = DistributionSetCreatedEvent.class, count = 1),
@@ -533,7 +633,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Expect(type = TargetPollEvent.class, count = 0) })
public void targetPollEventNotSendIfDisabled() {
repositoryProperties.setPublishTargetPollEvent(false);
controllerManagement.findOrRegisterTargetIfItDoesNotexist("AA", LOCALHOST);
controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
repositoryProperties.setPublishTargetPollEvent(true);
}

View File

@@ -41,11 +41,11 @@ public class LazyControllerManagementTest extends AbstractJpaIntegrationTest {
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
@Expect(type = TargetPollEvent.class, count = 2) })
public void lazyFindOrRegisterTargetIfItDoesNotexist() throws InterruptedException {
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotexist("AA", LOCALHOST);
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
assertThat(target).as("target should not be null").isNotNull();
TimeUnit.MILLISECONDS.sleep(10);
controllerManagement.findOrRegisterTargetIfItDoesNotexist("AA", LOCALHOST);
controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
TimeUnit.MILLISECONDS.sleep(repositoryProperties.getPollPersistenceFlushTime() + 1);
final Target updated = targetManagement.get(target.getId()).get();

View File

@@ -472,7 +472,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest {
Target target = createTargetWithAttributes("4711");
final long current = System.currentTimeMillis();
controllerManagement.findOrRegisterTargetIfItDoesNotexist("4711", LOCALHOST);
controllerManagement.findOrRegisterTargetIfItDoesNotExist("4711", LOCALHOST);
final DistributionSetAssignmentResult result = assignDistributionSet(set.getId(), "4711");
@@ -858,7 +858,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest {
@Expect(type = TargetPollEvent.class, count = 1) })
public void targetCanBeReadWithOnlyReadTargetPermission() throws Exception {
final String knownTargetControllerId = "readTarget";
controllerManagement.findOrRegisterTargetIfItDoesNotexist(knownTargetControllerId, new URI("http://127.0.0.1"));
controllerManagement.findOrRegisterTargetIfItDoesNotExist(knownTargetControllerId, new URI("http://127.0.0.1"));
securityRule.runAs(WithSpringAuthorityRule.withUser("bumlux", "READ_TARGET"), () -> {
final Target findTargetByControllerID = targetManagement.getByControllerID(knownTargetControllerId).get();

View File

@@ -48,7 +48,7 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
.name("targetName123").description("targetDesc123"));
attributes.put("revision", "1.1");
target = controllerManagement.updateControllerAttributes(target.getControllerId(), attributes, null);
target = controllerManagement.findOrRegisterTargetIfItDoesNotexist(target.getControllerId(), LOCALHOST);
target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target.getControllerId(), LOCALHOST);
createTargetMetadata(target.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "metaValue"));
target2 = targetManagement
@@ -56,7 +56,7 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
attributes.put("revision", "1.2");
Thread.sleep(1);
target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), attributes, null);
target2 = controllerManagement.findOrRegisterTargetIfItDoesNotexist(target2.getControllerId(), LOCALHOST);
target2 = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target2.getControllerId(), LOCALHOST);
createTargetMetadata(target2.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "value"));
final Target target3 = testdataFactory.createTarget("targetId1235");