Changes to enable adjusting limits on delete conflict handling through configuration.

This commit is contained in:
ianmarshall 2020-05-22 10:15:04 -04:00
parent e22549e0c4
commit a60e5809b2
4 changed files with 116 additions and 7 deletions

View File

@ -1244,7 +1244,7 @@ public enum Pointcut {
/**
* <b>Storage Hook:</b>
* Invoked before a resource will be created
* Invoked before a resource will be deleted
* <p>
* Hooks will have access to the contents of the resource being deleted
* but should not make any changes as storage has already occurred
@ -1282,7 +1282,7 @@ public enum Pointcut {
/**
* <b>Storage Hook:</b>
* Invoked when a resource delete operation is about to fail due to referential integrity hcts.
* Invoked when a resource delete operation is about to fail due to referential integrity checks. Intended for use with ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor.
* <p>
* Hooks will have access to the list of resources that have references to the resource being deleted.
* </p>

View File

@ -83,6 +83,9 @@ public class DaoConfig {
private static final Logger ourLog = LoggerFactory.getLogger(DaoConfig.class);
private static final int DEFAULT_EXPUNGE_BATCH_SIZE = 800;
private IndexEnabledEnum myIndexMissingFieldsEnabled = IndexEnabledEnum.DISABLED;
private static final int DEFAULT_MAXIMUM_DELETE_CONFLICT_COUNT = 60;
private static final int DEFAULT_MAXIMUM_DELETE_CONFLICT_RETRY_ATTEMPTS = 10;
/**
* Child Configurations
@ -153,6 +156,10 @@ public class DaoConfig {
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
private boolean myFilterParameterEnabled = false;
private StoreMetaSourceInformationEnum myStoreMetaSourceInformation = StoreMetaSourceInformationEnum.SOURCE_URI_AND_REQUEST_ID;
/**
* update setter javadoc if default changes
*/
private Integer myMaximumDeleteConflictQueryCount = DEFAULT_MAXIMUM_DELETE_CONFLICT_COUNT;
/**
* Do not change default of {@code true}!
*
@ -2021,4 +2028,33 @@ public class DaoConfig {
*/
ANY
}
/**
* <p>
* This determines the maximum number of conflicts that should be fetched and handled while retrying a delete of a resource.
* </p>
* <p>
* The default value for this setting is {@code 60}.
* </p>
*
* @since 4.1.0
*/
public Integer getMaximumDeleteConflictQueryCount() {
return myMaximumDeleteConflictQueryCount;
}
/**
* <p>
* This determines the maximum number of conflicts that should be fetched and handled while retrying a delete of a resource.
* </p>
* <p>
* The default value for this setting is {@code 60}.
* </p>
*
* @since 4.1.0
*/
public void setMaximumDeleteConflictQueryCount(Integer theMaximumDeleteConflictQueryCount) {
myMaximumDeleteConflictQueryCount = theMaximumDeleteConflictQueryCount;
}
}

View File

