Changes to enable adjusting limits on delete conflict handling through configuration.
This commit is contained in:
parent
e22549e0c4
commit
a60e5809b2
|
@ -1244,7 +1244,7 @@ public enum Pointcut {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <b>Storage Hook:</b>
|
* <b>Storage Hook:</b>
|
||||||
* Invoked before a resource will be created
|
* Invoked before a resource will be deleted
|
||||||
* <p>
|
* <p>
|
||||||
* Hooks will have access to the contents of the resource being deleted
|
* Hooks will have access to the contents of the resource being deleted
|
||||||
* but should not make any changes as storage has already occurred
|
* but should not make any changes as storage has already occurred
|
||||||
|
@ -1282,7 +1282,7 @@ public enum Pointcut {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <b>Storage Hook:</b>
|
* <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>
|
* <p>
|
||||||
* Hooks will have access to the list of resources that have references to the resource being deleted.
|
* Hooks will have access to the list of resources that have references to the resource being deleted.
|
||||||
* </p>
|
* </p>
|
||||||
|
|
|
@ -83,6 +83,9 @@ public class DaoConfig {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(DaoConfig.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(DaoConfig.class);
|
||||||
private static final int DEFAULT_EXPUNGE_BATCH_SIZE = 800;
|
private static final int DEFAULT_EXPUNGE_BATCH_SIZE = 800;
|
||||||
private IndexEnabledEnum myIndexMissingFieldsEnabled = IndexEnabledEnum.DISABLED;
|
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
|
* Child Configurations
|
||||||
|
@ -153,6 +156,10 @@ public class DaoConfig {
|
||||||
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
|
private ClientIdStrategyEnum myResourceClientIdStrategy = ClientIdStrategyEnum.ALPHANUMERIC;
|
||||||
private boolean myFilterParameterEnabled = false;
|
private boolean myFilterParameterEnabled = false;
|
||||||
private StoreMetaSourceInformationEnum myStoreMetaSourceInformation = StoreMetaSourceInformationEnum.SOURCE_URI_AND_REQUEST_ID;
|
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}!
|
* Do not change default of {@code true}!
|
||||||
*
|
*
|
||||||
|
@ -2021,4 +2028,33 @@ public class DaoConfig {
|
||||||
*/
|
*/
|
||||||
ANY
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.exceptions.ResourceVersionConflictException;
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import ca.uhn.fhir.util.OperationOutcomeUtil;
|
import ca.uhn.fhir.util.OperationOutcomeUtil;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -51,8 +52,8 @@ import java.util.List;
|
||||||
public class DeleteConflictService {
|
public class DeleteConflictService {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class);
|
||||||
public static final int FIRST_QUERY_RESULT_COUNT = 1;
|
public static final int FIRST_QUERY_RESULT_COUNT = 1;
|
||||||
public static final int RETRY_QUERY_RESULT_COUNT = 60;
|
public static int MAX_RETRY_ATTEMPTS = 10;
|
||||||
public static final 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
|
@Autowired
|
||||||
DeleteConflictFinderService myDeleteConflictFinderService;
|
DeleteConflictFinderService myDeleteConflictFinderService;
|
||||||
|
@ -81,11 +82,16 @@ public class DeleteConflictService {
|
||||||
while (outcome != null) {
|
while (outcome != null) {
|
||||||
int shouldRetryCount = Math.min(outcome.getShouldRetryCount(), MAX_RETRY_ATTEMPTS);
|
int shouldRetryCount = Math.min(outcome.getShouldRetryCount(), MAX_RETRY_ATTEMPTS);
|
||||||
if (!(retryCount < shouldRetryCount)) break;
|
if (!(retryCount < shouldRetryCount)) break;
|
||||||
newConflicts = new DeleteConflictList();
|
newConflicts = new DeleteConflictList(newConflicts);
|
||||||
outcome = findAndHandleConflicts(theRequest, newConflicts, theEntity, theForValidate, RETRY_QUERY_RESULT_COUNT, theTransactionDetails);
|
outcome = findAndHandleConflicts(theRequest, newConflicts, theEntity, theForValidate, myDaoConfig.getMaximumDeleteConflictQueryCount(), theTransactionDetails);
|
||||||
++retryCount;
|
++retryCount;
|
||||||
}
|
}
|
||||||
theDeleteConflicts.addAll(newConflicts);
|
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;
|
return retryCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,4 +168,9 @@ public class DeleteConflictService {
|
||||||
|
|
||||||
throw new ResourceVersionConflictException(firstMsg, oo);
|
throw new ResourceVersionConflictException(firstMsg, oo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static void setMaxRetryAttempts(Integer theMaxRetryAttempts) {
|
||||||
|
MAX_RETRY_ATTEMPTS = theMaxRetryAttempts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.delete;
|
||||||
|
|
||||||
import ca.uhn.fhir.interceptor.api.Hook;
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
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.api.model.DeleteConflictList;
|
||||||
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
|
||||||
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
|
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
|
||||||
|
@ -17,6 +18,7 @@ import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -28,6 +30,9 @@ import static org.junit.Assert.*;
|
||||||
public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
|
public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictServiceR4Test.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictServiceR4Test.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
DaoConfig myDaoConfig;
|
||||||
|
|
||||||
private DeleteConflictInterceptor myDeleteInterceptor = new DeleteConflictInterceptor();
|
private DeleteConflictInterceptor myDeleteInterceptor = new DeleteConflictInterceptor();
|
||||||
private int myInterceptorDeleteCount;
|
private int myInterceptorDeleteCount;
|
||||||
|
|
||||||
|
@ -133,6 +138,48 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
|
||||||
assertEquals(3, myInterceptorDeleteCount);
|
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
|
@Test
|
||||||
public void testBadInterceptorNoInfiniteLoop() {
|
public void testBadInterceptorNoInfiniteLoop() {
|
||||||
Organization organization = new Organization();
|
Organization organization = new Organization();
|
||||||
|
@ -150,7 +197,7 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
|
||||||
myOrganizationDao.delete(organizationId);
|
myOrganizationDao.delete(organizationId);
|
||||||
fail();
|
fail();
|
||||||
} catch (ResourceVersionConflictException e) {
|
} 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);
|
assertEquals(1 + DeleteConflictService.MAX_RETRY_ATTEMPTS, myDeleteInterceptor.myCallCount);
|
||||||
}
|
}
|
||||||
|
@ -198,6 +245,21 @@ public class DeleteConflictServiceR4Test extends BaseJpaR4Test {
|
||||||
return new DeleteConflictOutcome().setShouldRetryCount(myInterceptorDeleteCount);
|
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 {
|
private static class DeleteConflictInterceptor {
|
||||||
int myCallCount;
|
int myCallCount;
|
||||||
DeleteConflictList myDeleteConflictList;
|
DeleteConflictList myDeleteConflictList;
|
||||||
|
|
Loading…
Reference in New Issue