FIx deadlock in transaction processing, and put transaction outcome

in the right spot
This commit is contained in:
James 2017-07-21 07:39:11 -04:00
parent 6a178b08bd
commit a13c78d6cc
6 changed files with 426 additions and 280 deletions

View File

@ -19,48 +19,21 @@ package ca.uhn.fhir.jpa.dao.dstu3;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.Validate;
import org.apache.http.NameValuePair; import org.apache.http.NameValuePair;
import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Meta;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.*;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
@ -69,10 +42,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.*;
import ca.uhn.fhir.jpa.dao.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.entity.ResourceTable; import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.jpa.entity.TagDefinition; import ca.uhn.fhir.jpa.entity.TagDefinition;
import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails; import ca.uhn.fhir.jpa.provider.ServletSubRequestDetails;
@ -80,19 +50,12 @@ import ca.uhn.fhir.jpa.util.DeleteConflict;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.method.*;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.BaseResourceReturningMethodBinding;
import ca.uhn.fhir.rest.method.BaseResourceReturningMethodBinding.ResourceOrDstu1Bundle; import ca.uhn.fhir.rest.method.BaseResourceReturningMethodBinding.ResourceOrDstu1Bundle;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
@ -121,6 +84,8 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) { for (final BundleEntryComponent nextRequestEntry : theRequest.getEntry()) {
BaseServerResponseExceptionHolder caughtEx = new BaseServerResponseExceptionHolder();
TransactionCallback<Bundle> callback = new TransactionCallback<Bundle>() { TransactionCallback<Bundle> callback = new TransactionCallback<Bundle>() {
@Override @Override
public Bundle doInTransaction(TransactionStatus theStatus) { public Bundle doInTransaction(TransactionStatus theStatus) {
@ -133,13 +98,12 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} }
}; };
BaseServerResponseException caughtEx;
try { try {
Bundle nextResponseBundle = txTemplate.execute(callback); Bundle nextResponseBundle = callback.doInTransaction(null);
caughtEx = null;
BundleEntryComponent subResponseEntry = nextResponseBundle.getEntry().get(0); BundleEntryComponent subResponseEntry = nextResponseBundle.getEntry().get(0);
resp.addEntry(subResponseEntry); resp.addEntry(subResponseEntry);
/* /*
* If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it
*/ */
@ -148,21 +112,19 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} }
} catch (BaseServerResponseException e) { } catch (BaseServerResponseException e) {
caughtEx = e; caughtEx.setException(e);
} catch (Throwable t) { } catch (Throwable t) {
ourLog.error("Failure during BATCH sub transaction processing", t); ourLog.error("Failure during BATCH sub transaction processing", t);
caughtEx = new InternalErrorException(t); caughtEx.setException(new InternalErrorException(t));
} }
if (caughtEx != null) { if (caughtEx.getException() != null) {
BundleEntryComponent nextEntry = resp.addEntry(); BundleEntryComponent nextEntry = resp.addEntry();
OperationOutcome oo = new OperationOutcome(); populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry);
oo.addIssue().setSeverity(IssueSeverity.ERROR).setDiagnostics(caughtEx.getMessage());
nextEntry.setResource(oo);
BundleEntryResponseComponent nextEntryResp = nextEntry.getResponse(); BundleEntryResponseComponent nextEntryResp = nextEntry.getResponse();
nextEntryResp.setStatus(toStatusString(caughtEx.getStatusCode())); nextEntryResp.setStatus(toStatusString(caughtEx.getException().getStatusCode()));
} }
} }
@ -173,117 +135,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return resp; return resp;
} }
private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) { private Bundle doTransaction(final ServletRequestDetails theRequestDetails, final Bundle theRequest, final String theActionName) {
String url = nextEntry.getRequest().getUrl();
if (isBlank(url)) {
throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
}
return url;
}
/**
* This method is called for nested bundles (e.g. if we received a transaction with an entry that
* was a GET search, this method is called on the bundle for the search result, that will be placed in the
* outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
*
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = getContext().newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
private IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao<? extends IBaseResource> retVal = getDao(theClass);
if (retVal == null) {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName());
}
return retVal;
}
@Override
public Meta metaGetOperation(RequestDetails theRequestDetails) {
// Notify interceptors
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t)";
TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
List<TagDefinition> tagDefinitions = q.getResultList();
Meta retVal = toMeta(tagDefinitions);
return retVal;
}
protected Meta toMeta(Collection<TagDefinition> tagDefinitions) {
Meta retVal = new Meta();
for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) {
case PROFILE:
retVal.addProfile(next.getCode());
break;
case SECURITY_LABEL:
retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
case TAG:
retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
}
}
return retVal;
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = getContext().getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
// if (theParts.getResourceId() == null && theParts.getParams() == null) {
// String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
// throw new InvalidRequestException(msg);
// }
return dao;
}
@Transactional(propagation = Propagation.REQUIRED)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
}
String actionName = "Transaction";
return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
super.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return doTransaction(theRequestDetails, theRequest, theActionName);
} finally {
super.clearRequestAsProcessingSubRequest(theRequestDetails);
}
}
@SuppressWarnings("unchecked")
private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
BundleType transactionType = theRequest.getTypeElement().getValue(); BundleType transactionType = theRequest.getTypeElement().getValue();
if (transactionType == BundleType.BATCH) { if (transactionType == BundleType.BATCH) {
return batch(theRequestDetails, theRequest); return batch(theRequestDetails, theRequest);
@ -301,11 +153,11 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size()); ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Date updateTime = new Date(); final Date updateTime = new Date();
Set<IdType> allIds = new LinkedHashSet<IdType>(); final Set<IdType> allIds = new LinkedHashSet<IdType>();
Map<IdType, IdType> idSubstitutions = new HashMap<IdType, IdType>(); final Map<IdType, IdType> idSubstitutions = new HashMap<IdType, IdType>();
Map<IdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdType, DaoMethodOutcome>(); final Map<IdType, DaoMethodOutcome> idToPersistedOutcome = new HashMap<IdType, DaoMethodOutcome>();
// Do all entries have a verb? // Do all entries have a verb?
for (int i = 0; i < theRequest.getEntry().size(); i++) { for (int i = 0; i < theRequest.getEntry().size(); i++) {
@ -326,9 +178,9 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
* are saved in a two-phase way in order to deal with interdependencies, and * are saved in a two-phase way in order to deal with interdependencies, and
* we want the GET processing to use the final indexing state * we want the GET processing to use the final indexing state
*/ */
Bundle response = new Bundle(); final Bundle response = new Bundle();
List<BundleEntryComponent> getEntries = new ArrayList<BundleEntryComponent>(); List<BundleEntryComponent> getEntries = new ArrayList<BundleEntryComponent>();
IdentityHashMap<BundleEntryComponent, Integer> originalRequestOrder = new IdentityHashMap<Bundle.BundleEntryComponent, Integer>(); final IdentityHashMap<BundleEntryComponent, Integer> originalRequestOrder = new IdentityHashMap<Bundle.BundleEntryComponent, Integer>();
for (int i = 0; i < theRequest.getEntry().size(); i++) { for (int i = 0; i < theRequest.getEntry().size(); i++) {
originalRequestOrder.put(theRequest.getEntry().get(i), i); originalRequestOrder.put(theRequest.getEntry().get(i), i);
response.addEntry(); response.addEntry();
@ -338,6 +190,108 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} }
Collections.sort(theRequest.getEntry(), new TransactionSorter()); Collections.sort(theRequest.getEntry(), new TransactionSorter());
/*
* All of the write operations in the transaction (PUT, POST, etc.. basically anything
* except GET) are performed in their own database transaction before we do the reads.
* We do this because the reads (specifically the searches) often spawn their own
* secondary database transaction and if we allow that within the primary
* database transaction we can end up with deadlocks if the server is under
* heavy load with lots of concurrent transactions using all available
* database connections.
*/
TransactionTemplate txManager = new TransactionTemplate(myTxManager);
Map<BundleEntryComponent, ResourceTable> entriesToProcess = txManager.execute(new TransactionCallback<Map<BundleEntryComponent, ResourceTable>>() {
@Override
public Map<BundleEntryComponent, ResourceTable> doInTransaction(TransactionStatus status) {
return doTransactionWriteOperations(theRequestDetails, theRequest, theActionName, updateTime, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder);
}
});
for (Entry<BundleEntryComponent, ResourceTable> nextEntry : entriesToProcess.entrySet()) {
String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue();
String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart();
nextEntry.getKey().getResponse().setLocation(responseLocation);
nextEntry.getKey().getResponse().setEtag(responseEtag);
}
/*
* Loop through the request and process any entries of type GET
*/
for (int i = 0; i < getEntries.size(); i++) {
BundleEntryComponent nextReqEntry = getEntries.get(i);
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
BundleEntryComponent nextRespEntry = response.getEntry().get(originalOrder);
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails();
requestDetails.setServletRequest(theRequestDetails.getServletRequest());
requestDetails.setRequestType(RequestTypeEnum.GET);
requestDetails.setServer(theRequestDetails.getServer());
String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerb.GET);
int qIndex = url.indexOf('?');
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
requestDetails.setParameters(new HashMap<String, String[]>());
if (qIndex != -1) {
String params = url.substring(qIndex);
List<NameValuePair> parameters = translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
for (java.util.Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) {
String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]);
requestDetails.getParameters().put(nextParamEntry.getKey(), nextValue);
}
url = url.substring(0, qIndex);
}
requestDetails.setRequestPath(url);
requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase());
theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url);
if (method == null) {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) {
requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch());
}
if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist());
}
if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch());
}
Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url);
try {
ResourceOrDstu1Bundle responseData = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails);
IBaseResource resource = responseData.getResource();
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) {
resource = filterNestedBundle(requestDetails, resource);
}
nextRespEntry.setResource((Resource) resource);
nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
} catch (NotModifiedException e) {
nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
} catch (BaseServerResponseException e) {
ourLog.info("Failure processing transaction GET {}: {}", url, e.toString());
nextRespEntry.getResponse().setStatus(toStatusString(e.getStatusCode()));
populateEntryWithOperationOutcome(e, nextRespEntry);
}
}
long delay = System.currentTimeMillis() - start;
ourLog.info(theActionName + " completed in {}ms", new Object[] { delay });
response.setType(BundleType.TRANSACTIONRESPONSE);
return response;
}
@SuppressWarnings("unchecked")
private Map<BundleEntryComponent, ResourceTable> doTransactionWriteOperations(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName, Date updateTime, Set<IdType> allIds,
Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, Bundle response, IdentityHashMap<BundleEntryComponent, Integer> originalRequestOrder) {
Set<String> deletedResources = new HashSet<String>(); Set<String> deletedResources = new HashSet<String>();
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>(); List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
Map<BundleEntryComponent, ResourceTable> entriesToProcess = new IdentityHashMap<BundleEntryComponent, ResourceTable>(); Map<BundleEntryComponent, ResourceTable> entriesToProcess = new IdentityHashMap<BundleEntryComponent, ResourceTable>();
@ -562,87 +516,122 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
} }
ourLog.info("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); ourLog.info("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement);
} }
return entriesToProcess;
}
/* private String extractTransactionUrlOrThrowException(BundleEntryComponent nextEntry, HTTPVerb verb) {
* Loop through the request and process any entries of type GET String url = nextEntry.getRequest().getUrl();
*/ if (isBlank(url)) {
for (int i = 0; i < getEntries.size(); i++) { throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name()));
BundleEntryComponent nextReqEntry = getEntries.get(i); }
Integer originalOrder = originalRequestOrder.get(nextReqEntry); return url;
BundleEntryComponent nextRespEntry = response.getEntry().get(originalOrder); }
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(); /**
requestDetails.setServletRequest(theRequestDetails.getServletRequest()); * This method is called for nested bundles (e.g. if we received a transaction with an entry that
requestDetails.setRequestType(RequestTypeEnum.GET); * was a GET search, this method is called on the bundle for the search result, that will be placed in the
requestDetails.setServer(theRequestDetails.getServer()); * outer bundle). This method applies the _summary and _content parameters to the output of
* that bundle.
*
* TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future.
*/
private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) {
IParser p = getContext().newJsonParser();
RestfulServerUtils.configureResponseParser(theRequestDetails, p);
return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource));
}
String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerb.GET); private IFhirResourceDao<?> getDaoOrThrowException(Class<? extends IBaseResource> theClass) {
IFhirResourceDao<? extends IBaseResource> retVal = getDao(theClass);
if (retVal == null) {
throw new InvalidRequestException("Unable to process request, this server does not know how to handle resources of type " + getContext().getResourceDefinition(theClass).getName());
}
return retVal;
}
int qIndex = url.indexOf('?'); @Override
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create(); public Meta metaGetOperation(RequestDetails theRequestDetails) {
requestDetails.setParameters(new HashMap<String, String[]>()); // Notify interceptors
if (qIndex != -1) { ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails);
String params = url.substring(qIndex); notifyInterceptors(RestOperationTypeEnum.META, requestDetails);
List<NameValuePair> parameters = translateMatchUrl(params);
for (NameValuePair next : parameters) {
paramValues.put(next.getName(), next.getValue());
}
for (java.util.Map.Entry<String, Collection<String>> nextParamEntry : paramValues.asMap().entrySet()) {
String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]);
requestDetails.getParameters().put(nextParamEntry.getKey(), nextValue);
}
url = url.substring(0, qIndex);
}
requestDetails.setRequestPath(url); String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t)";
requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase()); TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
List<TagDefinition> tagDefinitions = q.getResultList();
theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url); Meta retVal = toMeta(tagDefinitions);
BaseMethodBinding<?> method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url);
if (method == null) {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { return retVal;
requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch()); }
}
if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist());
}
if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) {
requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch());
}
if (method instanceof BaseResourceReturningMethodBinding) { private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BundleEntryComponent nextEntry) {
try { OperationOutcome oo = new OperationOutcome();
ResourceOrDstu1Bundle responseData = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails); oo.addIssue().setSeverity(IssueSeverity.ERROR).setDiagnostics(caughtEx.getMessage());
IBaseResource resource = responseData.getResource(); nextEntry.getResponse().setOutcome(oo);
if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) { }
resource = filterNestedBundle(requestDetails, resource);
}
nextRespEntry.setResource((Resource) resource);
nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK));
} catch (NotModifiedException e) {
nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED));
}
} else {
throw new IllegalArgumentException("Unable to handle GET " + url);
}
private ca.uhn.fhir.jpa.dao.IFhirResourceDao<? extends IBaseResource> toDao(UrlParts theParts, String theVerb, String theUrl) {
RuntimeResourceDefinition resType;
try {
resType = getContext().getResourceDefinition(theParts.getResourceType());
} catch (DataFormatException e) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
}
IFhirResourceDao<? extends IBaseResource> dao = null;
if (resType != null) {
dao = getDao(resType.getImplementingClass());
}
if (dao == null) {
String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
throw new InvalidRequestException(msg);
} }
for (Entry<BundleEntryComponent, ResourceTable> nextEntry : entriesToProcess.entrySet()) { // if (theParts.getResourceId() == null && theParts.getParams() == null) {
String responseLocation = nextEntry.getValue().getIdDt().toUnqualified().getValue(); // String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl);
String responseEtag = nextEntry.getValue().getIdDt().getVersionIdPart(); // throw new InvalidRequestException(msg);
nextEntry.getKey().getResponse().setLocation(responseLocation); // }
nextEntry.getKey().getResponse().setEtag(responseEtag);
return dao;
}
protected Meta toMeta(Collection<TagDefinition> tagDefinitions) {
Meta retVal = new Meta();
for (TagDefinition next : tagDefinitions) {
switch (next.getTagType()) {
case PROFILE:
retVal.addProfile(next.getCode());
break;
case SECURITY_LABEL:
retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
case TAG:
retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
break;
}
}
return retVal;
}
@Transactional(propagation = Propagation.NEVER)
@Override
public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) {
if (theRequestDetails != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null);
notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails);
} }
long delay = System.currentTimeMillis() - start; String actionName = "Transaction";
ourLog.info(theActionName + " completed in {}ms", new Object[] { delay }); return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName);
}
response.setType(BundleType.TRANSACTIONRESPONSE); private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) {
return response; super.markRequestAsProcessingSubRequest(theRequestDetails);
try {
return doTransaction(theRequestDetails, theRequest, theActionName);
} finally {
super.clearRequestAsProcessingSubRequest(theRequestDetails);
}
} }
private static void handleTransactionCreateOrUpdateOutcome(Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome, private static void handleTransactionCreateOrUpdateOutcome(Map<IdType, IdType> idSubstitutions, Map<IdType, DaoMethodOutcome> idToPersistedOutcome, IdType nextResourceId, DaoMethodOutcome outcome,
@ -693,6 +682,19 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode));
} }
private static class BaseServerResponseExceptionHolder
{
private BaseServerResponseException myException;
public BaseServerResponseException getException() {
return myException;
}
public void setException(BaseServerResponseException myException) {
this.myException = myException;
}
}
//@formatter:off //@formatter:off
/** /**
* Transaction Order, per the spec: * Transaction Order, per the spec:

View File

@ -34,12 +34,10 @@ import javax.persistence.SequenceGenerator;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.UniqueConstraint; import javax.persistence.UniqueConstraint;
//@formatter:off
@Entity @Entity
@Table(name = "HFJ_SEARCH_RESULT", uniqueConstraints= { @Table(name = "HFJ_SEARCH_RESULT", uniqueConstraints= {
@UniqueConstraint(name="IDX_SEARCHRES_ORDER", columnNames= {"SEARCH_PID", "SEARCH_ORDER"}) @UniqueConstraint(name="IDX_SEARCHRES_ORDER", columnNames= {"SEARCH_PID", "SEARCH_ORDER"})
}) })
//@formatter:on
public class SearchResult implements Serializable { public class SearchResult implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.search; package ca.uhn.fhir.jpa.search;
/*-
* #%L
* HAPI FHIR JPA Server
* %%
* Copyright (C) 2014 - 2017 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.Date; import java.util.Date;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;

View File

@ -44,6 +44,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
* starvation * starvation
*/ */
int maxThreads = (int)(Math.random() * 6) + 2; int maxThreads = (int)(Math.random() * 6) + 2;
maxThreads = 2;
retVal.setMaxTotal(maxThreads); retVal.setMaxTotal(maxThreads);
DataSource dataSource = ProxyDataSourceBuilder DataSource dataSource = ProxyDataSourceBuilder

