001/*-
002 * #%L
003 * HAPI FHIR Storage api
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao.tx;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.api.HookParams;
024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
025import ca.uhn.fhir.interceptor.api.Pointcut;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.api.model.ResourceVersionConflictResolutionStrategy;
028import ca.uhn.fhir.jpa.dao.DaoFailureUtil;
029import ca.uhn.fhir.jpa.model.config.PartitionSettings;
030import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
031import ca.uhn.fhir.rest.api.server.RequestDetails;
032import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
033import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
035import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
036import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
037import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
038import ca.uhn.fhir.util.ICallable;
039import ca.uhn.fhir.util.SleepUtil;
040import com.google.common.annotations.VisibleForTesting;
041import jakarta.annotation.Nonnull;
042import jakarta.annotation.Nullable;
043import jakarta.persistence.PessimisticLockException;
044import org.apache.commons.lang3.Validate;
045import org.apache.commons.lang3.exception.ExceptionUtils;
046import org.hibernate.exception.ConstraintViolationException;
047import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050import org.springframework.beans.factory.annotation.Autowired;
051import org.springframework.dao.DataIntegrityViolationException;
052import org.springframework.dao.PessimisticLockingFailureException;
053import org.springframework.orm.ObjectOptimisticLockingFailureException;
054import org.springframework.transaction.PlatformTransactionManager;
055import org.springframework.transaction.TransactionStatus;
056import org.springframework.transaction.annotation.Isolation;
057import org.springframework.transaction.annotation.Propagation;
058import org.springframework.transaction.support.TransactionCallback;
059import org.springframework.transaction.support.TransactionCallbackWithoutResult;
060import org.springframework.transaction.support.TransactionOperations;
061import org.springframework.transaction.support.TransactionSynchronizationManager;
062import org.springframework.transaction.support.TransactionTemplate;
063
064import java.util.List;
065import java.util.Objects;
066import java.util.concurrent.Callable;
067
068/**
069 * @see IHapiTransactionService for an explanation of this class
070 */
071public class HapiTransactionService implements IHapiTransactionService {
072
073        public static final String XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS =
074                        HapiTransactionService.class.getName() + "_RESOLVED_TAG_DEFINITIONS";
075        public static final String XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS =
076                        HapiTransactionService.class.getName() + "_EXISTING_SEARCH_PARAMS";
077        private static final Logger ourLog = LoggerFactory.getLogger(HapiTransactionService.class);
078        private static final ThreadLocal<RequestPartitionId> ourRequestPartitionThreadLocal = new ThreadLocal<>();
079        private static final ThreadLocal<HapiTransactionService> ourExistingTransaction = new ThreadLocal<>();
080
081        /**
082         * Default value for {@link #setTransactionPropagationWhenChangingPartitions(Propagation)}
083         *
084         * @since 7.6.0
085         */
086        public static final Propagation DEFAULT_TRANSACTION_PROPAGATION_WHEN_CHANGING_PARTITIONS = Propagation.REQUIRED;
087
088        @Autowired
089        protected IInterceptorBroadcaster myInterceptorBroadcaster;
090
091        @Autowired
092        protected PlatformTransactionManager myTransactionManager;
093
094        @Autowired
095        protected IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
096
097        @Autowired
098        protected PartitionSettings myPartitionSettings;
099
100        private Propagation myTransactionPropagationWhenChangingPartitions =
101                        DEFAULT_TRANSACTION_PROPAGATION_WHEN_CHANGING_PARTITIONS;
102
103        private SleepUtil mySleepUtil = new SleepUtil();
104
105        @VisibleForTesting
106        public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) {
107                myInterceptorBroadcaster = theInterceptorBroadcaster;
108        }
109
110        @VisibleForTesting
111        public void setSleepUtil(SleepUtil theSleepUtil) {
112                mySleepUtil = theSleepUtil;
113        }
114
115        @Override
116        public IExecutionBuilder withRequest(@Nullable RequestDetails theRequestDetails) {
117                return buildExecutionBuilder(theRequestDetails);
118        }
119
120        @Override
121        public IExecutionBuilder withSystemRequest() {
122                return buildExecutionBuilder(null);
123        }
124
125        protected IExecutionBuilder buildExecutionBuilder(@Nullable RequestDetails theRequestDetails) {
126                return new ExecutionBuilder(theRequestDetails);
127        }
128
129        /**
130         * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead
131         */
132        @Deprecated
133        public <T> T execute(
134                        @Nullable RequestDetails theRequestDetails,
135                        @Nullable TransactionDetails theTransactionDetails,
136                        @Nonnull TransactionCallback<T> theCallback) {
137                return execute(theRequestDetails, theTransactionDetails, theCallback, null);
138        }
139
140        /**
141         * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead
142         */
143        @Deprecated
144        public void execute(
145                        @Nullable RequestDetails theRequestDetails,
146                        @Nullable TransactionDetails theTransactionDetails,
147                        @Nonnull Propagation thePropagation,
148                        @Nonnull Isolation theIsolation,
149                        @Nonnull Runnable theCallback) {
150                TransactionCallbackWithoutResult callback = new TransactionCallbackWithoutResult() {
151                        @Override
152                        protected void doInTransactionWithoutResult(@Nonnull TransactionStatus status) {
153                                theCallback.run();
154                        }
155                };
156                execute(theRequestDetails, theTransactionDetails, callback, null, thePropagation, theIsolation);
157        }
158
159        /**
160         * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead
161         */
162        @Deprecated
163        @Override
164        public <T> T withRequest(
165                        @Nullable RequestDetails theRequestDetails,
166                        @Nullable TransactionDetails theTransactionDetails,
167                        @Nonnull Propagation thePropagation,
168                        @Nonnull Isolation theIsolation,
169                        @Nonnull ICallable<T> theCallback) {
170
171                TransactionCallback<T> callback = tx -> theCallback.call();
172                return execute(theRequestDetails, theTransactionDetails, callback, null, thePropagation, theIsolation);
173        }
174
175        /**
176         * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead
177         */
178        @Deprecated
179        public <T> T execute(
180                        @Nullable RequestDetails theRequestDetails,
181                        @Nullable TransactionDetails theTransactionDetails,
182                        @Nonnull TransactionCallback<T> theCallback,
183                        @Nullable Runnable theOnRollback) {
184                return execute(theRequestDetails, theTransactionDetails, theCallback, theOnRollback, null, null);
185        }
186
187        /**
188         * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead
189         */
190        @Deprecated
191        @SuppressWarnings("ConstantConditions")
192        public <T> T execute(
193                        @Nullable RequestDetails theRequestDetails,
194                        @Nullable TransactionDetails theTransactionDetails,
195                        @Nonnull TransactionCallback<T> theCallback,
196                        @Nullable Runnable theOnRollback,
197                        @Nullable Propagation thePropagation,
198                        @Nullable Isolation theIsolation) {
199                return withRequest(theRequestDetails)
200                                .withTransactionDetails(theTransactionDetails)
201                                .withPropagation(thePropagation)
202                                .withIsolation(theIsolation)
203                                .onRollback(theOnRollback)
204                                .execute(theCallback);
205        }
206
207        /**
208         * @deprecated Use {@link #withRequest(RequestDetails)} with fluent call instead
209         */
210        @Deprecated
211        public <T> T execute(
212                        @Nullable RequestDetails theRequestDetails,
213                        @Nullable TransactionDetails theTransactionDetails,
214                        @Nonnull TransactionCallback<T> theCallback,
215                        @Nullable Runnable theOnRollback,
216                        @Nonnull Propagation thePropagation,
217                        @Nonnull Isolation theIsolation,
218                        RequestPartitionId theRequestPartitionId) {
219                return withRequest(theRequestDetails)
220                                .withTransactionDetails(theTransactionDetails)
221                                .withPropagation(thePropagation)
222                                .withIsolation(theIsolation)
223                                .withRequestPartitionId(theRequestPartitionId)
224                                .onRollback(theOnRollback)
225                                .execute(theCallback);
226        }
227
228        public boolean isCustomIsolationSupported() {
229                return false;
230        }
231
232        @VisibleForTesting
233        public void setRequestPartitionSvcForUnitTest(IRequestPartitionHelperSvc theRequestPartitionHelperSvc) {
234                myRequestPartitionHelperSvc = theRequestPartitionHelperSvc;
235        }
236
237        public PlatformTransactionManager getTransactionManager() {
238                return myTransactionManager;
239        }
240
241        @VisibleForTesting
242        public void setTransactionManager(PlatformTransactionManager theTransactionManager) {
243                myTransactionManager = theTransactionManager;
244        }
245
246        @VisibleForTesting
247        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
248                myPartitionSettings = thePartitionSettings;
249        }
250
251        @Nullable
252        protected <T> T doExecute(ExecutionBuilder theExecutionBuilder, TransactionCallback<T> theCallback) {
253                final RequestPartitionId requestPartitionId = theExecutionBuilder.getEffectiveRequestPartitionId();
254                RequestPartitionId previousRequestPartitionId = null;
255                if (requestPartitionId != null) {
256                        previousRequestPartitionId = ourRequestPartitionThreadLocal.get();
257                        ourRequestPartitionThreadLocal.set(requestPartitionId);
258                }
259
260                ourLog.trace("Starting doExecute for RequestPartitionId {}", requestPartitionId);
261                if (isCompatiblePartition(previousRequestPartitionId, requestPartitionId)) {
262                        if (ourExistingTransaction.get() == this && canReuseExistingTransaction(theExecutionBuilder)) {
263                                /*
264                                 * If we're already in an active transaction, and it's for the right partition,
265                                 * and it's not a read-only transaction, we don't need to open a new transaction
266                                 * so let's just add a method to the stack trace that makes this obvious.
267                                 */
268                                return executeInExistingTransaction(theCallback);
269                        }
270                }
271
272                HapiTransactionService previousExistingTransaction = ourExistingTransaction.get();
273                try {
274                        ourExistingTransaction.set(this);
275
276                        if (isRequiresNewTransactionWhenChangingPartitions()) {
277                                return executeInNewTransactionForPartitionChange(
278                                                theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId);
279                        } else {
280                                return doExecuteInTransaction(
281                                                theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId);
282                        }
283                } finally {
284                        ourExistingTransaction.set(previousExistingTransaction);
285                }
286        }
287
288        protected boolean isRequiresNewTransactionWhenChangingPartitions() {
289                return myTransactionPropagationWhenChangingPartitions == Propagation.REQUIRES_NEW;
290        }
291
292        @Override
293        public boolean isCompatiblePartition(
294                        RequestPartitionId theRequestPartitionId, RequestPartitionId theOtherRequestPartitionId) {
295                return !myPartitionSettings.isPartitioningEnabled()
296                                || !isRequiresNewTransactionWhenChangingPartitions()
297                                || Objects.equals(theRequestPartitionId, theOtherRequestPartitionId);
298        }
299
300        @Nullable
301        private <T> T executeInNewTransactionForPartitionChange(
302                        ExecutionBuilder theExecutionBuilder,
303                        TransactionCallback<T> theCallback,
304                        RequestPartitionId requestPartitionId,
305                        RequestPartitionId previousRequestPartitionId) {
306                ourLog.trace("executeInNewTransactionForPartitionChange");
307                theExecutionBuilder.myPropagation = myTransactionPropagationWhenChangingPartitions;
308                return doExecuteInTransaction(theExecutionBuilder, theCallback, requestPartitionId, previousRequestPartitionId);
309        }
310
311        private boolean isThrowableOrItsSubclassPresent(Throwable theThrowable, Class<? extends Throwable> theClass) {
312                return ExceptionUtils.indexOfType(theThrowable, theClass) != -1;
313        }
314
315        private boolean isThrowablePresent(Throwable theThrowable, Class<? extends Throwable> theClass) {
316                return ExceptionUtils.indexOfThrowable(theThrowable, theClass) != -1;
317        }
318
319        private boolean isRetriable(Throwable theThrowable) {
320                return isThrowablePresent(theThrowable, ResourceVersionConflictException.class)
321                                || isThrowablePresent(theThrowable, DataIntegrityViolationException.class)
322                                || isThrowablePresent(theThrowable, ConstraintViolationException.class)
323                                || isThrowablePresent(theThrowable, ObjectOptimisticLockingFailureException.class)
324                                // calling isThrowableOrItsSubclassPresent instead of isThrowablePresent for
325                                // PessimisticLockingFailureException, because we want to retry on its subclasses as well,  especially
326                                // CannotAcquireLockException, which is thrown in some deadlock situations which we want to retry
327                                || isThrowableOrItsSubclassPresent(theThrowable, PessimisticLockingFailureException.class)
328                                || isThrowableOrItsSubclassPresent(theThrowable, PessimisticLockException.class);
329        }
330
331        @Nullable
332        private <T> T doExecuteInTransaction(
333                        ExecutionBuilder theExecutionBuilder,
334                        TransactionCallback<T> theCallback,
335                        RequestPartitionId requestPartitionId,
336                        RequestPartitionId previousRequestPartitionId) {
337                ourLog.trace("doExecuteInTransaction");
338                try {
339                        // retry loop
340                        for (int i = 0; ; i++) {
341                                try {
342
343                                        return doExecuteCallback(theExecutionBuilder, theCallback);
344
345                                } catch (Exception e) {
346                                        int retriesRemaining = 0;
347                                        int maxRetries = 0;
348                                        boolean exceptionIsRetriable = isRetriable(e);
349                                        if (exceptionIsRetriable) {
350                                                maxRetries = calculateMaxRetries(theExecutionBuilder.myRequestDetails, e);
351                                                retriesRemaining = maxRetries - i;
352                                        }
353
354                                        // we roll back on all exceptions.
355                                        theExecutionBuilder.rollbackTransactionProcessingChanges(retriesRemaining > 0);
356
357                                        if (!exceptionIsRetriable) {
358                                                ourLog.debug("Unexpected transaction exception. Will not be retried.", e);
359                                                throw e;
360                                        } else {
361                                                // We have several exceptions that we consider retriable, call all of them "version conflicts"
362                                                ourLog.debug("Version conflict detected", e);
363
364                                                // should we retry?
365                                                if (retriesRemaining > 0) {
366                                                        // We are retrying.
367                                                        sleepForRetry(i);
368                                                } else {
369                                                        throwResourceVersionConflictException(i, maxRetries, e);
370                                                }
371                                        }
372                                }
373                        }
374                } finally {
375                        if (requestPartitionId != null) {
376                                ourRequestPartitionThreadLocal.set(previousRequestPartitionId);
377                        }
378                }
379        }
380
381        private static void throwResourceVersionConflictException(
382                        int theAttemptIndex, int theMaxRetries, Exception theCause) {
383                IBaseOperationOutcome oo = null;
384                if (theCause instanceof ResourceVersionConflictException) {
385                        oo = ((ResourceVersionConflictException) theCause).getOperationOutcome();
386                }
387
388                if (theAttemptIndex > 0) {
389                        // log if we tried to retry, but still failed
390                        String msg = "Max retries (" + theMaxRetries + ") exceeded for version conflict: " + theCause.getMessage();
391                        ourLog.info(msg);
392                        throw new ResourceVersionConflictException(Msg.code(549) + msg, theCause, oo);
393                }
394
395                throw new ResourceVersionConflictException(Msg.code(550) + theCause.getMessage(), theCause, oo);
396        }
397
398        /**
399         * Sleep a bit more each time, with 0 sleep on first retry and some random dither.
400         * @param theAttemptIndex 0-index for the first attempt, 1 for second, etc.
401         */
402        private void sleepForRetry(int theAttemptIndex) {
403                double sleepAmount = (250.0d * theAttemptIndex) * Math.random();
404                long sleepAmountLong = (long) sleepAmount;
405                ourLog.info(
406                                "About to start a transaction retry due to conflict or constraint error. Sleeping {}ms first.",
407                                sleepAmountLong);
408                mySleepUtil.sleepAtLeast(sleepAmountLong, false);
409        }
410
411        public void setTransactionPropagationWhenChangingPartitions(
412                        Propagation theTransactionPropagationWhenChangingPartitions) {
413                Objects.requireNonNull(theTransactionPropagationWhenChangingPartitions);
414                myTransactionPropagationWhenChangingPartitions = theTransactionPropagationWhenChangingPartitions;
415        }
416
417        @Nullable
418        protected <T> T doExecuteCallback(ExecutionBuilder theExecutionBuilder, TransactionCallback<T> theCallback) {
419                try {
420                        TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
421
422                        if (theExecutionBuilder.myPropagation != null) {
423                                txTemplate.setPropagationBehavior(theExecutionBuilder.myPropagation.value());
424                        }
425
426                        if (isCustomIsolationSupported()
427                                        && theExecutionBuilder.myIsolation != null
428                                        && theExecutionBuilder.myIsolation != Isolation.DEFAULT) {
429                                txTemplate.setIsolationLevel(theExecutionBuilder.myIsolation.value());
430                        }
431
432                        if (theExecutionBuilder.myReadOnly) {
433                                txTemplate.setReadOnly(true);
434                        }
435
436                        return txTemplate.execute(theCallback);
437                } catch (MyException e) {
438                        if (e.getCause() instanceof RuntimeException) {
439                                throw (RuntimeException) e.getCause();
440                        } else {
441                                throw new InternalErrorException(Msg.code(551) + e);
442                        }
443                }
444        }
445
446        private int calculateMaxRetries(RequestDetails theRequestDetails, Exception e) {
447                int maxRetries = 0;
448
449                /*
450                 * If two client threads both concurrently try to add the same tag that isn't
451                 * known to the system already, they'll both try to create a row in HFJ_TAG_DEF,
452                 * which is the tag definition table. In that case, a constraint error will be
453                 * thrown by one of the client threads, so we auto-retry in order to avoid
454                 * annoying spurious failures for the client.
455                 */
456                if (DaoFailureUtil.isTagStorageFailure(e)) {
457                        maxRetries = 3;
458                }
459
460                // Our default policy is no-retry.
461                // But we often register UserRequestRetryVersionConflictsInterceptor, which supports a retry header
462                // and retry settings on RequestDetails.
463                if (maxRetries == 0) {
464                        IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster(
465                                        this.myInterceptorBroadcaster, theRequestDetails);
466                        if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_VERSION_CONFLICT)) {
467                                HookParams params = new HookParams()
468                                                .add(RequestDetails.class, theRequestDetails)
469                                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
470                                ResourceVersionConflictResolutionStrategy conflictResolutionStrategy =
471                                                (ResourceVersionConflictResolutionStrategy) compositeBroadcaster.callHooksAndReturnObject(
472                                                                Pointcut.STORAGE_VERSION_CONFLICT, params);
473                                if (conflictResolutionStrategy != null && conflictResolutionStrategy.isRetry()) {
474                                        maxRetries = conflictResolutionStrategy.getMaxRetries();
475                                }
476                        }
477                }
478                return maxRetries;
479        }
480
481        protected class ExecutionBuilder implements IExecutionBuilder, TransactionOperations, Cloneable {
482                private final RequestDetails myRequestDetails;
483                private Isolation myIsolation;
484                private Propagation myPropagation;
485                private boolean myReadOnly;
486                private TransactionDetails myTransactionDetails;
487                private Runnable myOnRollback;
488                protected RequestPartitionId myRequestPartitionId;
489
490                protected ExecutionBuilder(RequestDetails theRequestDetails) {
491                        myRequestDetails = theRequestDetails;
492                }
493
494                @Override
495                public ExecutionBuilder withIsolation(Isolation theIsolation) {
496                        assert myIsolation == null;
497                        myIsolation = theIsolation;
498                        return this;
499                }
500
501                @Override
502                public ExecutionBuilder withTransactionDetails(TransactionDetails theTransactionDetails) {
503                        assert myTransactionDetails == null;
504                        myTransactionDetails = theTransactionDetails;
505                        return this;
506                }
507
508                @Override
509                public ExecutionBuilder withPropagation(Propagation thePropagation) {
510                        assert myPropagation == null;
511                        myPropagation = thePropagation;
512                        return this;
513                }
514
515                @Override
516                public ExecutionBuilder withRequestPartitionId(RequestPartitionId theRequestPartitionId) {
517                        assert myRequestPartitionId == null;
518                        myRequestPartitionId = theRequestPartitionId;
519                        return this;
520                }
521
522                @Override
523                public ExecutionBuilder readOnly() {
524                        myReadOnly = true;
525                        return this;
526                }
527
528                @Override
529                public ExecutionBuilder onRollback(Runnable theOnRollback) {
530                        assert myOnRollback == null;
531                        myOnRollback = theOnRollback;
532                        return this;
533                }
534
535                @Override
536                public void execute(Runnable theTask) {
537                        TransactionCallback<Void> task = tx -> {
538                                theTask.run();
539                                return null;
540                        };
541                        execute(task);
542                }
543
544                @Override
545                public <T> T execute(Callable<T> theTask) {
546                        TransactionCallback<T> callback = tx -> invokeCallableAndHandleAnyException(theTask);
547                        return execute(callback);
548                }
549
550                @Override
551                public <T> T execute(@Nonnull TransactionCallback<T> callback) {
552                        return doExecute(this, callback);
553                }
554
555                @VisibleForTesting
556                public RequestPartitionId getRequestPartitionIdForTesting() {
557                        return myRequestPartitionId;
558                }
559
560                @VisibleForTesting
561                public RequestDetails getRequestDetailsForTesting() {
562                        return myRequestDetails;
563                }
564
565                public Propagation getPropagation() {
566                        return myPropagation;
567                }
568
569                @Nullable
570                protected RequestPartitionId getEffectiveRequestPartitionId() {
571                        final RequestPartitionId requestPartitionId;
572                        if (myRequestPartitionId != null) {
573                                requestPartitionId = myRequestPartitionId;
574                        } else if (myRequestDetails != null) {
575                                requestPartitionId = myRequestPartitionHelperSvc.determineGenericPartitionForRequest(myRequestDetails);
576                        } else {
577                                requestPartitionId = null;
578                        }
579                        return requestPartitionId;
580                }
581
582                /**
583                 * This method is called when a transaction has failed, and we need to rollback any changes made to the
584                 * state of our objects in RAM.
585                 * <p>
586                 * This is used to undo any changes made during transaction resolution, such as conditional references,
587                 * placeholders, etc.
588                 *
589                 * @param theWillRetry Should be <code>true</code> if the transaction is about to be automatically retried
590                 *                     by the transaction service.
591                 */
592                void rollbackTransactionProcessingChanges(boolean theWillRetry) {
593                        if (myOnRollback != null) {
594                                myOnRollback.run();
595                        }
596
597                        if (myTransactionDetails != null) {
598                                /*
599                                 * Loop through the rollback undo actions in reverse order so we leave things in the correct
600                                 * initial state. E.g., the resource ID may get modified twice if a resource is being modified
601                                 * within a FHIR transaction: first the TransactionProcessor sets a new ID and adds a rollback
602                                 * item, and then the Resource DAO touches the ID a second time and adds a second rollback item.
603                                 */
604                                List<Runnable> rollbackUndoActions = myTransactionDetails.getRollbackUndoActions();
605                                for (int i = rollbackUndoActions.size() - 1; i >= 0; i--) {
606                                        Runnable rollbackUndoAction = rollbackUndoActions.get(i);
607                                        rollbackUndoAction.run();
608                                }
609
610                                /*
611                                 * If we're about to retry the transaction, we shouldn't clear the rollback undo actions
612                                 * because we need to re-execute them if the transaction fails a second time.
613                                 */
614                                if (!theWillRetry) {
615                                        myTransactionDetails.clearRollbackUndoActions();
616                                }
617
618                                myTransactionDetails.clearResolvedItems();
619                                myTransactionDetails.clearUserData(XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS);
620                                myTransactionDetails.clearUserData(XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS);
621                        }
622                }
623        }
624
625        /**
626         * This is just an unchecked exception so that we can catch checked exceptions inside TransactionTemplate
627         * and rethrow them outside of it
628         */
629        static class MyException extends RuntimeException {
630
631                public MyException(Throwable theThrowable) {
632                        super(theThrowable);
633                }
634        }
635
636        /**
637         * Returns true if we already have an active transaction associated with the current thread, AND
638         * either it's non-read-only or we only need a read-only transaction, AND
639         * the newly requested transaction has a propagation of REQUIRED
640         */
641        private static boolean canReuseExistingTransaction(ExecutionBuilder theExecutionBuilder) {
642                return TransactionSynchronizationManager.isActualTransactionActive()
643                                && (!TransactionSynchronizationManager.isCurrentTransactionReadOnly() || theExecutionBuilder.myReadOnly)
644                                && (theExecutionBuilder.myPropagation == null
645                                                || theExecutionBuilder.myPropagation
646                                                                == DEFAULT_TRANSACTION_PROPAGATION_WHEN_CHANGING_PARTITIONS);
647        }
648
649        @Nullable
650        private static <T> T executeInExistingTransaction(@Nonnull TransactionCallback<T> theCallback) {
651                ourLog.trace("executeInExistingTransaction");
652                // TODO we could probably track the TransactionStatus we need as a thread local like we do our partition id.
653                return theCallback.doInTransaction(null);
654        }
655
656        /**
657         * Invokes {@link Callable#call()} and rethrows any exceptions thrown by that method.
658         * If the exception extends {@link BaseServerResponseException} it is rethrown unmodified.
659         * Otherwise, it's wrapped in a {@link InternalErrorException}.
660         */
661        public static <T> T invokeCallableAndHandleAnyException(Callable<T> theTask) {
662                try {
663                        return theTask.call();
664                } catch (BaseServerResponseException e) {
665                        throw e;
666                } catch (Exception e) {
667                        throw new InternalErrorException(Msg.code(2223) + e.getMessage(), e);
668                }
669        }
670
671        public static <T> T executeWithDefaultPartitionInContext(@Nonnull ICallable<T> theCallback) {
672                RequestPartitionId previousRequestPartitionId = ourRequestPartitionThreadLocal.get();
673                ourRequestPartitionThreadLocal.set(RequestPartitionId.defaultPartition());
674                try {
675                        return theCallback.call();
676                } finally {
677                        ourRequestPartitionThreadLocal.set(previousRequestPartitionId);
678                }
679        }
680
681        public static RequestPartitionId getRequestPartitionAssociatedWithThread() {
682                return ourRequestPartitionThreadLocal.get();
683        }
684
685        /**
686         * Throws an {@link IllegalArgumentException} if a transaction is active
687         */
688        public static void noTransactionAllowed() {
689                Validate.isTrue(
690                                !TransactionSynchronizationManager.isActualTransactionActive(),
691                                "Transaction must not be active but found an active transaction");
692        }
693
694        /**
695         * Throws an {@link IllegalArgumentException} if no transaction is active
696         */
697        public static void requireTransaction() {
698                Validate.isTrue(
699                                TransactionSynchronizationManager.isActualTransactionActive(),
700                                "Transaction required here but no active transaction found");
701        }
702}