diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
index cbf4321461c..9b615da7d0d 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java
@@ -1612,6 +1612,56 @@ public enum Pointcut implements IPointcut {
),
+ /**
+ * Storage Hook:
+ * Invoked during a FHIR transaction, immediately before processing all write operations (i.e. immediately
+ * before a database transaction will be opened)
+ *
+ * Hooks may accept the following parameters:
+ *
+ *
+ * -
+ * ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails - Contains details about the transaction that is about to start
+ *
+ * -
+ * ca.uhn.fhir.rest.api.server.storage.TransactionDetails - The outer transaction details object (since 5.0.0)
+ *
+ *
+ *
+ * Hooks should return void
.
+ *
+ */
+ STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE(void.class,
+ "ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails",
+ "ca.uhn.fhir.rest.api.server.storage.TransactionDetails"
+ ),
+
+ /**
+ * Storage Hook:
+ * Invoked during a FHIR transaction, immediately after processing all write operations (i.e. immediately
+ * after the transaction has been committed or rolled back). This hook will always be called if
+ * {@link #STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE} has been called, regardless of whether the operation
+ * succeeded or failed.
+ *
+ * Hooks may accept the following parameters:
+ *
+ *
+ * -
+ * ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails - Contains details about the transaction that is about to start
+ *
+ * -
+ * ca.uhn.fhir.rest.api.server.storage.TransactionDetails - The outer transaction details object (since 5.0.0)
+ *
+ *
+ *
+ * Hooks should return void
.
+ *
+ */
+ STORAGE_TRANSACTION_WRITE_OPERATIONS_POST(void.class,
+ "ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails",
+ "ca.uhn.fhir.rest.api.server.storage.TransactionDetails"
+ ),
+
/**
* Storage Hook:
* Invoked when a resource delete operation is about to fail due to referential integrity checks. Intended for use with {@literal ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor}.
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/TransactionWriteOperationsDetails.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/TransactionWriteOperationsDetails.java
new file mode 100644
index 00000000000..e13608bcaea
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/model/TransactionWriteOperationsDetails.java
@@ -0,0 +1,29 @@
+package ca.uhn.fhir.interceptor.model;
+
+import java.util.List;
+
+/**
+ * This is an experimental API, use with caution
+ */
+public class TransactionWriteOperationsDetails {
+
+ private List myConditionalCreateRequestUrls;
+ private List myUpdateRequestUrls;
+
+ public List getConditionalCreateRequestUrls() {
+ return myConditionalCreateRequestUrls;
+ }
+
+ public void setConditionalCreateRequestUrls(List theConditionalCreateRequestUrls) {
+ myConditionalCreateRequestUrls = theConditionalCreateRequestUrls;
+ }
+
+ public List getUpdateRequestUrls() {
+ return myUpdateRequestUrls;
+ }
+
+ public void setUpdateRequestUrls(List theUpdateRequestUrls) {
+ myUpdateRequestUrls = theUpdateRequestUrls;
+ }
+
+}
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2743-add-transaxction-semaphore-interceptor.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2743-add-transaxction-semaphore-interceptor.yaml
new file mode 100644
index 00000000000..53e9f2ff3c2
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_5_0/2743-add-transaxction-semaphore-interceptor.yaml
@@ -0,0 +1,6 @@
+---
+type: add
+issue: 2743
+title: "A new interceptor has been added for JPA servers that uses semaphores to avoid multiple concurrent
+ FHIR transactions from trying to create/update the same resource at the same time. This can improve
+ overall performance when writing many concurrent transactions since it avoids the need for retries."
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java
index f1407ffb7d8..ada43d0f291 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirHibernateJpaDialect.java
@@ -117,7 +117,8 @@ public class HapiFhirHibernateJpaDialect extends HibernateJpaDialect {
}
}
- return super.convertHibernateAccessException(theException);
+ DataAccessException retVal = super.convertHibernateAccessException(theException);
+ return retVal;
}
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
index 4ff003396da..2e53b186e0c 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java
@@ -183,10 +183,10 @@ public abstract class BaseHapiFhirDao extends BaseStora
public static final String OO_SEVERITY_INFO = "information";
public static final String OO_SEVERITY_WARN = "warning";
public static final String XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS = BaseHapiFhirDao.class.getName() + "_RESOLVED_TAG_DEFINITIONS";
+ public static final String XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS = BaseHapiFhirDao.class.getName() + "_EXISTING_SEARCH_PARAMS";
private static final Logger ourLog = LoggerFactory.getLogger(BaseHapiFhirDao.class);
private static final Map ourRetrievalContexts = new HashMap<>();
private static final String PROCESSING_SUB_REQUEST = "BaseHapiFhirDao.processingSubRequest";
- private static final String TRANSACTION_DETAILS_CACHE_KEY_EXISTING_SEARCH_PARAMS = BaseHapiFhirDao.class.getName() + "_EXISTING_SEARCH_PARAMS";
private static boolean ourValidationDisabledForUnitTest;
private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
@@ -1160,7 +1160,7 @@ public abstract class BaseHapiFhirDao extends BaseStora
// CREATE or UPDATE
- IdentityHashMap existingSearchParams = theTransactionDetails.getOrCreateUserData(TRANSACTION_DETAILS_CACHE_KEY_EXISTING_SEARCH_PARAMS, () -> new IdentityHashMap<>());
+ IdentityHashMap existingSearchParams = theTransactionDetails.getOrCreateUserData(XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, () -> new IdentityHashMap<>());
existingParams = existingSearchParams.get(entity);
if (existingParams == null) {
existingParams = new ResourceIndexedSearchParams(entity);
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
index af0c0c77b15..6d74ef0cf43 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
@@ -211,7 +211,7 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
- return myTransactionService.execute(theRequestDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails));
+ return myTransactionService.execute(theRequestDetails, theTransactionDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails));
}
@VisibleForTesting
@@ -420,10 +420,12 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) {
+ TransactionDetails transactionDetails = new TransactionDetails();
+
validateIdPresentForDelete(theId);
validateDeleteEnabled();
- return myTransactionService.execute(theRequestDetails, tx -> {
+ return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> {
DeleteConflictList deleteConflicts = new DeleteConflictList();
if (isNotBlank(theId.getValue())) {
deleteConflicts.setResourceIdMarkedForDeletion(theId);
@@ -431,7 +433,7 @@ public abstract class BaseHapiFhirResourceDao extends B
StopWatch w = new StopWatch();
- DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, new TransactionDetails());
+ DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, transactionDetails);
DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
@@ -521,13 +523,15 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequest) {
validateDeleteEnabled();
+
+ TransactionDetails transactionDetails = new TransactionDetails();
ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl);
if (resourceSearch.isDeleteExpunge()) {
return deleteExpunge(theUrl, theRequest);
}
- return myTransactionService.execute(theRequest, tx -> {
+ return myTransactionService.execute(theRequest, transactionDetails, tx -> {
DeleteConflictList deleteConflicts = new DeleteConflictList();
DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest);
DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
@@ -542,8 +546,9 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequestDetails) {
validateDeleteEnabled();
+ TransactionDetails transactionDetails = new TransactionDetails();
- return myTransactionService.execute(theRequestDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theRequestDetails));
+ return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theRequestDetails));
}
@Nonnull
@@ -1016,7 +1021,8 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) {
- return myTransactionService.execute(theRequest, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest, new TransactionDetails()));
+ TransactionDetails transactionDetails = new TransactionDetails();
+ return myTransactionService.execute(theRequest, transactionDetails, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest, transactionDetails));
}
private DaoMethodOutcome doPatch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
@@ -1132,8 +1138,9 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
validateResourceTypeAndThrowInvalidRequestException(theId);
+ TransactionDetails transactionDetails = new TransactionDetails();
- return myTransactionService.execute(theRequest, tx -> doRead(theId, theRequest, theDeletedOk));
+ return myTransactionService.execute(theRequest, transactionDetails, tx -> doRead(theId, theRequest, theDeletedOk));
}
public T doRead(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
@@ -1459,7 +1466,9 @@ public abstract class BaseHapiFhirResourceDao extends B
@Override
public Set searchForIds(SearchParameterMap theParams, RequestDetails theRequest) {
- return myTransactionService.execute(theRequest, tx -> {
+ TransactionDetails transactionDetails = new TransactionDetails();
+
+ return myTransactionService.execute(theRequest, transactionDetails, tx -> {
if (theParams.getLoadSynchronousUpTo() != null) {
theParams.setLoadSynchronousUpTo(Math.min(getConfig().getInternalSynchronousSearchSize(), theParams.getLoadSynchronousUpTo()));
@@ -1566,8 +1575,8 @@ public abstract class BaseHapiFhirResourceDao extends B
String id = theResource.getIdElement().getValue();
Runnable onRollback = () -> theResource.getIdElement().setValue(id);
- // Execute the update in a retriable transaction
- return myTransactionService.execute(theRequest, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
+ // Execute the update in a retryable transaction
+ return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
}
private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java
index 2e3c0c7c6f2..29c993d2109 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseTransactionProcessor.java
@@ -25,6 +25,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
+import ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
@@ -446,33 +447,7 @@ public abstract class BaseTransactionProcessor {
}
entries.sort(new TransactionSorter(placeholderIds));
- /*
- * 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.
- */
- TransactionCallback