View File

@ -26,31 +26,14 @@ import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hl7.fhir.dstu3.model.Appointment; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryRequestComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryRequestComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryResponseComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType; import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb; import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Condition;
import org.hl7.fhir.dstu3.model.DiagnosticReport;
import org.hl7.fhir.dstu3.model.Encounter;
import org.hl7.fhir.dstu3.model.EpisodeOfCare;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Medication;
import org.hl7.fhir.dstu3.model.MedicationRequest;
import org.hl7.fhir.dstu3.model.Meta;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; import org.hl7.fhir.dstu3.model.Observation.ObservationStatus;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.dstu3.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.dstu3.model.Organization;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.After; import org.junit.After;
@ -497,9 +480,10 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertThat(resp.getEntry().get(0).getResponse().getLocation(), startsWith("Patient/")); assertThat(resp.getEntry().get(0).getResponse().getLocation(), startsWith("Patient/"));
// Bundle.entry[1] is failed read response // Bundle.entry[1] is failed read response
assertEquals(OperationOutcome.class, resp.getEntry().get(1).getResource().getClass()); Resource oo = resp.getEntry().get(1).getResponse().getOutcome();
assertEquals(IssueSeverity.ERROR, ((OperationOutcome) resp.getEntry().get(1).getResource()).getIssue().get(0).getSeverityElement().getValue()); assertEquals(OperationOutcome.class, oo.getClass());
assertEquals("Resource Patient/THIS_ID_DOESNT_EXIST is not known", ((OperationOutcome) resp.getEntry().get(1).getResource()).getIssue().get(0).getDiagnostics()); assertEquals(IssueSeverity.ERROR, ((OperationOutcome) oo).getIssue().get(0).getSeverityElement().getValue());
assertEquals("Resource Patient/THIS_ID_DOESNT_EXIST is not known", ((OperationOutcome) oo).getIssue().get(0).getDiagnostics());
assertEquals("404 Not Found", resp.getEntry().get(1).getResponse().getStatus()); assertEquals("404 Not Found", resp.getEntry().get(1).getResponse().getStatus());
// Check POST // Check POST
@ -891,6 +875,143 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
} }
} }
@Test
public void testTransactionCreateWithBadRead() {
Bundle request = new Bundle();
request.setType(BundleType.TRANSACTION);
Patient p;
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("FOO");
request
.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient");
request
.addEntry()
.getRequest()
.setMethod(HTTPVerb.GET)
.setUrl("Patient/BABABABA");
Bundle response = mySystemDao.transaction(mySrd, request);
assertEquals(2, response.getEntry().size());
assertEquals("201 Created", response.getEntry().get(0).getResponse().getStatus());
assertThat(response.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+.*"));
assertEquals("404 Not Found", response.getEntry().get(1).getResponse().getStatus());
OperationOutcome oo = (OperationOutcome) response.getEntry().get(1).getResponse().getOutcome();
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals(IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity());
assertEquals("Resource Patient/BABABABA is not known", oo.getIssue().get(0).getDiagnostics());
}
@Test
public void testTransactionCreateWithBadSearch() {
Bundle request = new Bundle();
request.setType(BundleType.TRANSACTION);
Patient p;
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("FOO");
request
.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient");
request
.addEntry()
.getRequest()
.setMethod(HTTPVerb.GET)
.setUrl("Patient?foobadparam=1");
Bundle response = mySystemDao.transaction(mySrd, request);
assertEquals(2, response.getEntry().size());
assertEquals("201 Created", response.getEntry().get(0).getResponse().getStatus());
assertThat(response.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+.*"));
assertEquals("400 Bad Request", response.getEntry().get(1).getResponse().getStatus());
OperationOutcome oo = (OperationOutcome) response.getEntry().get(1).getResponse().getOutcome();
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals(IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity());
assertThat(oo.getIssue().get(0).getDiagnostics(), containsString("Unknown search parameter"));
}
@Test
public void testBatchCreateWithBadRead() {
Bundle request = new Bundle();
request.setType(BundleType.BATCH);
Patient p;
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("FOO");
request
.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient");
request
.addEntry()
.getRequest()
.setMethod(HTTPVerb.GET)
.setUrl("Patient/BABABABA");
Bundle response = mySystemDao.transaction(mySrd, request);
assertEquals(2, response.getEntry().size());
assertEquals("201 Created", response.getEntry().get(0).getResponse().getStatus());
assertThat(response.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+.*"));
assertEquals("404 Not Found", response.getEntry().get(1).getResponse().getStatus());
OperationOutcome oo = (OperationOutcome) response.getEntry().get(1).getResponse().getOutcome();
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals(IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity());
assertEquals("Resource Patient/BABABABA is not known", oo.getIssue().get(0).getDiagnostics());
}
@Test
public void testBatchCreateWithBadSearch() {
Bundle request = new Bundle();
request.setType(BundleType.BATCH);
Patient p;
p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("FOO");
request
.addEntry()
.setResource(p)
.getRequest()
.setMethod(HTTPVerb.POST)
.setUrl("Patient");
request
.addEntry()
.getRequest()
.setMethod(HTTPVerb.GET)
.setUrl("Patient?foobadparam=1");
Bundle response = mySystemDao.transaction(mySrd, request);
assertEquals(2, response.getEntry().size());
assertEquals("201 Created", response.getEntry().get(0).getResponse().getStatus());
assertThat(response.getEntry().get(0).getResponse().getLocation(), matchesPattern(".*Patient/[0-9]+.*"));
assertEquals("400 Bad Request", response.getEntry().get(1).getResponse().getStatus());
OperationOutcome oo = (OperationOutcome) response.getEntry().get(1).getResponse().getOutcome();
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(oo));
assertEquals(IssueSeverity.ERROR, oo.getIssue().get(0).getSeverity());
assertThat(oo.getIssue().get(0).getDiagnostics(), containsString("Unknown search parameter"));
}
@Test @Test
public void testTransactionCreateWithDuplicateMatchUrl02() { public void testTransactionCreateWithDuplicateMatchUrl02() {
String methodName = "testTransactionCreateWithDuplicateMatchUrl02"; String methodName = "testTransactionCreateWithDuplicateMatchUrl02";
@ -1331,20 +1452,15 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
@Test @Test
public void testTransactionDoesNotLeavePlaceholderIds() throws Exception { public void testTransactionDoesNotLeavePlaceholderIds() throws Exception {
newTxTemplate().execute(new TransactionCallbackWithoutResult() { String input;
@Override try {
protected void doInTransactionWithoutResult(TransactionStatus theStatus) { input = IOUtils.toString(getClass().getResourceAsStream("/cdr-bundle.json"), StandardCharsets.UTF_8);
String input; } catch (IOException e) {
try { fail(e.toString());
input = IOUtils.toString(getClass().getResourceAsStream("/cdr-bundle.json"), StandardCharsets.UTF_8); return;
} catch (IOException e) { }
fail(e.toString()); Bundle bundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, input);
return; mySystemDao.transaction(mySrd, bundle);
}
Bundle bundle = myFhirCtx.newJsonParser().parseResource(Bundle.class, input);
mySystemDao.transaction(mySrd, bundle);
}
});
IBundleProvider history = mySystemDao.history(null, null, null); IBundleProvider history = mySystemDao.history(null, null, null);
Bundle list = toBundle(history); Bundle list = toBundle(history);

View File

@ -177,8 +177,17 @@
GitHub user @CarthageKing for the pull request! GitHub user @CarthageKing for the pull request!
</action> </action>
<action type="fix"> <action type="fix">
Fix potential deadlock in stale search deleting task in JPA server Fix potential deadlock in stale search deleting task in JPA server, as well
as potential deadlock when executing transactions containing nested
searches when operating under extremely heavy load.
</action> </action>
<action type="add">
JPA server transaction operations now put OperationOutcome resources resulting
from actions in
<![CDATA[<code>Bundle.entry.response.outcome</code>]]>
instead of the previous
<![CDATA[<code>Bundle.entry.resource</code>]]>
<action>
</release> </release>
<release version="2.5" date="2017-06-08"> <release version="2.5" date="2017-06-08">
<action type="fix"> <action type="fix">