001package ca.uhn.fhir.jpa.dao.tx; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy; 028import ca.uhn.fhir.jpa.dao.DaoFailureUtil; 029import ca.uhn.fhir.rest.api.server.RequestDetails; 030import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 031import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 032import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 033import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 034import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 035import ca.uhn.fhir.util.TestUtil; 036import com.google.common.annotations.VisibleForTesting; 037import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040import org.springframework.beans.factory.annotation.Autowired; 041import org.springframework.dao.DataIntegrityViolationException; 042import org.springframework.transaction.PlatformTransactionManager; 043import org.springframework.transaction.support.TransactionCallback; 044import org.springframework.transaction.support.TransactionTemplate; 045 046import javax.annotation.Nullable; 047import javax.annotation.PostConstruct; 048 049public class HapiTransactionService { 050 051 public static final String XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS = HapiTransactionService.class.getName() + "_RESOLVED_TAG_DEFINITIONS"; 052 public static final String XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS = HapiTransactionService.class.getName() + "_EXISTING_SEARCH_PARAMS"; 053 private static final Logger ourLog = LoggerFactory.getLogger(HapiTransactionService.class); 054 @Autowired 055 protected IInterceptorBroadcaster myInterceptorBroadcaster; 056 @Autowired 057 protected PlatformTransactionManager myTransactionManager; 058 protected TransactionTemplate myTxTemplate; 059 060 @VisibleForTesting 061 public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) { 062 myInterceptorBroadcaster = theInterceptorBroadcaster; 063 } 064 065 @VisibleForTesting 066 public void setTransactionManager(PlatformTransactionManager theTransactionManager) { 067 myTransactionManager = theTransactionManager; 068 } 069 070 @PostConstruct 071 public void start() { 072 myTxTemplate = new TransactionTemplate(myTransactionManager); 073 } 074 075 public <T> T execute(RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, TransactionCallback<T> theCallback) { 076 return execute(theRequestDetails, theTransactionDetails, theCallback, null); 077 } 078 079 public <T> T execute(RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, TransactionCallback<T> theCallback, Runnable theOnRollback) { 080 081 for (int i = 0; ; i++) { 082 try { 083 084 return doExecuteCallback(theCallback); 085 086 } catch (ResourceVersionConflictException | DataIntegrityViolationException e) { 087 ourLog.debug("Version conflict detected", e); 088 089 if (theOnRollback != null) { 090 theOnRollback.run(); 091 } 092 093 int maxRetries = 0; 094 095 /* 096 * If two client threads both concurrently try to add the same tag that isn't 097 * known to the system already, they'll both try to create a row in HFJ_TAG_DEF, 098 * which is the tag definition table. In that case, a constraint error will be 099 * thrown by one of the client threads, so we auto-retry in order to avoid 100 * annoying spurious failures for the client. 101 */ 102 if (DaoFailureUtil.isTagStorageFailure(e)) { 103 maxRetries = 3; 104 } 105 106 if (maxRetries == 0) { 107 HookParams params = new HookParams() 108 .add(RequestDetails.class, theRequestDetails) 109 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 110 ResourceVersionConflictResolutionStrategy conflictResolutionStrategy = (ResourceVersionConflictResolutionStrategy) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_VERSION_CONFLICT, params); 111 if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) { 112 maxRetries = conflictResolutionStrategy.getMaxRetries(); 113 } 114 } 115 116 if (i < maxRetries) { 117 theTransactionDetails.getRollbackUndoActions().forEach(t -> t.run()); 118 theTransactionDetails.clearRollbackUndoActions(); 119 theTransactionDetails.clearResolvedItems(); 120 theTransactionDetails.clearUserData(XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS); 121 theTransactionDetails.clearUserData(XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS); 122 double sleepAmount = (250.0d * i) * Math.random(); 123 long sleepAmountLong = (long) sleepAmount; 124 TestUtil.sleepAtLeast(sleepAmountLong, false); 125 126 ourLog.info("About to start a transaction retry due to conflict or constraint error. Sleeping {}ms first.", sleepAmountLong); 127 continue; 128 } 129 130 IBaseOperationOutcome oo = null; 131 if (e instanceof ResourceVersionConflictException) { 132 oo = ((ResourceVersionConflictException) e).getOperationOutcome(); 133 } 134 135 if (maxRetries > 0) { 136 String msg = "Max retries (" + maxRetries + ") exceeded for version conflict: " + e.getMessage(); 137 ourLog.info(msg, maxRetries); 138 throw new ResourceVersionConflictException(Msg.code(549) + msg); 139 } 140 141 throw new ResourceVersionConflictException(Msg.code(550) + e.getMessage(), e, oo); 142 } 143 } 144 145 } 146 147 @Nullable 148 protected <T> T doExecuteCallback(TransactionCallback<T> theCallback) { 149 try { 150 return myTxTemplate.execute(theCallback); 151 } catch (MyException e) { 152 if (e.getCause() instanceof RuntimeException) { 153 throw (RuntimeException) e.getCause(); 154 } else { 155 throw new InternalErrorException(Msg.code(551) + e); 156 } 157 } 158 } 159 160 /** 161 * This is just an unchecked exception so that we can catch checked exceptions inside TransactionTemplate 162 * and rethrow them outside of it 163 */ 164 static class MyException extends RuntimeException { 165 166 public MyException(Throwable theThrowable) { 167 super(theThrowable); 168 } 169 } 170}