@ -38,6 +38,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,8 +52,8 @@ import java.util.List;
public class DeleteConflictService {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
public static final int FIRST_QUERY_RESULT_COUNT = 1;
public static final int RETRY_QUERY_RESULT_COUNT = 60;
public static final int MAX_RETRY_ATTEMPTS = 10;
public static int MAX_RETRY_ATTEMPTS = 10;
public static String MAX_RETRY_ATTEMPTS_EXCEEDED_MSG = "Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count.";
@Autowired
DeleteConflictFinderService myDeleteConflictFinderService;
@ -81,11 +82,16 @@ public class DeleteConflictService {
while (outcome != null) {
int shouldRetryCount = Math.min(outcome.getShouldRetryCount(), MAX_RETRY_ATTEMPTS);
if (!(retryCount < shouldRetryCount)) break;
newConflicts = new DeleteConflictList();
outcome = findAndHandleConflicts(theRequest, newConflicts, theEntity, theForValidate, RETRY_QUERY_RESULT_COUNT, theTransactionDetails);
newConflicts = new DeleteConflictList(newConflicts);
outcome = findAndHandleConflicts(theRequest, newConflicts, theEntity, theForValidate, myDaoConfig.getMaximumDeleteConflictQueryCount(), theTransactionDetails);
++retryCount;
}
theDeleteConflicts.addAll(newConflicts);
if(retryCount >= MAX_RETRY_ATTEMPTS && !newConflicts.isEmpty()) {
IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(myFhirContext);
OperationOutcomeUtil.addIssue(myFhirContext, oo, BaseHapiFhirDao.OO_SEVERITY_ERROR, MAX_RETRY_ATTEMPTS_EXCEEDED_MSG,null, "processing");
throw new ResourceVersionConflictException(MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, oo);
}
return retryCount;
}
@ -162,4 +168,9 @@ public class DeleteConflictService {
throw new ResourceVersionConflictException(firstMsg, oo);
}
@VisibleForTesting
static void setMaxRetryAttempts(Integer theMaxRetryAttempts) {
MAX_RETRY_ATTEMPTS = theMaxRetryAttempts;
}
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.delete;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
@ -17,6 +18,7 @@ import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Iterator;
@ -28,6 +30,9 @@ import static org.junit.Assert.*;
public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictServiceR4Test.class);
@Autowired
DaoConfig myDaoConfig;
private DeleteConflictInterceptor myDeleteInterceptor = new DeleteConflictInterceptor();
private int myInterceptorDeleteCount;
@ -133,6 +138,48 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
assertEquals(3, myInterceptorDeleteCount);
}
@Test
public void testDeleteHookDeletesLargeNumberOfConflicts() {
Organization organization = new Organization();
organization.setName("FOO");
IIdType organizationId = myOrganizationDao.create(organization).getId().toUnqualifiedVersionless();
// Create 12 conflicts.
for (int j=0; j < 12 ; j++) {
Patient patient = new Patient();
patient.setManagingOrganization(new Reference(organizationId));
myPatientDao.create(patient).getId().toUnqualifiedVersionless();
}
DeleteConflictService.setMaxRetryAttempts(3);
myDaoConfig.setMaximumDeleteConflictQueryCount(5);
myDeleteInterceptor.deleteConflictFunction = this::deleteConflictsFixedRetryCount;
try {
myOrganizationDao.delete(organizationId);
// Needs a fourth pass to ensure that all conflicts are now gone.
fail();
} catch (ResourceVersionConflictException e) {
assertEquals(DeleteConflictService.MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, e.getMessage());
}
// Try again with Maximum conflict count set to 6.
myDeleteInterceptor.myCallCount=0;
myInterceptorDeleteCount = 0;
myDaoConfig.setMaximumDeleteConflictQueryCount(6);
try {
myOrganizationDao.delete(organizationId);
} catch (ResourceVersionConflictException e) {
fail();
}
assertNotNull(myDeleteInterceptor.myDeleteConflictList);
assertEquals(3, myDeleteInterceptor.myCallCount);
assertEquals(12, myInterceptorDeleteCount);
}
@Test
public void testBadInterceptorNoInfiniteLoop() {
Organization organization = new Organization();
@ -150,7 +197,7 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
myOrganizationDao.delete(organizationId);
fail();
} catch (ResourceVersionConflictException e) {
assertEquals("Unable to delete Organization/" + organizationId.getIdPart() + " because at least one resource has a reference to this resource. First reference found was resource Patient/" + patientId.getIdPart() + " in path Patient.managingOrganization", e.getMessage());
assertEquals(DeleteConflictService.MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, e.getMessage());
}
assertEquals(1 + DeleteConflictService.MAX_RETRY_ATTEMPTS, myDeleteInterceptor.myCallCount);
}
@ -198,6 +245,21 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
return new DeleteConflictOutcome().setShouldRetryCount(myInterceptorDeleteCount);
}
private DeleteConflictOutcome deleteConflictsFixedRetryCount(DeleteConflictList theList) {
Iterator<DeleteConflict> iterator = theList.iterator();
while (iterator.hasNext()) {
DeleteConflict next = iterator.next();
IdDt source = next.getSourceId();
if ("Patient".equals(source.getResourceType())) {
ourLog.info("Deleting {}", source);
myPatientDao.delete(source, theList, null, null);
// myPatientDao.delete(source);
++myInterceptorDeleteCount;
}
}
return new DeleteConflictOutcome().setShouldRetryCount(DeleteConflictService.MAX_RETRY_ATTEMPTS);
}
private static class DeleteConflictInterceptor {
int myCallCount;
DeleteConflictList myDeleteConflictList;