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}